fix(replay): Dispatch session-deadline stop off the replay worker thread#5826
Draft
runningcode wants to merge 2 commits into
Draft
fix(replay): Dispatch session-deadline stop off the replay worker thread#5826runningcode wants to merge 2 commits into
runningcode wants to merge 2 commits into
Conversation
…ead (DART-323) When a session replay reaches its duration deadline, the stop was invoked inline from the replay worker thread inside the frame-processing task. Because ReplayExecutorService.submit() runs tasks synchronously when already on that thread, stop() encoded the final segment and deleted the replay cache while holding the replay lifecycle lock. A foreground start() on the main thread then parked on that lock long enough to trigger a background ANR. Dispatch the deadline stop through options.executorService so the segment encoding is enqueued asynchronously and the lifecycle lock is only held for the fast state transition, matching the existing timer-executor stop path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📲 Install BuildsAndroid
|
Contributor
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 22f4345 | 325.23 ms | 454.66 ms | 129.43 ms |
| 22f4345 | 312.78 ms | 347.40 ms | 34.62 ms |
| 8558cac | 306.16 ms | 355.24 ms | 49.09 ms |
| bb0ff41 | 317.76 ms | 384.66 ms | 66.90 ms |
| 7c1a728 | 289.46 ms | 368.15 ms | 78.69 ms |
| d501a7e | 314.55 ms | 343.34 ms | 28.79 ms |
| 4fc476b | 280.63 ms | 363.04 ms | 82.42 ms |
| 6727e14 | 337.22 ms | 373.94 ms | 36.71 ms |
| ae7fed0 | 293.84 ms | 380.22 ms | 86.38 ms |
| ee747ae | 400.46 ms | 423.61 ms | 23.15 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 22f4345 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| 22f4345 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| 8558cac | 0 B | 0 B | 0 B |
| bb0ff41 | 0 B | 0 B | 0 B |
| 7c1a728 | 0 B | 0 B | 0 B |
| d501a7e | 0 B | 0 B | 0 B |
| 4fc476b | 0 B | 0 B | 0 B |
| 6727e14 | 1.58 MiB | 2.28 MiB | 718.64 KiB |
| ae7fed0 | 1.58 MiB | 2.12 MiB | 551.77 KiB |
| ee747ae | 1.58 MiB | 2.10 MiB | 530.95 KiB |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📜 Description
When a session replay reaches its duration deadline (default 1h), the resulting
stop()was invoked inline from the replay worker thread, inside theadd_frametask inSessionCaptureStrategy.onScreenshotRecorded. BecauseReplayExecutorService.submit()runs tasks synchronously when the caller is already on aSentryReplayIntegration-*thread,stop()encoded the final video segment and recursively deleted the replay cache while holding the replay lifecycle lock (AutoClosableReentrantLock).A foreground
start()(LifecycleWatcher.onForeground→startSession) runs on the main thread and must acquire that same lock, so it parked long enough to trigger a background ANR.This change dispatches the deadline
stop()throughoptions.executorService(a non-worker thread).stop()then submits the segment encoding asynchronously onto the single-threaded replay executor (preserving ordering), and the lifecycle lock is held only for the fast state transition — matching the existing timer-executorendSessionstop path.💡 Motivation and Context
Fixes the background ANR reported in getsentry/sentry-dart#3556 (Linear: DART-323), whose stack shows the main thread parked on the replay lifecycle lock in
ReplayIntegration.start()on foreground. The deadline stop re-entering the lifecycle lock from the worker thread is the only path where the segment encode runs synchronously under the lock; every otherstart/stop/pause/resumecaller already submits the encode asynchronously.💚 How did you test it?
Added a regression test in
SessionCaptureStrategyTestusing a deferred executor that asserts the deadlinestop()is dispatched off the calling thread (not run inline) and only runs once the executor drains. Updated the existing deadline test's fixture to use an immediate executor so it still exercises the dispatched stop.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
None.