Skip to content

feat(profiling): Send Hermes samples as UI profile chunks on Android - #6966

Open
alwx wants to merge 5 commits into
mainfrom
alwx/feature/6894
Open

alwx wants to merge 5 commits into
mainfrom
alwx/feature/6894

Conversation

@alwx

@alwx alwx commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

On Android 15 (API 35) and newer, uiProfilingIntegration now runs the Hermes sampler during the native profiler session. Every 60 seconds, and when the session stops, it sends the samples as a javascript profile chunk with the native profiler_id, so Sentry merges JS and native frames into one profile. The Hermes samples use their own thread id (2^32), and spans point to it, because the native Perfetto profiler samples the JS thread under the same OS thread id.

💡 Motivation and Context

Closes #6894. UI profiles on Android showed only native frames. The feature is limited to API 35+, because older devices send legacy Android chunks, and vroom cannot merge them with sample-format chunks. iOS needs the profiler id from sentry-cocoa first.

💚 How did you test it?

New Jest tests for the chunk builder, the integration, and the wrapper. I ran the sample app on an API 36 emulator with profilingOptions (trace lifecycle). Perfetto and the Hermes sampler ran together, chunks were sent every 60 seconds, and both chunks passed Relay's sample v2 parse and normalize rules. The merged profile in the Sentry UI still needs a check.

📝 Checklist

  • I added tests to verify changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.
  • No breaking changes.

🔮 Next steps

Add iOS when sentry-cocoa exposes the current profiler id.

alwx added a commit that referenced this pull request Oct 29, 2026
Replace the PR number placeholder in the CHANGELOG entry with #6966.
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

⚪ None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(profiling): Send Hermes samples as UI profile chunks on Android by alwx in #6966
  • docs(core): Drop stale logger.warn note from AGENTS.md by antonis and claude in #6968
  • chore(deps): update JavaScript SDK to v10.78.0 by antonis and claude in #6973
  • chore(deps): Bump getsentry/craft from 2.35.0 to 3.5.0 by alwx in #6983
  • chore(deps): update Maestro to v2.12.1 by github-actions in #6981
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 3.0.0 to 3.4.1 by alwx with dependabot in #6979
  • chore(deps): bump node-forge from 1.4.0 to 1.4.2 by dependabot in #6982
  • chore(deps): bump getsentry/github-workflows/validate-pr from 36c729264d2edc29ebae61950c50e1e9f043ad7e to 01bcb7fe69bb48bacd83e675840e3dc95b33c730 by dependabot in #6978
  • chore(deps): bump ruby/setup-ruby from 1.334.0 to 1.336.0 by dependabot in #6977
  • chore(deps): bump gradle/actions/setup-gradle from 6.4.0 to 6.4.1 by dependabot in #6976
  • fix(ios): Honor Sentry.Mask in error screenshots by antonis and claude in #6974
  • fix(android): Restore enableLogs option mapping by antonis and claude in #6967
  • chore(deps): update Wizard to v9.1.0 by github-actions in #6972
  • chore(deps): update Sentry Android Gradle Plugin to v6.25.0 by github-actions in #6971

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 39616cf

@alwx
alwx marked this pull request as ready for review October 29, 2026 14:41
@alwx
alwx requested review from a team and lucas-zimerman as code owners October 29, 2026 14:41
@alwx
alwx requested review from antonis and buenaflor and removed request for a team October 29, 2026 14:41

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 13c7c2d. Configure here.

@antonis antonis added ready-to-merge Triggers the full CI test suite and removed ready-to-merge Triggers the full CI test suite labels Oct 30, 2026

@antonis antonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for your work on this @alwx 🚀
Overall LGTM. I've also run a few tests on an emulator and I think the result is the expected (Sentry link).

The CI looks stuck at this point. Merging the latest main or closing/opening the PR should unstuck it 🤞

Also looping in @markushi who has more context on the Perfetto Android implementation in case he has any feedback.

alwx added 5 commits November 3, 2026 15:40
UI profiling on Android showed only native frames, so the profile of a transaction had no JavaScript functions. The integration now runs the Hermes sampler while the native profiler session runs. Every 60 seconds, and when the session stops, it sends the samples as a JavaScript profile_chunk with the native profiler_id. Sentry merges all chunks of a profiler_id into one profile.

The feature is limited to Android API 35 and newer. Older devices use the legacy profiler, whose chunks vroom cannot merge with sample-format chunks (it returns HTTP 400). iOS needs the profiler id from sentry-cocoa first.

The native Perfetto profiler samples the JS thread under its OS thread id too. Hermes uses the same id, so the Hermes samples get their own thread id (2^32) and spans point to it. Hermes timestamps use a monotonic clock, so each chunk is anchored to the Unix time when the sampler started.

The sampler starts only after the native SDK returns a profiler id. It does not run when the session is not sampled or when transaction profiling (profilesSampleRate) uses the sampler. NATIVE.stopProfiling no longer warns about a missing native profile when only Hermes was started.
The close handler collected the current Hermes chunk but kept the profile session. If NATIVE.startProfiler() resolved after close, the profiler id passed the session check and started the Hermes sampler and its 60 second restart timer again. The close handler now clears the session, so a late profiler id is ignored.
The UI profiling integration turned off Hermes chunks whenever profilesSampleRate was set, also when it was 0. At rate 0 the transaction profiler never starts the Hermes sampler, and the React Native SDK does not pass profilesSampleRate to sentry-java, so the native UI profiler still runs. Hermes chunks are now turned off only for a positive profilesSampleRate.
@sentry/core emits the close hook after it disables the client, so sendEnvelope drops anything sent from there. The close handler built and sent the last Hermes chunk anyway, which only wasted a conversion. It now stops the Hermes sampler without building a chunk. Stopping and sending are split into stopHermesSampler and collectHermesChunk for this.
The last Hermes chunk was lost when the client closed, because @sentry/core disables the client before it emits close. Client.close() flushes first, and flush emits its hook while the client is still enabled and before the transport drains. The integration now sends the current chunk and restarts the sampler on flush, so close sends the last chunk, and Sentry.flush() and the fatal error handler send the JS samples collected so far. The close handler only stops the sampler.
@alwx
alwx force-pushed the alwx/feature/6894 branch from 7837007 to 39616cf Compare November 3, 2026 14:42
@sentry

sentry Bot commented Nov 3, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Sentry RN io.sentry.reactnative.sample 8.33.0 (113) Release

⚙️ sentry-react-native Build Distribution Settings

Comment on lines 295 to 300
client.on('close', () => {
clearStopTimeout();
stopHermesSampler();
session = undefined;
resolveAppStartSession?.(null);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

close clears session without stopping the native UI profiler

On close, stop the native profiler (e.g. call NATIVE.stopProfiler() / stopNativeProfiler()) before clearing session; otherwise Perfetto can keep running after SDK shutdown and later profiler.stopProfiler() cannot stop it.

Evidence
  • The new close handler calls stopHermesSampler() and sets session = undefined, but never calls NATIVE.stopProfiler().
  • stopNativeProfiler() is the only helper that stops native (NATIVE.stopProfiler()), and it only runs when syncNativeProfiler() sees session && !shouldRun().
  • After session is cleared on close, syncNativeProfiler() will not stop an in-flight native session even if profiler.stopProfiler() sets isManualRequested = false.
  • The close test asserts Hermes start/stop counts only, not NATIVE.stopProfiler.

Identified by Warden · code-review · X9G-Q5G

Comment on lines +377 to +385
it('does not start the Hermes sampler when the client closes before the native start resolves', async () => {
const client = setupClient({ lifecycle: 'manual' });

profiler.startProfiler();
client.emit('close');
await jest.advanceTimersByTimeAsync(0);

expect(NATIVE.startProfiling).not.toHaveBeenCalled();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Client close leaves the profiling integration able to start another session

The close handler stops the current Hermes sampler and clears session, but leaves the integration's run conditions and callbacks active. If afterInit fires later, the handler can call syncNativeProfiler() while isManualRequested is still true; with no session, that starts a new native session and can restart Hermes sampling after close. Make close terminal for profiling by clearing pending run state and guarding later callbacks from starting sessions.

Evidence
  • The close handler clears the timer and sampler, sets session to undefined, and resolves the app-start promise, but leaves isManualRequested and the resolver unchanged.
  • The afterInit handler calls syncNativeProfiler() after resolving a pending app-start session; for manual lifecycle, shouldRun() still returns true after close.
  • With session cleared, syncNativeProfiler() creates a new session and calls startHermesChunks(). The existing close-before-start-resolution test does not cover a later afterInit invoking this path.

Identified by Warden · find-bugs · 84X-D2R

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

Labels

ready-to-merge Triggers the full CI test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adopt Perfetto format for Android 17 profiling

2 participants