fix(replay): Don't let a wedged video encoder freeze the app - #5842
Merged
Conversation
romtsn
requested review from
0xadam-brown,
adinauer,
markushi and
runningcode
as code owners
July 27, 2026 15:37
📲 Install BuildsAndroid
|
Contributor
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6b019b7 | 319.84 ms | 333.15 ms | 13.31 ms |
| d15471f | 322.58 ms | 396.08 ms | 73.50 ms |
| b750b96 | 421.25 ms | 444.09 ms | 22.84 ms |
| d217708 | 411.22 ms | 430.86 ms | 19.63 ms |
| 604a261 | 380.65 ms | 451.27 ms | 70.62 ms |
| 5b1a06b | 352.27 ms | 413.70 ms | 61.43 ms |
| fcec2f2 | 357.47 ms | 447.32 ms | 89.85 ms |
| 7414e9b | 370.39 ms | 422.18 ms | 51.79 ms |
| fcec2f2 | 311.35 ms | 384.94 ms | 73.59 ms |
| f6cdbf0 | 314.19 ms | 357.59 ms | 43.40 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 6b019b7 | 0 B | 0 B | 0 B |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| b750b96 | 1.58 MiB | 2.10 MiB | 533.20 KiB |
| d217708 | 1.58 MiB | 2.10 MiB | 532.97 KiB |
| 604a261 | 1.58 MiB | 2.10 MiB | 533.42 KiB |
| 5b1a06b | 0 B | 0 B | 0 B |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.50 KiB |
| 7414e9b | 0 B | 0 B | 0 B |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.51 KiB |
| f6cdbf0 | 0 B | 0 B | 0 B |
runningcode
reviewed
Jul 27, 2026
runningcode
left a comment
Contributor
There was a problem hiding this comment.
looks good, i left a question for a discussion!
0xadam-brown
approved these changes
Jul 28, 2026
0xadam-brown
left a comment
Member
There was a problem hiding this comment.
A few quick comments for your consideration (no blockers); otherwise lgtm
runningcode
approved these changes
Jul 29, 2026
romtsn
force-pushed
the
rz/fix/replay-encoder-wedge-anr
branch
from
August 7, 2026 16:39
6d7152c to
228b6e9
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 228b6e9. Configure here.
Some hardware encoders never emit BUFFER_FLAG_END_OF_STREAM after signalEndOfInputStream(), so SimpleVideoEncoder.drainCodec() spun forever while holding encoderLock. ReplayCache.close() then blocked on that lock, and since it runs inline under ReplayIntegration's lifecycleLock, the main thread froze until the system killed the process. Two bounds, both needed: the drain loop now gives up after 10 consecutive no-progress iterations (~1s), and close() only waits 2s for the encoder lock before skipping the release. The loop bound alone isn't enough -- a native dequeueOutputBuffer call can itself never return, since ALooper::awaitResponse has no deadline of its own. Adds AutoClosableReentrantLock.tryAcquire(timeout, unit) for the latter. Fixes getsentry/sentry-dart#3556 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It's an abnormal condition that drops frames, and it fires at most once per segment, so it's worth surfacing without debug logging enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move isClosed.set(true) into a finally block so it runs even on unchecked exceptions. Add a released flag to ReplayShadowMediaCodec and assert that close() releases the encoder when the lock is available, and skips it when timed out. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
romtsn
force-pushed
the
rz/fix/replay-encoder-wedge-anr
branch
from
August 11, 2026 21:49
228b6e9 to
c40bd6f
Compare
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
Fixes an ANR where Android apps freeze on foreground/background transitions until the system kills the process.
A reporter pulled the full deobfuscated 131-thread dump from Play Console (the Sentry ANR event only carries
main, which is why the lock holder was invisible), giving us the exact chain:Two cooperating defects, so two bounds — both are needed:
SimpleVideoEncoder.drainCodec()had awhile (true)with no exit on theendOfStream = truepath. Some hardware encoders never emitBUFFER_FLAG_END_OF_STREAMaftersignalEndOfInputStream(), so the loop spun forever while holdingencoderLock. It now gives up after 10 consecutive no-progress iterations (~1s at the existing 100ms poll) and drops the remaining frames.ReplayCache.close()used an unboundedencoderLock.acquire(). It runs inline on the caller's thread fromBaseCaptureStrategy.stop(), whichReplayIntegration.stop()invokes while holdinglifecycleLock— that nesting is what promotes a stuck encoder into a frozen main thread. It now waits at most 2s and skips the release otherwise.The loop bound alone isn't sufficient: the native frames show
MediaCodec::dequeueOutputBuffer→AMessage::postAndAwaitResponse→ALooper::awaitResponsewithMediaCodec_looperidle.TIMEOUT_USECis handed to the codec looper, which is supposed to reply once it elapses — a dead looper never replies, andawaitResponsehas no deadline of its own. So a single call can never return, and only bounding the lock wait keeps the lifecycle path free.On the timeout path the (already-dead) codec is not released, leaking a native handle. That's the deliberate trade: leaking a handle in a process whose encoder is wedged beats freezing the app.
isClosed.set(true)still runs on every path, sincepersistSegmentValuesgates on it.The other four
encoderLock.acquire()sites inReplayCacheare unchanged — they all run on the replay worker, the thread that would be stuck, so a deadline there buys nothing.Adds
AutoClosableReentrantLock.tryAcquire(timeout, unit)(@ApiStatus.Internal, additive) for the second bound. Returns the token on success andnullon timeout, so callers must branch explicitly rather than silently no-op via?.use {}.Net effect: a wedged codec degrades to "replay stops working for this process" instead of "the app freezes."
Fixes getsentry/sentry-dart#3556
Fixes #5870
💚 How did you test it?
ReplayShadowMediaCodecgained two hooks, since it ignorestimeoutUsand always eventually produces EOS:neverSignalEosandblockOnDequeue. New tests cover both bounds inReplayCacheTest, plus 4 fortryAcquireinAutoClosableReentrantLockTest.Both
ReplayCacheTestcases were mutation-checked — reverting each fix in turn makes the matching test fail with its assertion message rather than hang. Full:sentry-android-replay:testReleaseUnitTestpasses;apiDumpadds exactly the onetryAcquireline.No device repro is available (Redmi Note 14 Pro 5G / Pro+ 5G, Android 16, MediaTek), so the shadow-based tests are the verification of record.
📝 Checklist
sendDefaultPiiis enabled.🤖 Generated with Claude Code