Skip to content

Commit b118172

Browse files
runningcodeclaude
andcommitted
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 <noreply@anthropic.com>
1 parent adad078 commit b118172

4 files changed

Lines changed: 46 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
```
2525
- Parse ART memory and garbage collector info from ANR tombstones into ART context ([#5428](https://github.com/getsentry/sentry-java/pull/5428))
2626

27+
### Improvements
28+
29+
- Improve SDK init performance by deferring `ReplayIntegration.start()` off the main thread ([#XXXX](https://github.com/getsentry/sentry-java/pull/XXXX))
30+
2731
## 8.42.0
2832

2933
### Features

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public class ReplayIntegration(
161161
lifecycle.currentState >= STARTED && lifecycle.currentState < STOPPED
162162

163163
override fun start() {
164+
val isFullSession: Boolean
164165
lifecycleLock.acquire().use {
165166
if (!isEnabled.get()) {
166167
return
@@ -174,7 +175,7 @@ public class ReplayIntegration(
174175
return
175176
}
176177

177-
val isFullSession = random.sample(options.sessionReplay.sessionSampleRate)
178+
isFullSession = random.sample(options.sessionReplay.sessionSampleRate)
178179
if (!isFullSession && !options.sessionReplay.isSessionReplayForErrorsEnabled) {
179180
options.logger.log(
180181
INFO,
@@ -184,30 +185,39 @@ public class ReplayIntegration(
184185
}
185186

186187
lifecycle.currentState = STARTED
187-
captureStrategy =
188-
replayCaptureStrategyProvider?.invoke(isFullSession)
189-
?: if (isFullSession) {
190-
SessionCaptureStrategy(
191-
options,
192-
scopes,
193-
dateProvider,
194-
replayExecutor,
195-
replayCacheProvider,
196-
)
197-
} else {
198-
BufferCaptureStrategy(
199-
options,
200-
scopes,
201-
dateProvider,
202-
random,
203-
replayExecutor,
204-
replayCacheProvider,
205-
)
206-
}
207-
recorder?.start()
208-
captureStrategy?.start()
188+
}
189+
190+
// Defer the expensive work (strategy creation, recorder start, listener registration)
191+
// off the calling thread to avoid blocking SentryAndroid.init() on the main thread.
192+
options.executorService.submitSafely(options, "ReplayIntegration.start") {
193+
lifecycleLock.acquire().use {
194+
captureStrategy =
195+
replayCaptureStrategyProvider?.invoke(isFullSession)
196+
?: if (isFullSession) {
197+
SessionCaptureStrategy(
198+
options,
199+
scopes,
200+
dateProvider,
201+
replayExecutor,
202+
replayCacheProvider,
203+
)
204+
} else {
205+
BufferCaptureStrategy(
206+
options,
207+
scopes,
208+
dateProvider,
209+
random,
210+
replayExecutor,
211+
replayCacheProvider,
212+
)
213+
}
214+
recorder?.start()
215+
captureStrategy?.start()
216+
}
209217

210-
registerRootViewListeners()
218+
// Post to main thread since registerRootViewListeners triggers View operations
219+
// (e.g. addOnLayoutChangeListener) via the OnRootViewsChangedListener callback
220+
mainLooperHandler.post { registerRootViewListeners() }
211221
}
212222
}
213223

sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import io.sentry.android.replay.ReplayIntegrationWithRecorderTest.LifecycleState
1818
import io.sentry.android.replay.util.ReplayShadowMediaCodec
1919
import io.sentry.rrweb.RRWebMetaEvent
2020
import io.sentry.rrweb.RRWebVideoEvent
21+
import io.sentry.test.ImmediateExecutorService
2122
import io.sentry.transport.CurrentDateProvider
2223
import io.sentry.transport.ICurrentDateProvider
2324
import io.sentry.util.thread.NoOpThreadChecker
@@ -44,7 +45,11 @@ class ReplayIntegrationWithRecorderTest {
4445
@get:Rule val tmpDir = TemporaryFolder()
4546

4647
internal class Fixture {
47-
val options = SentryOptions().apply { threadChecker = NoOpThreadChecker.getInstance() }
48+
val options =
49+
SentryOptions().apply {
50+
threadChecker = NoOpThreadChecker.getInstance()
51+
executorService = ImmediateExecutorService()
52+
}
4853
val scopes = mock<IScopes>()
4954

5055
fun getSut(

sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import io.sentry.SentryReplayEvent.ReplayType
2121
import io.sentry.android.replay.util.ReplayShadowMediaCodec
2222
import io.sentry.rrweb.RRWebMetaEvent
2323
import io.sentry.rrweb.RRWebVideoEvent
24+
import io.sentry.test.ImmediateExecutorService
2425
import io.sentry.transport.CurrentDateProvider
2526
import io.sentry.transport.ICurrentDateProvider
2627
import java.time.Duration
@@ -59,7 +60,7 @@ class ReplaySmokeTest {
5960
@get:Rule val tmpDir = TemporaryFolder()
6061

6162
internal class Fixture {
62-
val options = SentryOptions()
63+
val options = SentryOptions().apply { executorService = ImmediateExecutorService() }
6364
val scope = Scope(options)
6465
val scopes =
6566
mock<IScopes> {

0 commit comments

Comments
 (0)