From 9a60ad1c951221a26a552ed0936775cee4308004 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 01/14] 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 6d90bb5ca8..ce0bdfd7c9 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"; @@ -497,6 +499,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 81b73d5dea..0881c138e8 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 b9cbb2ae1b..89f9dd5949 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3710,6 +3710,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 @@ -3835,6 +3836,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 86086f8816..a8205f1972 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -642,6 +642,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 @@ -2233,6 +2240,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 da014b30f7..10937bb804 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -757,6 +757,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 1250aa1b1317fe1459ecb0cc6261596bb2e9b25e 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 02/14] 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" /> -