Skip to content

perf(android): Defer Session Replay start off SDK init critical path - #5904

Closed
runningcode wants to merge 2 commits into
mainfrom
claude/replay-startup-delay-cc77b9
Closed

perf(android): Defer Session Replay start off SDK init critical path#5904
runningcode wants to merge 2 commits into
mainfrom
claude/replay-startup-delay-cc77b9

Conversation

@runningcode

@runningcode runningcode commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📜 Description

This posts the Session Replay start to a new Handler, delaying the start of the Session Replay. This just speeds up the SentryAndroid.init but 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 at session-sample-rate = 1.0): ran the sentry-uitest-android-macrobenchmark cold-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.init section (the start() work now runs outside it):

SentryAndroid.init slice median mean min max
baseline (inline start()) 80.3 ms 81.3 75.8 89.3
this change (posted start()) 71.7 ms 72.9 67.8 96.2

Δ median ≈ −8.6 ms (~11%), consistent in both interleaved rounds (−5.5 ms, −11.7 ms), so it is not thermal drift.

Whole-app timeToInitialDisplay trended 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

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

None.

runningcode and others added 2 commits August 6, 2026 16:50
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>
@sentry

sentry Bot commented Aug 6, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.52.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 315.29 ms 375.74 ms 60.45 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

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

@runningcode
runningcode marked this pull request as ready for review August 6, 2026 15:20

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9495885. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this matters because session replays for startup crashes so early won't have anything visible anyways.

@runningcode runningcode added the sanity-check PR needs a lightweight review for obvious issues label Aug 6, 2026

@0xadam-brown 0xadam-brown left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@runningcode

Copy link
Copy Markdown
Contributor Author

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.

@runningcode runningcode closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sanity-check PR needs a lightweight review for obvious issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants