Skip to content

[Fix][issue-227] Native agent 10x start up - #379

Open
buongarzoni wants to merge 5 commits into
masterfrom
fix/issue-227/native-agent-10x-start-up
Open

[Fix][issue-227] Native agent 10x start up#379
buongarzoni wants to merge 5 commits into
masterfrom
fix/issue-227/native-agent-10x-start-up

Conversation

@buongarzoni

Copy link
Copy Markdown
Collaborator

Description of the change

The JVMTI native agent made JVM startup 2–10x slower (reporter saw 15s → 400s on a Spring Boot app). Three independent causes:

  1. The Exception event was armed at Agent_OnLoad for all threads, so every exception thrown during VM and framework bootstrap entered the callback, tens of thousands of them, before ThrowableCache was even loadable.
  2. FindClass could not resolve ThrowableCache. Called from a JVMTI callback with no Java frame, it resolves against the system classloader, so inside a Spring Boot fat jar it failed permanently, the agent was pure overhead and captured nothing. Each failure also ran a Java upcall to build the error message, at every log level.
  3. Nothing was cached, and locals were captured for every frame, including the 100+ framework frames nobody wants locals from.

Changes

  • Activation is gated on a ClassPrepare callback for com.rollbar.jvmti.ThrowableCache; the supplied jclass is cached as a global ref, so FindClass is never used for Rollbar classes again. This fixes fat ar and container classloaders.
  • All classes, method IDs, and boxing helpers resolved once instead of per-event and per-frame.
  • Native app package pre filter (memoized per jmethodID) and locals capture restricted to app frames. Frame array stays 1:1 with the stack so BodyFactory.frames() is unaffected.
  • Re entrancy guard, PushLocalFrame/PopLocalFrame, frame cap.
  • ThrowableCache.appPackages is now copy-on-write volatile (was an unsynchronized HashSet read from arbitrary throwing threads), plus a version counter the agent reads cheaply. Older agents/jars degrade gracefully in both directions.
  • GetEnv requests JVMTI 1.2 rather than the build JDK's version, a JDK 21 build previously refused to load on JDK 17.
  • Separately: the JSON serializer no longer throws when a captured local's toString() returns null or throws. This surfaced only once locals capture started working.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Maintenance
  • New release

Related issues

Shortcut stories and GitHub issues (delete irrelevant)

Checklists

Development

  • Lint rules pass locally
  • The code changed/added as part of this pull request has been covered with tests
  • All tests related to the changed code pass in development

Code review

  • This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached
  • "Ready for review" label attached to the PR and reviewers assigned
  • Issue from task tracker has a link to this pull request
  • Changes have been reviewed by at least one other engineer

@buongarzoni buongarzoni self-assigned this Jul 30, 2026
@buongarzoni buongarzoni added this to the v2.4.0 milestone Jul 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b9a3a65af

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +133 to +139
let returned = returned.max(0).min(MAX_SCAN_FRAMES) as usize;
for frame in frames.iter().take(returned) {
if is_app_method(jvmti_env, frame.method)? {
return Ok(true);
}
}
Ok(false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when the scan hits the frame cap

When an exception has more than 128 frames and the first configured app frame is deeper than this scan window, stack_has_app_frame returns false, so decide skips shouldCacheThrowable entirely even though the Java filter scans the full stack and would have cached it. In this capped case the native scan has only proven “not in the first 128 frames”, so it should return the Java fallback instead of NoAppFrame.

Useful? React with 👍 / 👎.

Comment on lines +59 to +66
let frames = build_stack_trace_frames(
jvmti_env,
jni_env,
capture,
thread,
start_depth,
num_frames.min(MAX_FRAMES),
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve stack alignment when capping cached frames

For throwables with more than 64 stack frames, this stores only the top 64 CacheFrames while BodyFactory.frames() starts matching from cachedFrames.length - 1 against the deepest Java stack element; that drains the shortened array before reaching the captured top frames, so locals are not applied at all (and shouldCacheThrowable will keep recapturing the same deep throwable because numFrames > existing.length). The cap needs to keep alignment with the Java stack or the Java merge logic needs to understand truncated arrays.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native agent adds 10x startup time to Spring Boot app

1 participant