diff --git a/CHANGELOG.md b/CHANGELOG.md index c21a8cce7d..0446a766e1 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 ([#5474](https://github.com/getsentry/sentry-java/pull/5474)) + ## 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 07e91d7648..b6770d0770 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 75626f4e4c..d6dcdb1ad7 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 c26e6be9c4..b55b302033 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 {