feat(profiling): Add Android ProfilingManager (Perfetto) support#5251
feat(profiling): Add Android ProfilingManager (Perfetto) support#5251
Conversation
|
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. This PR will not appear in the changelog. 🤖 This preview updates automatically when you update the PR. |
Sentry Build Distribution
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d15471f | 369.38 ms | 459.08 ms | 89.70 ms |
| 9fbb112 | 359.71 ms | 421.85 ms | 62.14 ms |
| 1df7eb6 | 397.04 ms | 429.64 ms | 32.60 ms |
| ee747ae | 400.46 ms | 423.61 ms | 23.15 ms |
| ee747ae | 357.79 ms | 421.84 ms | 64.05 ms |
| b193867 | 331.08 ms | 397.06 ms | 65.98 ms |
| a416a65 | 333.78 ms | 410.37 ms | 76.59 ms |
| b193867 | 319.59 ms | 403.09 ms | 83.50 ms |
| b6cfb57 | 372.92 ms | 507.77 ms | 134.85 ms |
| ee35ac3 | 346.83 ms | 435.48 ms | 88.65 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| 9fbb112 | 1.58 MiB | 2.11 MiB | 539.18 KiB |
| 1df7eb6 | 1.58 MiB | 2.10 MiB | 532.97 KiB |
| ee747ae | 1.58 MiB | 2.10 MiB | 530.95 KiB |
| ee747ae | 1.58 MiB | 2.10 MiB | 530.95 KiB |
| b193867 | 1.58 MiB | 2.19 MiB | 620.00 KiB |
| a416a65 | 1.58 MiB | 2.12 MiB | 555.26 KiB |
| b193867 | 1.58 MiB | 2.19 MiB | 620.00 KiB |
| b6cfb57 | 1.58 MiB | 2.28 MiB | 718.80 KiB |
| ee35ac3 | 1.58 MiB | 2.13 MiB | 558.77 KiB |
sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Serialize uses field instead of getter for
meta_lengthSentryEnvelopeItemHeader.serialize()now usesgetMetaLength()(captured once in a local) so callable-backed Perfetto chunks correctly emitmeta_lengthin envelope headers.
Or push these changes by commenting:
@cursor push 56eb859503
Preview (56eb859503)
diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItemHeader.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItemHeader.java
--- a/sentry/src/main/java/io/sentry/SentryEnvelopeItemHeader.java
+++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItemHeader.java
@@ -219,8 +219,9 @@
if (itemCount != null) {
writer.name(JsonKeys.ITEM_COUNT).value(itemCount);
}
- if (metaLength != null) {
- writer.name(JsonKeys.META_LENGTH).value(metaLength);
+ final @Nullable Integer metaLengthValue = getMetaLength();
+ if (metaLengthValue != null) {
+ writer.name(JsonKeys.META_LENGTH).value(metaLengthValue);
}
writer.name(JsonKeys.LENGTH).value(getLength());
if (unknown != null) {This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java
Show resolved
Hide resolved
4e173d3 to
b4b28c9
Compare
sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java
Show resolved
Hide resolved
sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java
Show resolved
Hide resolved
Adds a new boolean option `useProfilingManager` that gates whether the SDK uses Android's ProfilingManager API (API 35+) for Perfetto-based profiling. On devices below API 35 where ProfilingManager is not available, no profiling data is collected — the legacy Debug-based profiler is not used as a fallback. Wired through SentryOptions and ManifestMetadataReader (AndroidManifest meta-data). Defaults to false (opt-in). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds UI controls to the profiling sample activity for testing both legacy and Perfetto profiling paths. Enables useProfilingManager flag in the sample manifest for API 35+ testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show active profiler status line with (i) info button to show SDK config (sample rates, lifecycle mode, use-profiling-manager) - Conditionally show Start(Manual) or Start(Transaction) button based on profileLifecycle mode, since each is a no-op in the wrong mode - Hide duration seekbar in MANUAL mode (only affects transaction length) - Remove inline profiling result TextView; show results via Toast and in the (i) dialog instead - Apply AppTheme.Main to fix edge-to-edge clipping on API 35+ - Add indices to the bitmap list items so user can see the list view jumping around Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ager is set When useProfilingManager is true, SentryPerformanceProvider now skips creating the legacy Debug-based profiler at app start. This ensures AndroidOptionsInitializer creates a Perfetto profiler instead, without needing special handover logic between the two profiling engines. The useProfilingManager flag is persisted in SentryAppStartProfilingOptions (written at end of Sentry.init(), read on next app launch) so the decision is available before SDK initialization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> squash into options commit
…Profiler Introduces PerfettoProfiler, which uses Android's ProfilingManager system service (API 35+) for Perfetto-based stack sampling. When useProfilingManager is enabled, AndroidContinuousProfiler selects PerfettoProfiler at init time via createWithProfilingManager(); on older devices no profiling data is collected and the legacy Debug-based profiler is not used as a fallback. Key changes: - PerfettoProfiler: calls requestProfiling(STACK_SAMPLING), waits for ProfilingResult via CountDownLatch, reads .pftrace via getResultFilePath() - AndroidContinuousProfiler: factory methods createLegacy() / createWithProfilingManager() replace the public constructor; init() split into initLegacy() / initProfilingManager() for clarity; stopFuture uses cancel(false) to avoid interrupting the Perfetto result wait - AndroidOptionsInitializer: branches on isUseProfilingManager() to select the correct factory method - SentryEnvelopeItem: fromPerfettoProfileChunk() builds a single envelope item with meta_length header separating JSON metadata from binary .pftrace - SentryEnvelopeItemHeader: adds metaLength field for the binary format - ProfileChunk: adds contentType and version fields; Builder.setContentType() - SentryClient: routes Perfetto chunks to fromPerfettoProfileChunk() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eader SentryEnvelopeItemHeader.serialize() checked the raw metaLength field instead of calling getMetaLength(), so the callable path used by Perfetto profile chunks was never invoked and meta_length was never written to the envelope header JSON. Refactor SentryEnvelopeItemHeader to remove the metaLength field entirely — all constructors now store a single calculateMetaLength callable. Eager constructors (deserializer) wrap the Integer in a lambda. All constructors delegate to one private primary constructor. In fromPerfettoProfileChunk, replace the round-trip through ProfileChunk.setMetaLength/getMetaLength with a local AtomicInteger shared between the CachedItem lambda and the header callable, keeping meta_length as an envelope transport concern rather than in ProfileChunk Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uousProfiler Separate the Perfetto/ProfilingManager profiling backend into its own IContinuousProfiler implementation to keep the two backends independent. - AndroidContinuousProfiler is restored to legacy-only (no Perfetto fields, no conditional branches, no @SuppressLint annotations) - PerfettoContinuousProfiler is a new @RequiresApi(35) class that delegates to PerfettoProfiler and always sets content_type="perfetto" - AndroidOptionsInitializer branches on useProfilingManager to pick the right implementation - Consistent locking: startInternal/stopInternal both require caller to hold the lock, with callers wrapped accordingly - Renamed rootSpanCounter to activeTraceCount in PerfettoContinuousProfiler - Extracted tryResolveScopes/onScopesAvailable from initScopes in both classes - Fixed duplicate listener bug in PerfettoProfiler (was using local lambda instead of class-scope profilingResultListener)
…miting Verify that onRateLimitChanged stops the profiler, resets profiler/chunk IDs, and logs the expected warning. Run with: ./gradlew :sentry-android-core:testDebugUnitTest --tests "io.sentry.android.core.PerfettoContinuousProfilerTest"
…ousProfiler Currently PerfettoContinuousProfiler is not doing app-start profiling. Because of this, scopes are always available. Remove the legacy patterns that were carried over from AndroidContinuousProfiler: - Replace tryResolveScopes/onScopesAvailable with resolveScopes() that returns @NotNull IScopes and logs an error if scopes is unexpectedly unavailable - Remove payloadBuilders list, payloadLock, and sendChunks() buffering; replace with sendChunk() that sends a single chunk immediately - Remove scopes != null guards and SentryNanotimeDate fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Lock in isRunning(), getProfilerId(), getChunkId() so all public getters are synchronized with writes in startInternal/stopInternal - Lock in reevaluateSampling() - Remove volatile from shouldSample; all accesses are now under the same lock - Replace ArrayDeque with ConcurrentLinkedDeque in PerfettoProfiler for frame measurement collections; these are written by the FrameMetrics HandlerThread and read by the executor thread in endAndCollect() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…startProfiler shouldStop was always true, so we'd ever only collect max one profile session (startProfiler(MANUAL)->stopProfiler(MANUAL) plus one more chunk before profiler stopped itself. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…artProfiler Same fix as PerfettoContinuousProfiler — shouldStop was never reset to false after stopProfiler, so a stop/start cycle would leave shouldStop=true and silently stop the profiler after one chunk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
b4b28c9 to
83b1f1a
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 10c415f. Configure here.
| () -> { | ||
| try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { | ||
| stopInternal(true); | ||
| } |
There was a problem hiding this comment.
Lock held during blocking endAndCollect risks ANR
Medium Severity
The scheduled chunk timer acquires the reentrant lock and then calls stopInternal, which calls perfettoProfiler.endAndCollect(). That method calls resultLatch.await(5, SECONDS), blocking the thread while the lock is held. Any call to startProfiler, stopProfiler, isRunning, getChunkId, getProfilerId, or close from another thread (including the main/UI thread) will be blocked for up to 5 seconds. On Android, a 5-second main-thread block causes an ANR. The legacy AndroidContinuousProfiler doesn't have this problem because its endAndCollect is non-blocking.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 10c415f. Configure here.
There was a problem hiding this comment.
love this cursor bot thing. was planning this work already -> see todos
| null, | ||
| profileChunk.getPlatform(), | ||
| null, | ||
| (Callable<Integer>) metaLength::get); |
There was a problem hiding this comment.
getMetaLength returns -1 instead of null before evaluation
Medium Severity
The metaLength is initialized as new AtomicInteger(-1), and the callable metaLength::get always returns a non-null Integer (autoboxed from int). So getMetaLength() never returns null when the callable is set — it returns -1 before data evaluation. In SentryEnvelopeItemHeader.serialize(), getMetaLength() is called before getLength(), so if the header is ever serialized without item.getData() being called first, meta_length: -1 is written. The current JsonSerializer flow protects against this, but the contract of returning @Nullable Integer is violated.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 10c415f. Configure here.



📜 Description
Adds opt-in
useProfilingManageroption that uses Android'sProfilingManagerAPI (API 35+) for Perfetto-based stack sampling instead of the legacyDebug.startMethodTracingSamplingengine.PerfettoContinuousProfileris mutually exclusive withAndroidContinuousProfiler— the option gates which implementation is created at init time. The legacy path is unchanged.Why a new ContinuousProfiler class
The first few commits wire the Perfetto backend into
AndroidContinuousProfiler(ported from an earlier branch). The later commits extract a standalonePerfettoContinuousProfilerbecause:AndroidContinuousProfilerhas a lot of state and theif (perfetto) { ... } else { legacy }branching makes paths hard to follow => the two codepaths will never be active at the same time.startProfiler,stopProfiler,close(true),reevaluateSamplingConcurrentLinkedDeque(code)new HandlerThread("...SentryFrameMetricsCollector")stopInternal(true)— scheduled chunk timer. AlsosendChunk()submits work here.new Thread(r, "SentryExecutorServiceThreadFactory-" + cnt++)onRateLimitChanged— inline callback (code)new Thread(r, "SentryAsyncConnection-" + cnt++)onRateLimitChanged— rate limit expiry (code);close(false)— session timeout (code); not a direct caller butCompositePerformanceCollectorrunssetup()andcollect()every 100ms (code)new Timer(true)in RateLimiter, LifecycleWatcher, CompositePerformanceCollectorstartProfiler(TRACE)(code),stopProfiler(TRACE)(code)PerfettoContinuousProfiler+PerfettoProfilermeans fewer@SuppressLint("NewApi")scattered throughAndroidContinuousProfilerKey files
SentryOptions.useProfilingManager— opt-in flag, readable from manifestio.sentry.profiling.use-profiling-managerPerfettoContinuousProfiler—IContinuousProfilerimpl,@RequiresApi(35), delegates toPerfettoProfilerPerfettoProfiler— wrapsProfilingManager.requestProfiling(PROFILING_TYPE_STACK_SAMPLING, ...)SentryEnvelopeItem.fromPerfettoProfileChunk()— binary envelope format withmeta_lengthheaderAndroidContinuousProfiler— legacy only, no Perfetto references💡 Motivation and Context
Android's
ProfilingManager(API 35+) provides OS-level Perfetto stack sampling. The legacyDebug.startMethodTracingSamplingpath is preserved unchanged. On API < 35 withuseProfilingManager=true, profiling is disabled (no silent fallback).💚 How did you test it?
content_type: "perfetto".pftracefiles and inspected in Perfetto UIPerfettoContinuousProfilerTest(rate limiting),SentryOptionsTest,ManifestMetadataReaderTest,SentryEnvelopeItemTestJAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew :sentry-android-core:testDebugUnitTest --tests "io.sentry.android.core.PerfettoContinuousProfilerTest"📝 Checklist
sendDefaultPIIis enabled.Testing locally
🔮 Next steps
PROFILING_TYPE_STACK_SAMPLINGtraces (ProfilingManager doesn't seem to includelinux.process_statsdata source)