perf(android): Defer Session Replay start off SDK init critical path - #5904
perf(android): Defer Session Replay start off SDK init critical path#5904runningcode wants to merge 2 commits into
Conversation
The init-time ReplayController.start() ran synchronously on the main thread at the end of Sentry.init, paying for class-loading the capture strategy graph inline during app start. Post it to the main looper so init returns first. start() is idempotent, so the later lifecycle-driven start remains a no-op if this one ran first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d15471f | 303.49 ms | 439.08 ms | 135.59 ms |
| b750b96 | 408.98 ms | 480.32 ms | 71.34 ms |
| 17a0955 | 372.53 ms | 446.70 ms | 74.17 ms |
| d217708 | 355.34 ms | 381.39 ms | 26.05 ms |
| 5b1a06b | 310.56 ms | 362.79 ms | 52.22 ms |
| fcec2f2 | 311.35 ms | 384.94 ms | 73.59 ms |
| 62b579c | 349.26 ms | 426.26 ms | 77.00 ms |
| 5f14e5d | 325.76 ms | 368.32 ms | 42.56 ms |
| 48277cd | 320.38 ms | 379.90 ms | 59.52 ms |
| a416a65 | 295.53 ms | 373.74 ms | 78.21 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| b750b96 | 1.58 MiB | 2.10 MiB | 533.19 KiB |
| 17a0955 | 1.58 MiB | 2.10 MiB | 533.20 KiB |
| d217708 | 1.58 MiB | 2.10 MiB | 532.97 KiB |
| 5b1a06b | 0 B | 0 B | 0 B |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.51 KiB |
| 62b579c | 0 B | 0 B | 0 B |
| 5f14e5d | 1.58 MiB | 2.19 MiB | 620.00 KiB |
| 48277cd | 0 B | 0 B | 0 B |
| a416a65 | 1.58 MiB | 2.12 MiB | 555.26 KiB |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9495885. Configure here.
| // time. start() is idempotent, so the later start() from the app lifecycle integration | ||
| // (once the first activity is in foreground) is a no-op if this one ran first. | ||
| new Handler(Looper.getMainLooper()) | ||
| .post(() -> scopes.getOptions().getReplayController().start()); |
There was a problem hiding this comment.
Replay start deferred past app onCreate
Medium Severity
Handler.post only runs after the current main-looper message finishes. Auto-init runs inside handleBindApplication, so that message also includes Application.onCreate. Session Replay therefore starts only after Application.onCreate, and startup crashes there no longer get a replayId or recording. Previously start() ran synchronously during init, before onCreate continued.
Reviewed by Cursor Bugbot for commit 9495885. Configure here.
There was a problem hiding this comment.
I don't think this matters because session replays for startup crashes so early won't have anything visible anyways.
0xadam-brown
left a comment
There was a problem hiding this comment.
Nice. Looks reasonable to me, though feel free to check in with Roman if you need someone who knows more about possible side-effects of delaying start.
| @Config(sdk = [26]) | ||
| fun `init starts session replay if app is in foreground`() { | ||
| initSentryWithForegroundImportance(true) { _ -> | ||
| // replay start is posted to the main looper, so drain it before asserting |
There was a problem hiding this comment.
Looks like we should assert that isRecording() is false before draining the main looper b/c we don't want this test to continue to pass if we regress back to synchronous init.
|
Thanks, yes I had a chat with Roman about this and he will work on better thread confinement as part of #5847 and it will also achieve this same effect of posting a handler to avoid doing the work during startup. I'll close this for now. |


📜 Description
This posts the Session Replay start to a new Handler, delaying the start of the Session Replay. This just speeds up the
SentryAndroid.initbut the work is just done later.Alternatively, we could try moving some of that work to a background thread to move the work off the main thread but this is a one line easy change.
Benchmark results shows this speeds up the SDK start by another
10%!!💡 Motivation and Context
Reduce the main-thread work performed synchronously during
Sentry.init, improving SDK startup time on Android when Session Replay is enabled.💚 How did you test it?
Device benchmark (Pixel 3,
sentry-samples-android, Session Replay atsession-sample-rate = 1.0): ran thesentry-uitest-android-macrobenchmarkcold-start benchmark (CompilationMode.Full,StartupMode.COLD), A/B-ing baseline vs. this change in two interleaved rounds of 12 iterations each (first iteration per round dropped as a class-load outlier).The change lifts ~8.6 ms of synchronous work off the init critical path. Measured on the traced
SentryAndroid.initsection (thestart()work now runs outside it):SentryAndroid.initslicestart())start())Δ median ≈ −8.6 ms (~11%), consistent in both interleaved rounds (−5.5 ms, −11.7 ms), so it is not thermal drift.
Whole-app
timeToInitialDisplaytrended slightly faster (median 1330 → 1312 ms, tighter tail) but the ranges overlap — within cold-start noise for a change this small, as the benchmark README notes. So the init-section reduction is the reliable signal, not TTID.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
None.