From b118172159e686ffcfeb845398326eaefba7eebe Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 28 May 2026 11:28:27 +0200 Subject: [PATCH 1/2] perf(replay): Defer ReplayIntegration.start() off the main thread Move the expensive work in ReplayIntegration.start() (capture strategy creation, recorder start, root view listener registration) to the executor service, keeping only the lightweight state checks and lifecycle transition synchronous. This saves ~16ms on the main thread during SentryAndroid.init() (measured on Pixel 3). The registerRootViewListeners() call is posted to the main looper handler since it triggers View operations via OnRootViewsChangedListener. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 4 ++ .../android/replay/ReplayIntegration.kt | 58 +++++++++++-------- .../ReplayIntegrationWithRecorderTest.kt | 7 ++- .../sentry/android/replay/ReplaySmokeTest.kt | 3 +- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c21a8cce7d7..0598336a59c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ ``` - Parse ART memory and garbage collector info from ANR tombstones into ART context ([#5428](https://github.com/getsentry/sentry-java/pull/5428)) +### Improvements + +- Improve SDK init performance by deferring `ReplayIntegration.start()` off the main thread ([#XXXX](https://github.com/getsentry/sentry-java/pull/XXXX)) + ## 8.42.0 ### Features diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 07e91d76486..b6770d0770a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -161,6 +161,7 @@ public class ReplayIntegration( lifecycle.currentState >= STARTED && lifecycle.currentState < STOPPED override fun start() { + val isFullSession: Boolean lifecycleLock.acquire().use { if (!isEnabled.get()) { return @@ -174,7 +175,7 @@ public class ReplayIntegration( return } - val isFullSession = random.sample(options.sessionReplay.sessionSampleRate) + isFullSession = random.sample(options.sessionReplay.sessionSampleRate) if (!isFullSession && !options.sessionReplay.isSessionReplayForErrorsEnabled) { options.logger.log( INFO, @@ -184,30 +185,39 @@ public class ReplayIntegration( } lifecycle.currentState = STARTED - captureStrategy = - replayCaptureStrategyProvider?.invoke(isFullSession) - ?: if (isFullSession) { - SessionCaptureStrategy( - options, - scopes, - dateProvider, - replayExecutor, - replayCacheProvider, - ) - } else { - BufferCaptureStrategy( - options, - scopes, - dateProvider, - random, - replayExecutor, - replayCacheProvider, - ) - } - recorder?.start() - captureStrategy?.start() + } + + // Defer the expensive work (strategy creation, recorder start, listener registration) + // off the calling thread to avoid blocking SentryAndroid.init() on the main thread. + options.executorService.submitSafely(options, "ReplayIntegration.start") { + lifecycleLock.acquire().use { + captureStrategy = + replayCaptureStrategyProvider?.invoke(isFullSession) + ?: if (isFullSession) { + SessionCaptureStrategy( + options, + scopes, + dateProvider, + replayExecutor, + replayCacheProvider, + ) + } else { + BufferCaptureStrategy( + options, + scopes, + dateProvider, + random, + replayExecutor, + replayCacheProvider, + ) + } + recorder?.start() + captureStrategy?.start() + } - registerRootViewListeners() + // Post to main thread since registerRootViewListeners triggers View operations + // (e.g. addOnLayoutChangeListener) via the OnRootViewsChangedListener callback + mainLooperHandler.post { registerRootViewListeners() } } } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt index 75626f4e4cf..d6dcdb1ad7e 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt @@ -18,6 +18,7 @@ import io.sentry.android.replay.ReplayIntegrationWithRecorderTest.LifecycleState import io.sentry.android.replay.util.ReplayShadowMediaCodec import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent +import io.sentry.test.ImmediateExecutorService import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider import io.sentry.util.thread.NoOpThreadChecker @@ -44,7 +45,11 @@ class ReplayIntegrationWithRecorderTest { @get:Rule val tmpDir = TemporaryFolder() internal class Fixture { - val options = SentryOptions().apply { threadChecker = NoOpThreadChecker.getInstance() } + val options = + SentryOptions().apply { + threadChecker = NoOpThreadChecker.getInstance() + executorService = ImmediateExecutorService() + } val scopes = mock() fun getSut( diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt index c26e6be9c41..b55b3020338 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt @@ -21,6 +21,7 @@ import io.sentry.SentryReplayEvent.ReplayType import io.sentry.android.replay.util.ReplayShadowMediaCodec import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent +import io.sentry.test.ImmediateExecutorService import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider import java.time.Duration @@ -59,7 +60,7 @@ class ReplaySmokeTest { @get:Rule val tmpDir = TemporaryFolder() internal class Fixture { - val options = SentryOptions() + val options = SentryOptions().apply { executorService = ImmediateExecutorService() } val scope = Scope(options) val scopes = mock { From b208f1883d65855fe7f4e17f803c270cc51ef468 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 28 May 2026 11:29:01 +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 0598336a59c..0446a766e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ ### Improvements -- Improve SDK init performance by deferring `ReplayIntegration.start()` off the main thread ([#XXXX](https://github.com/getsentry/sentry-java/pull/XXXX)) +- Improve SDK init performance by deferring `ReplayIntegration.start()` off the main thread ([#5474](https://github.com/getsentry/sentry-java/pull/5474)) ## 8.42.0