From 659b164b52c746d215a7cb7ac33e08191e18caa3 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 20 Oct 2026 14:24:47 +0200 Subject: [PATCH 1/2] fix(profiling): Cancel a pending stop when the profiler is started again A stop request only sets shouldStop, and the profiler stops when the current chunk ends. A start call before that was ignored because the profiler was still running, and shouldStop stayed set. So the profiler stopped at the end of the chunk, even in TRACE mode while sampled spans were active. The legacy profiler also never reset shouldStop, so after the first stop every later profile ended after one chunk. Reset shouldStop on every sampled start call in AndroidContinuousProfiler and PerfettoContinuousProfiler. A manual start while the profiler runs is now an expected call, so the Perfetto profiler logs it at DEBUG level like the legacy profiler. --- CHANGELOG.md | 2 ++ .../core/AndroidContinuousProfiler.java | 2 ++ .../core/PerfettoContinuousProfiler.java | 7 +++---- .../core/AndroidContinuousProfilerTest.kt | 12 +++++++++++ .../core/ContinuousProfilerTestCases.kt | 20 +++++++++++++++++++ .../core/PerfettoContinuousProfilerTest.kt | 14 ++++++++----- 6 files changed, 48 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 496c5333ec1..5db7de2c21b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixes +- Keep UI profiling running when `startProfiler` is called before a requested stop takes effect at the end of the current chunk ([#XXXX](https://github.com/getsentry/sentry-java/pull/XXXX)) + - Before, the start call was ignored and the profiler still stopped at the end of the chunk, even in `TRACE` mode while sampled spans were active. On API 34 and lower, every later profile also stopped after one chunk. - Keep sending events from Android 7.1.0 and older devices after Sentry changes its TLS certificate authority ([#6227](https://github.com/getsentry/sentry-java/pull/6227)) - In February 2027, Sentry moves its TLS certificates from DigiCert to Let's Encrypt and Google Trust Services ([announcement](https://sentry.io/changelog/were-changing-our-tls-certificate-authority-in-february-2027)). - Android 7.1.0 (API 25) and older don't trust the Let's Encrypt root certificate (ISRG Root X1), so without this fix, the SDK on those devices can no longer send events to Sentry after the switch. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index 20fea39a28d..4f115ca9d7f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -131,6 +131,8 @@ public void startProfiler( logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); return; } + // A stop only takes effect when the current chunk ends, so a start before then cancels it. + shouldStop = false; switch (profileLifecycle) { case TRACE: // rootSpanCounter should never be negative, unless the user changed profile lifecycle 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 index 58065c708ec..15ae88f22c8 100644 --- 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 @@ -141,6 +141,8 @@ public void startProfiler( logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); return; } + // A stop only takes effect when the current chunk ends, so a start before then cancels it. + shouldStop = false; switch (profileLifecycle) { case TRACE: activeTraceCount = Math.max(0, activeTraceCount); // safety check. @@ -148,16 +150,13 @@ public void startProfiler( break; case MANUAL: if (isRunning()) { - logger.log( - SentryLevel.WARNING, - "Unexpected call to startProfiler(MANUAL) while profiler already running. Skipping."); + logger.log(SentryLevel.DEBUG, "Profiler is already running."); return; } break; } if (!isRunning()) { logger.log(SentryLevel.DEBUG, "Started Profiler."); - shouldStop = false; startInternal(); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt index caaa30152a4..a13f3fe1246 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -278,6 +278,18 @@ class AndroidContinuousProfilerTest { } .testDoesNotStartWhenOffline(mocks) + @Test + fun `manual start cancels a pending stop`() = + fixture.getSut().testManualStartCancelsPendingStop(mocks) + + @Test + fun `trace start cancels a pending stop`() = + fixture.getSut().testTraceStartCancelsPendingStop(mocks) + + @Test + fun `manual profiler can be started again after a full start-stop cycle`() = + fixture.getSut().testCanBeStartedAgainAfterStopCycle(mocks) + // -- Legacy-specific tests (AndroidContinuousProfiler only) -- @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContinuousProfilerTestCases.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContinuousProfilerTestCases.kt index 5e4e0504ddf..267529fd33d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContinuousProfilerTestCases.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContinuousProfilerTestCases.kt @@ -192,3 +192,23 @@ fun IContinuousProfiler.testCanBeStartedAgainAfterStopCycle(mocks: ProfilerMocks mocks.executor.runAll() assertTrue(isRunning, "shouldStop must be reset on start") } + +fun IContinuousProfiler.testManualStartCancelsPendingStop(mocks: ProfilerMocks) { + startProfiler(ProfileLifecycle.MANUAL, mocks.tracesSampler) + val profilerIdBeforeStop = profilerId + stopProfiler(ProfileLifecycle.MANUAL) + startProfiler(ProfileLifecycle.MANUAL, mocks.tracesSampler) + mocks.executor.runAll() + assertTrue(isRunning, "a start before the chunk ends must cancel the pending stop") + assertEquals(profilerIdBeforeStop, profilerId) +} + +fun IContinuousProfiler.testTraceStartCancelsPendingStop(mocks: ProfilerMocks) { + startProfiler(ProfileLifecycle.TRACE, mocks.tracesSampler) + val profilerIdBeforeStop = profilerId + stopProfiler(ProfileLifecycle.TRACE) + startProfiler(ProfileLifecycle.TRACE, mocks.tracesSampler) + mocks.executor.runAll() + assertTrue(isRunning, "a start before the chunk ends must cancel the pending stop") + assertEquals(profilerIdBeforeStop, profilerId) +} 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 index d6f29b97b7b..3bafc29be3d 100644 --- 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 @@ -178,6 +178,14 @@ class PerfettoContinuousProfilerTest { fun `manual profiler can be started again after a full start-stop cycle`() = fixture.getSut().testCanBeStartedAgainAfterStopCycle(mocks) + @Test + fun `manual start cancels a pending stop`() = + fixture.getSut().testManualStartCancelsPendingStop(mocks) + + @Test + fun `trace start cancels a pending stop`() = + fixture.getSut().testTraceStartCancelsPendingStop(mocks) + // -- Perfetto-specific tests -- @Test @@ -503,10 +511,6 @@ class PerfettoContinuousProfilerTest { assertTrue(profiler.isRunning) profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) - verify(fixture.mockLogger) - .log( - eq(SentryLevel.WARNING), - eq("Unexpected call to startProfiler(MANUAL) while profiler already running. Skipping."), - ) + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) } } From 1ee292407bf6971678bbd6a3ad576d2b4ba530e2 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 20 Oct 2026 14:50:52 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db7de2c21b..94cdeea85aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixes -- Keep UI profiling running when `startProfiler` is called before a requested stop takes effect at the end of the current chunk ([#XXXX](https://github.com/getsentry/sentry-java/pull/XXXX)) +- Keep UI profiling running when `startProfiler` is called before a requested stop takes effect at the end of the current chunk ([#6291](https://github.com/getsentry/sentry-java/pull/6291)) - Before, the start call was ignored and the profiler still stopped at the end of the chunk, even in `TRACE` mode while sampled spans were active. On API 34 and lower, every later profile also stopped after one chunk. - Keep sending events from Android 7.1.0 and older devices after Sentry changes its TLS certificate authority ([#6227](https://github.com/getsentry/sentry-java/pull/6227)) - In February 2027, Sentry moves its TLS certificates from DigiCert to Let's Encrypt and Google Trust Services ([announcement](https://sentry.io/changelog/were-changing-our-tls-certificate-authority-in-february-2027)).