From 0d34c421adef21e417b7ec9e8cab1d3e68330ccb Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Fri, 27 Mar 2026 13:12:36 -0400 Subject: [PATCH 1/4] feat(profiling): Add useProfilingManager option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../android/core/ManifestMetadataReader.java | 6 +++++ .../core/ManifestMetadataReaderTest.kt | 25 ++++++++++++++++++ sentry/api/sentry.api | 2 ++ .../main/java/io/sentry/SentryOptions.java | 26 +++++++++++++++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 11 ++++++++ 5 files changed, 70 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 822d7fbbe0..a65763490f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -108,6 +108,8 @@ final class ManifestMetadataReader { static final String ENABLE_APP_START_PROFILING = "io.sentry.profiling.enable-app-start"; + static final String USE_PROFILING_MANAGER = "io.sentry.profiling.use-profiling-manager"; + static final String ENABLE_SCOPE_PERSISTENCE = "io.sentry.enable-scope-persistence"; static final String REPLAYS_SESSION_SAMPLE_RATE = "io.sentry.session-replay.session-sample-rate"; @@ -494,6 +496,10 @@ static void applyMetadata( readBool( metadata, logger, ENABLE_APP_START_PROFILING, options.isEnableAppStartProfiling())); + options.setUseProfilingManager( + readBool( + metadata, logger, USE_PROFILING_MANAGER, options.isUseProfilingManager())); + options.setEnableScopePersistence( readBool( metadata, logger, ENABLE_SCOPE_PERSISTENCE, options.isEnableScopePersistence())); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index ba01a9ecf7..0439c51a0e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1469,6 +1469,31 @@ class ManifestMetadataReaderTest { assertFalse(fixture.options.isEnableAppStartProfiling) } + @Test + fun `applyMetadata reads useProfilingManager flag to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.USE_PROFILING_MANAGER to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.isUseProfilingManager) + } + + @Test + fun `applyMetadata reads useProfilingManager flag to options and keeps default if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertFalse(fixture.options.isUseProfilingManager) + } + @Test fun `applyMetadata reads enableScopePersistence flag to options`() { // Arrange diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index cb9078ac07..af3c6f0068 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3688,6 +3688,7 @@ public class io/sentry/SentryOptions { public fun isTraceOptionsRequests ()Z public fun isTraceSampling ()Z public fun isTracingEnabled ()Z + public fun isUseProfilingManager ()Z public fun merge (Lio/sentry/ExternalOptions;)V public fun setAttachServerName (Z)V public fun setAttachStacktrace (Z)V @@ -3809,6 +3810,7 @@ public class io/sentry/SentryOptions { public fun setTransactionProfiler (Lio/sentry/ITransactionProfiler;)V public fun setTransportFactory (Lio/sentry/ITransportFactory;)V public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V + public fun setUseProfilingManager (Z)V public fun setVersionDetector (Lio/sentry/IVersionDetector;)V public fun setViewHierarchyExporters (Ljava/util/List;)V } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index a831a11ea8..0839ba8db5 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -622,6 +622,13 @@ public class SentryOptions { */ private boolean startProfilerOnAppStart = false; + /** + * When true, the SDK uses Android's {@code ProfilingManager} (Perfetto-based stack sampling) on + * API 35+ devices. On older devices where ProfilingManager is not available, no profiling data is + * collected — the legacy {@code Debug}-based profiler is not used as a fallback. + */ + private boolean useProfilingManager = false; + /** * Controls the deadline timeout in milliseconds for automatic transactions. When set to a * positive value, that value is used as the deadline timeout. When set to a value less than or @@ -2213,6 +2220,25 @@ public void setStartProfilerOnAppStart(final boolean startProfilerOnAppStart) { this.startProfilerOnAppStart = startProfilerOnAppStart; } + /** + * Whether to use Android's ProfilingManager (Perfetto) for profiling on Android 35+. + * + * @return true if ProfilingManager-based profiling is enabled. + */ + public boolean isUseProfilingManager() { + return useProfilingManager; + } + + /** + * Set whether to use Android's ProfilingManager (Perfetto) for profiling on Android 35+. On + * devices below API 35 where ProfilingManager is not available, no profiling data is collected. + * + * @param useProfilingManager true to use ProfilingManager-based profiling. + */ + public void setUseProfilingManager(final boolean useProfilingManager) { + this.useProfilingManager = useProfilingManager; + } + public long getDeadlineTimeout() { return deadlineTimeout; } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 960b2838e2..1d7ab20ef9 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -750,6 +750,17 @@ class SentryOptionsTest { assertTrue(options.isEnableAppStartProfiling) } + @Test + fun `when options are initialized, useProfilingManager is set to false by default`() { + assertFalse(SentryOptions().isUseProfilingManager) + } + + @Test + fun `when setUseProfilingManager is called, value is set`() { + val options = SentryOptions().apply { isUseProfilingManager = true } + assertTrue(options.isUseProfilingManager) + } + @Test fun `when options are initialized, profilingTracesHz is set to 101 by default`() { assertEquals(101, SentryOptions().profilingTracesHz) From 86e585c1e8aa4232d7c8e84c9853e2b3174c4fa7 Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Fri, 27 Mar 2026 13:15:37 -0400 Subject: [PATCH 2/4] chore(samples): Update sample app to support Perfetto profiling testing 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) --- .../src/main/AndroidManifest.xml | 4 ++ .../samples/android/ProfilingActivity.kt | 42 ++++++++++++++++++- .../main/res/layout/activity_profiling.xml | 31 ++++++++++++-- .../src/main/res/values/strings.xml | 8 +++- 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 548e5e8ac0..10f63ef8c4 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -163,6 +163,10 @@ + + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt index 8626c12c6c..00789b412b 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt @@ -1,5 +1,6 @@ package io.sentry.samples.android +import android.os.Build import android.os.Bundle import android.view.View import android.widget.SeekBar @@ -22,6 +23,7 @@ class ProfilingActivity : AppCompatActivity() { private lateinit var binding: ActivityProfilingBinding private val executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) private var profileFinished = true + private var manualProfilingActive = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -30,7 +32,7 @@ class ProfilingActivity : AppCompatActivity() { this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (profileFinished) { + if (profileFinished && !manualProfilingActive) { isEnabled = false onBackPressedDispatcher.onBackPressed() } else { @@ -42,6 +44,16 @@ class ProfilingActivity : AppCompatActivity() { ) binding = ActivityProfilingBinding.inflate(layoutInflater) + // Show which profiler backend is active + val options = Sentry.getCurrentScopes().options + val isPerfetto = options.isUseProfilingManager && Build.VERSION.SDK_INT >= 35 + binding.profilingStatus.text = + if (isPerfetto) { + getString(R.string.profiling_status_perfetto) + } else { + getString(R.string.profiling_status_legacy) + } + binding.profilingDurationSeekbar.setOnSeekBarChangeListener( object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(p0: SeekBar, p1: Int, p2: Boolean) { @@ -76,6 +88,7 @@ class ProfilingActivity : AppCompatActivity() { binding.profilingList.adapter = ProfilingListAdapter() binding.profilingList.layoutManager = LinearLayoutManager(this) + // Transaction-based profiling (existing) binding.profilingStart.setOnClickListener { binding.profilingProgressBar.visibility = View.VISIBLE profileFinished = false @@ -92,6 +105,33 @@ class ProfilingActivity : AppCompatActivity() { } .start() } + + // Manual continuous profiling (exercises Perfetto path on API 35+) + binding.profilingStartManual.setOnClickListener { + if (!manualProfilingActive) { + Sentry.startProfiler() + manualProfilingActive = true + profileFinished = false + binding.profilingStartManual.text = getString(R.string.profiling_stop_manual) + binding.profilingProgressBar.visibility = View.VISIBLE + + // Start background work to generate interesting profile data + val threads = getBackgroundThreads() + repeat(threads) { executors.submit { runMathOperations() } } + executors.submit { swipeList() } + + binding.profilingResult.text = getString(R.string.profiling_manual_started) + } else { + Sentry.stopProfiler() + manualProfilingActive = false + profileFinished = true + binding.profilingStartManual.text = getString(R.string.profiling_start_manual) + binding.profilingProgressBar.visibility = View.GONE + + binding.profilingResult.text = getString(R.string.profiling_manual_stopped) + } + } + setContentView(binding.root) Sentry.reportFullyDisplayed() } diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_profiling.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_profiling.xml index 8100834f78..3dca64b6d2 100644 --- a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_profiling.xml +++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_profiling.xml @@ -38,11 +38,34 @@ android:gravity="center" android:text="@string/profiling_result" /> -