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. |
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6ea4329 | 309.06 ms | 353.48 ms | 44.42 ms |
| d15471f | 286.65 ms | 314.68 ms | 28.03 ms |
| 9054d65 | 330.94 ms | 403.24 ms | 72.30 ms |
| 94bff8d | 313.23 ms | 352.77 ms | 39.54 ms |
| d364ace | 382.77 ms | 443.21 ms | 60.44 ms |
| ee747ae | 358.21 ms | 389.41 ms | 31.20 ms |
| 5f14e5d | 325.76 ms | 368.32 ms | 42.56 ms |
| b6cfb57 | 372.92 ms | 507.77 ms | 134.85 ms |
| d15471f | 361.89 ms | 378.07 ms | 16.18 ms |
| d15471f | 379.40 ms | 470.76 ms | 91.36 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6ea4329 | 1.58 MiB | 2.29 MiB | 719.82 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| 9054d65 | 1.58 MiB | 2.29 MiB | 723.38 KiB |
| 94bff8d | 1.58 MiB | 2.20 MiB | 635.37 KiB |
| d364ace | 1.58 MiB | 2.11 MiB | 539.75 KiB |
| ee747ae | 1.58 MiB | 2.10 MiB | 530.95 KiB |
| 5f14e5d | 1.58 MiB | 2.19 MiB | 620.00 KiB |
| b6cfb57 | 1.58 MiB | 2.28 MiB | 718.80 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
Previous results on branch: claude/dreamy-solomon
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7da193d | 319.39 ms | 375.24 ms | 55.85 ms |
| 0db7260 | 329.68 ms | 375.67 ms | 45.99 ms |
| cee9ee9 | 342.21 ms | 405.42 ms | 63.21 ms |
| c26f799 | 319.88 ms | 358.02 ms | 38.14 ms |
| b3c0878 | 316.40 ms | 345.51 ms | 29.11 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 7da193d | 0 B | 0 B | 0 B |
| 0db7260 | 0 B | 0 B | 0 B |
| cee9ee9 | 0 B | 0 B | 0 B |
| c26f799 | 0 B | 0 B | 0 B |
| b3c0878 | 0 B | 0 B | 0 B |
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>
b4b28c9 to
83b1f1a
Compare
| () -> { | ||
| 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.
10c415f to
873bad3
Compare
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 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Unconditional
shouldStopreset causes unintended profiler continuationshouldStopis now reset only in TRACE starts and in MANUAL starts that actually proceed, so a skipped MANUAL start no longer clears a pending TRACE stop.
- ✅ Fixed: Missing API level guard for PerfettoContinuousProfiler creation
setupProfilernow guardsuseProfilingManagerbehind an API 35+ check and falls back toNoOpContinuousProfileron lower API levels to avoid loading Perfetto classes.
Or push these changes by commenting:
@cursor push ce6f706c8b
Preview (ce6f706c8b)
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
@@ -337,6 +337,17 @@
performanceCollector.start(chunkId.toString());
}
} else {
+ if (options.isUseProfilingManager()
+ && buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ options
+ .getLogger()
+ .log(
+ SentryLevel.INFO,
+ "useProfilingManager is enabled, but API level is below %d. Continuous profiling is disabled.",
+ Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ options.setContinuousProfiler(NoOpContinuousProfiler.getInstance());
+ return;
+ }
final @NotNull SentryFrameMetricsCollector frameMetricsCollector =
Objects.requireNonNull(
options.getFrameMetricsCollector(), "options.getFrameMetricsCollector is required");
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java
--- a/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerfettoContinuousProfiler.java
@@ -107,7 +107,6 @@
final @NotNull ProfileLifecycle profileLifecycle,
final @NotNull TracesSampler tracesSampler) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- shouldStop = false;
if (shouldSample) {
isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble());
shouldSample = false;
@@ -118,6 +117,7 @@
}
switch (profileLifecycle) {
case TRACE:
+ shouldStop = false;
activeTraceCount = Math.max(0, activeTraceCount); // safety check.
activeTraceCount++;
break;
@@ -128,6 +128,7 @@
"Unexpected call to startProfiler(MANUAL) while profiler already running. Skipping.");
return;
}
+ shouldStop = false;
break;
}
if (!isRunning()) {
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt
--- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt
@@ -376,7 +376,16 @@
assertTrue(fixture.sentryOptions.continuousProfiler is AndroidContinuousProfiler)
}
+ @Config(sdk = [34])
@Test
+ fun `init with profiling manager below API 35 sets no-op continuous profiler`() {
+ fixture.initSut(configureOptions = { isUseProfilingManager = true }, useRealContext = true)
+
+ assertEquals(NoOpTransactionProfiler.getInstance(), fixture.sentryOptions.transactionProfiler)
+ assertEquals(NoOpContinuousProfiler.getInstance(), fixture.sentryOptions.continuousProfiler)
+ }
+
+ @Test
fun `init with profilesSampleRate should set Android transaction profiler`() {
fixture.initSut(configureOptions = { profilesSampleRate = 1.0 })
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoContinuousProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoContinuousProfilerTest.kt
--- a/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoContinuousProfilerTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerfettoContinuousProfilerTest.kt
@@ -132,4 +132,24 @@
"Profiler should continue running after chunk restart — shouldStop must be reset on start",
)
}
+
+ @Test
+ fun `manual start while trace profiling is running does not cancel pending trace stop`() {
+ val profiler = fixture.getSut()
+
+ profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler)
+ assertTrue(profiler.isRunning)
+
+ profiler.stopProfiler(ProfileLifecycle.TRACE)
+ profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler)
+
+ fixture.executor.runAll()
+
+ assertFalse(profiler.isRunning)
+ verify(fixture.mockLogger)
+ .log(
+ eq(SentryLevel.WARNING),
+ eq("Unexpected call to startProfiler(MANUAL) while profiler already running. Skipping."),
+ )
+ }
}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
Outdated
Show resolved
Hide resolved
| frameMetricsCollector, | ||
| () -> options.getExecutorService(), | ||
| () -> | ||
| new PerfettoProfiler(context.getApplicationContext(), options.getLogger())) |
There was a problem hiding this comment.
Missing API level guard for PerfettoContinuousProfiler creation
High Severity
PerfettoContinuousProfiler is created when isUseProfilingManager() is true with no API level check. On API < 35, when ensureProfiler() eventually evaluates the supplier, it loads PerfettoProfiler which references ProfilingManager (API 35+), causing a NoClassDefFoundError crash. The PR description states profiling is "disabled" on API < 35 but no code enforces this.
Reviewed by Cursor Bugbot for commit 873bad3. Configure here.
| measurements.put( | ||
| ProfileMeasurement.ID_SCREEN_FRAME_RATES, | ||
| new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements)); | ||
| } |
There was a problem hiding this comment.
Shared mutable deques cause frame measurement data loss
High Severity
ChunkMeasurementCollector passes its ConcurrentLinkedDeque instance fields (e.g. slowFrameRenderMeasurements) directly into ProfileMeasurement constructors in addFrameDataToMeasurements. These are the same deque instances that start() calls .clear() on when the next chunk begins. Since sendChunk submits the ProfileChunk.Builder asynchronously via the executor, the deques are cleared by the next chunkMeasurements.start() call in startInternal() before the async task serializes the ProfileMeasurement objects from the previous chunk. This causes all frame metric measurements (slow frames, frozen frames, refresh rate) to be empty in sent profile chunks.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6370485. Configure here.
…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 AtomicReference shared between the CachedItem lambda and the header callable, keeping meta_length as an envelope transport concern rather than in ProfileChunk
…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)
…ofilerTestCases.kt Shared test logic (sampling, lifecycle, rate limiting, chunk restart, close) is defined as extension functions on IContinuousProfiler in ContinuousProfilerTestCases.kt. AndroidContinuousProfilerTest delegates to these for common behavior while keeping legacy-specific tests inline. This enables PerfettoContinuousProfilerTest to reuse the same test cases with its own fixture. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6370485 to
fdd7287
Compare
| options.isUseProfilingManager() | ||
| ? new PerfettoContinuousProfiler( | ||
| buildInfoProvider, | ||
| options.getLogger(), | ||
| frameMetricsCollector, | ||
| () -> options.getExecutorService(), | ||
| () -> | ||
| new PerfettoProfiler(context.getApplicationContext(), options.getLogger())) |
There was a problem hiding this comment.
Bug: The code instantiates PerfettoContinuousProfiler without a runtime API level check, causing a NoSuchFieldError crash on devices with Android API < 35 when profiling is enabled.
Severity: HIGH
Suggested Fix
Add a runtime API level check in setupProfiler() to ensure PerfettoContinuousProfiler is only instantiated on supported devices. The check should be Build.VERSION.SDK_INT >= 35 combined with the existing options.isUseProfilingManager() condition.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java#L344-L351
Potential issue: In `AndroidOptionsInitializer.setupProfiler()`,
`PerfettoContinuousProfiler` is instantiated if `options.isUseProfilingManager()` is
true, without a required runtime API level check. The `PerfettoContinuousProfiler` and
its dependencies, like `ProfilingManager`, are only available on Android API 35 and
newer. On devices with an API level below 35, when profiling is initiated, the code
attempts to access `Context.PROFILING_SERVICE`. This constant field does not exist on
older APIs, which will result in a `NoSuchFieldError` and cause the application to
crash. This issue occurs despite `@RequiresApi(35)` annotations, as they are only
lint-time checks and do not prevent runtime execution.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 5 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 96dae7f. Configure here.
| public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; | ||
| public fun getEnvironment ()Ljava/lang/String; | ||
| public fun getMeasurements ()Ljava/util/Map; | ||
| public fun getMetaLength ()I |
There was a problem hiding this comment.
API file declares non-existent ProfileChunk methods
Medium Severity
The API surface file adds getMetaLength() and setMetaLength(int) to ProfileChunk, but these methods do not exist anywhere in ProfileChunk.java. The actual metaLength concept is handled entirely at the envelope item header level (SentryEnvelopeItemHeader), not on ProfileChunk. This will cause apiCheck to fail or, if the file was manually edited, it misrepresents the actual public API.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 96dae7f. Configure here.
sentry-android-core/src/test/java/io/sentry/android/core/ContinuousProfilerTestCases.kt
Show resolved
Hide resolved
…ases Reuse 17 existing tests by delegating to shared extension functions in ContinuousProfilerTestCases.kt Add 1 Perfetto-specific test for the manual mode duplicate start warning message.
…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>
…o PerfettoContinuousProfiler This separation keeps PerfettoProfiler focused on the ProfilingManager API and puts all measurement collection (frame metrics + performanceCollector) at the PerfettoContinuousProfiler layer, making both classes easier to reason about independently.
…entCollector Rename FrameMetricsProfiler to ChunkMeasurementCollector and expand it to own the measurement lifecycle for both types of profiling we do outside of perfetto: 1) frame metrics (slow/frozen frames, refresh rate) (previous) 2) performance data (CPU usage, memory footprint) from the CompositePerformanceCollector (new change). Also restores the performanceCollector impl that was accidentally removed in an earlier commit.
shouldStop = false was unconditionally set at the top of startProfiler, which could cancel a pending stop if startProfiler was called on an early-return path (e.g. MANUAL while already running). Move the reset to inside the !isRunning() block so it only triggers when the profiler is actually about to start. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96dae7f to
11331ea
Compare



📜 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 UIUnit Tests
PerfettoContinuousProfilerTestSentryOptionsTest,ManifestMetadataReaderTest,SentryEnvelopeItemTest📝 Checklist
sendDefaultPIIis enabled.Testing locally
🔮 Next steps
PROFILING_TYPE_STACK_SAMPLINGtraces (ProfilingManager doesn't seem to includelinux.process_statsdata source)