Skip to content

feat(profiling): Add Android ProfilingManager (Perfetto) support#5251

Open
43jay wants to merge 15 commits intomainfrom
claude/dreamy-solomon
Open

feat(profiling): Add Android ProfilingManager (Perfetto) support#5251
43jay wants to merge 15 commits intomainfrom
claude/dreamy-solomon

Conversation

@43jay
Copy link
Copy Markdown
Collaborator

@43jay 43jay commented Mar 31, 2026

📜 Description

Adds opt-in useProfilingManager option that uses Android's ProfilingManager API (API 35+) for Perfetto-based stack sampling instead of the legacy Debug.startMethodTracingSampling engine.

PerfettoContinuousProfiler is mutually exclusive with AndroidContinuousProfiler — 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 standalone PerfettoContinuousProfiler because:

  1. Mutually exclusiveAndroidContinuousProfiler has a lot of state and the if (perfetto) { ... } else { legacy } branching makes paths hard to follow => the two codepaths will never be active at the same time.
  2. Threading — a large # different threads are involved and reasoning about locking is harder with two backends in one class
Thread Callers Creation site
Caller's thread (main/app) startProfiler, stopProfiler, close(true), reevaluateSampling Not created by Sentry
FrameMetrics HandlerThread Writes frame measurements to PerfettoProfiler's ConcurrentLinkedDeque (code) new HandlerThread("...SentryFrameMetricsCollector")
SentryExecutorServiceThreadFactory-N stopInternal(true) — scheduled chunk timer. Also sendChunk() submits work here. new Thread(r, "SentryExecutorServiceThreadFactory-" + cnt++)
SentryAsyncConnection-N onRateLimitChanged — inline callback (code) new Thread(r, "SentryAsyncConnection-" + cnt++)
Timer daemon onRateLimitChanged — rate limit expiry (code); close(false) — session timeout (code); not a direct caller but CompositePerformanceCollector runs setup() and collect() every 100ms (code) JDK internal — new Timer(true) in RateLimiter, LifecycleWatcher, CompositePerformanceCollector
OTel span processor startProfiler(TRACE) (code), stopProfiler(TRACE) (code) Created by OpenTelemetry SDK — not Sentry-controlled
  1. App-start profiling — the legacy profiler has special null-scopes handling for app-start. ProfilingManager doesn't support app-start, so this complexity doesn't apply
  2. API level annotations — confining all ProfilingManager call sites to PerfettoContinuousProfiler + PerfettoProfiler means fewer @SuppressLint("NewApi") scattered through AndroidContinuousProfiler

Key files

  • SentryOptions.useProfilingManager — opt-in flag, readable from manifest io.sentry.profiling.use-profiling-manager
  • PerfettoContinuousProfilerIContinuousProfiler impl, @RequiresApi(35), delegates to PerfettoProfiler
  • PerfettoProfiler — wraps ProfilingManager.requestProfiling(PROFILING_TYPE_STACK_SAMPLING, ...)
  • SentryEnvelopeItem.fromPerfettoProfileChunk() — binary envelope format with meta_length header
  • AndroidContinuousProfiler — legacy only, no Perfetto references

💡 Motivation and Context

Android's ProfilingManager (API 35+) provides OS-level Perfetto stack sampling. The legacy Debug.startMethodTracingSampling path is preserved unchanged. On API < 35 with useProfilingManager=true, profiling is disabled (no silent fallback).

💚 How did you test it?

  • Manual testing on Pixel Fold AVD (API 35) — verified Perfetto chunks captured with content_type: "perfetto"
  • Extracted .pftrace files and inspected in Perfetto UI
  • Unit tests: PerfettoContinuousProfilerTest (rate limiting), SentryOptionsTest, ManifestMetadataReaderTest, SentryEnvelopeItemTest
  • Run: JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew :sentry-android-core:testDebugUnitTest --tests "io.sentry.android.core.PerfettoContinuousProfilerTest"

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the 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.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

Testing locally

# Disable ProfilingManager rate limiting (required for repeated testing)
adb shell device_config put profiling_testing rate_limiter.disabled true

# Watch logcat for the file path
adb logcat -s Sentry | grep "ProfilingResult"

# Pull the .pftrace file (can't adb pull from app-private dir, use run-as + cat)
PKG="io.sentry.samples.android"
REMOTE_DIR="/data/user/0/$PKG/files/profiling"
adb shell "run-as $PKG cat '$REMOTE_DIR/<filename>'" > ~/Desktop/profile.pftrace

# Open in https://ui.perfetto.dev/

🔮 Next steps

  • Remove CountdownLatch from PerfettoProfiler
  • Refactor AndroidContinuousProfilerTest to extend existing test scenarios to PerfettoContinuousProfilerTest
  • Verify backend ingest WAE
  • Investigate missing thread names in PROFILING_TYPE_STACK_SAMPLING traces (ProfilingManager doesn't seem to include linux.process_stats data source)
  • Docs and CHANGELOG update once PR is stable #skip-changelog

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

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

Generated by 🚫 dangerJS against 873bad3

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

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


This PR will not appear in the changelog.


🤖 This preview updates automatically when you update the PR.

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 31, 2026

Sentry Build Distribution

App Name App ID Version Configuration Install Page
SDK Size io.sentry.tests.size 8.38.0 (1) release Install Build

Configure sentry-android build distribution settings

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 319.08 ms 341.54 ms 22.46 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

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
c26f799 319.88 ms 358.02 ms 38.14 ms
7da193d 319.39 ms 375.24 ms 55.85 ms
b3c0878 316.40 ms 345.51 ms 29.11 ms

App size

Revision Plain With Sentry Diff
c26f799 0 B 0 B 0 B
7da193d 0 B 0 B 0 B
b3c0878 0 B 0 B 0 B

@43jay 43jay marked this pull request as ready for review April 7, 2026 20:47
Copy link
Copy Markdown

@cursor cursor bot left a comment

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.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Serialize uses field instead of getter for meta_length
    • SentryEnvelopeItemHeader.serialize() now uses getMetaLength() (captured once in a local) so callable-backed Perfetto chunks correctly emit meta_length in envelope headers.

Create PR

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.

@43jay 43jay force-pushed the claude/dreamy-solomon branch from 4e173d3 to b4b28c9 Compare April 7, 2026 21:23
@43jay 43jay marked this pull request as draft April 9, 2026 17:30
43jay and others added 5 commits April 9, 2026 14:06
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>
@43jay 43jay force-pushed the claude/dreamy-solomon branch from b4b28c9 to 83b1f1a Compare April 9, 2026 20:36
@43jay 43jay marked this pull request as ready for review April 9, 2026 20:36
() -> {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
stopInternal(true);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 10c415f. Configure here.

43jay and others added 6 commits April 10, 2026 17:52
…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
…le contract

AtomicInteger initialized to -1 would write meta_length: -1 if the
header were serialized before the payload was materialized. Replace
with AtomicReference<Integer> initialized to null so getMetaLength()
correctly returns null before evaluation, matching the @nullable
contract and causing serialize() to skip the field.

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)
…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>
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>
43jay and others added 4 commits April 10, 2026 17:52
- 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.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…entCollector

Rename FrameMetricsProfiler to ChunkMeasurementCollector and expand it
to own the full measurement lifecycle for a profiling chunk: both frame
metrics (slow/frozen frames, refresh rate) (previous)
AND performance data (CPU usage, memory footprint) from the
CompositePerformanceCollector (new change).

Also restores the performanceCollector impl that was accidentally
removed in an earlier commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@43jay 43jay force-pushed the claude/dreamy-solomon branch from 10c415f to 873bad3 Compare April 13, 2026 02:22
Comment on lines +319 to +322

final @NotNull Map<String, ProfileMeasurement> measurements = chunkMeasurements.stop();

final @Nullable File traceFile = perfettoProfiler.endAndCollect();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The stopInternal() method holds a lock while calling the blocking endAndCollect() method, which can block the main thread and cause an ANR.
Severity: HIGH

Suggested Fix

The blocking call perfettoProfiler.endAndCollect() should be executed without holding the lock. The lock should be released before the blocking operation and re-acquired if necessary afterward, or the operation should be moved to a background thread to avoid blocking the caller's thread.

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/PerfettoContinuousProfiler.java#L319-L322

Potential issue: The `stopInternal()` method in `PerfettoContinuousProfiler` is called
with a lock already acquired. It then invokes `perfettoProfiler.endAndCollect()`, which
blocks for up to 5 seconds waiting for a result. If `stopInternal()` is triggered on the
main thread (e.g., when starting a transaction while the profiler is rate-limited or the
device is offline), the main thread will be blocked for the duration of the call. This
can lead to an Application Not Responding (ANR) error on Android, as the ANR threshold
is 5 seconds.

Copy link
Copy Markdown

@cursor cursor bot left a comment

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 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Unconditional shouldStop reset causes unintended profiler continuation
    • shouldStop is 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
    • setupProfiler now guards useProfilingManager behind an API 35+ check and falls back to NoOpContinuousProfiler on lower API levels to avoid loading Perfetto classes.

Create PR

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.

Reviewed by Cursor Bugbot for commit 873bad3. Configure here.

final @NotNull ProfileLifecycle profileLifecycle,
final @NotNull TracesSampler tracesSampler) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
shouldStop = 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.

Unconditional shouldStop reset causes unintended profiler continuation

Medium Severity

shouldStop = false is set unconditionally at the top of startProfiler(), before any lifecycle checks. When startProfiler(MANUAL) is called while the profiler is already running from a TRACE lifecycle, the method resets shouldStop to false but then returns early at line 129 with a "Skipping" warning. This silently cancels a pending stop request from a prior stopProfiler(TRACE) call, causing the profiler to restart indefinitely at chunk boundaries instead of stopping as intended.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 873bad3. Configure here.

frameMetricsCollector,
() -> options.getExecutorService(),
() ->
new PerfettoProfiler(context.getApplicationContext(), options.getLogger()))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 873bad3. Configure here.

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.

1 participant