Skip to content

fix(replay): Release MediaMuxer when the encoder fails to start#5607

Merged
runningcode merged 5 commits into
mainfrom
no/fix-replay-muxer-leak-on-start-failure
Jul 23, 2026
Merged

fix(replay): Release MediaMuxer when the encoder fails to start#5607
runningcode merged 5 commits into
mainfrom
no/fix-replay-muxer-leak-on-start-failure

Conversation

@runningcode

Copy link
Copy Markdown
Contributor

📜 Description

The MediaMuxer is opened eagerly in SimpleVideoEncoder's constructor, but its release() was only reachable on paths that assume start() succeeded. Two cases leaked it when the encoder failed to start:

  • ReplayCache.createVideoOf constructed the encoder and called start() in a single expression (SimpleVideoEncoder(...).also { it.start() }). If start() threw, the assignment to the encoder field never happened, so the only caller of release() could never run.
  • SimpleVideoEncoder.release() released the muxer as the last statement of the try block, after draining and stopping the codec. Draining a codec that never started throws, which jumped to the catch and skipped the muxer release.

The fix releases the encoder if start() throws (then rethrows), and always releases the muxer from a finally block so it is freed even when draining/stopping the codec fails.

💡 Motivation and Context

This surfaced as a CloseGuard "A resource was acquired at attached stack trace but never released" warning pointing at the MediaMuxer allocation in SimpleMp4FrameMuxer. It is a real resource leak on devices where the encoder fails to start, not just a test artifact. Complements #5583, which closed the no-frames and never-started paths.

💚 How did you test it?

Added a regression test (ReplayCacheTest.releases the muxer when the encoder fails to start) that forces start() to fail via the test MediaCodec shadow and asserts, through ShadowCloseGuard.getErrors(), that no MediaMuxer leak is reported. Verified the test fails without the fix (with the exact Explicit termination method 'release' not called error) and passes with it. The full ReplayCacheTest suite is green.

📝 Checklist

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

None.

@sentry

sentry Bot commented Jun 23, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 345.80 ms 378.31 ms 32.51 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

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

Previous results on branch: no/fix-replay-muxer-leak-on-start-failure

Startup times

Revision Plain With Sentry Diff
23ac7bc 304.46 ms 364.53 ms 60.07 ms
5f1e9d5 327.00 ms 385.27 ms 58.27 ms

App size

Revision Plain With Sentry Diff
23ac7bc 0 B 0 B 0 B
5f1e9d5 0 B 0 B 0 B

@romtsn romtsn 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.

sorry this slipped, looks great!

@runningcode
runningcode force-pushed the no/fix-replay-muxer-leak-on-start-failure branch from 5cdf0c4 to 938d944 Compare July 23, 2026 10:21
@runningcode
runningcode marked this pull request as ready for review July 23, 2026 10:21
The MediaMuxer is opened eagerly in SimpleVideoEncoder's constructor, but
its release() was only reachable on paths that assume start() succeeded.
Two cases leaked it:

- createVideoOf constructed the encoder and called start() in one
  expression, so when start() threw the encoder was never assigned and
  release() could never run.
- SimpleVideoEncoder.release() released the muxer as the last statement of
  the try block, after draining and stopping the codec. Draining a codec
  that never started throws, skipping the muxer release.

Release the encoder if start() throws, and always release the muxer from a
finally block so it is freed even when draining/stopping the codec fails.
This surfaced as a CloseGuard "resource was acquired but never released"
warning. Complements #5583.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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 938d944. Configure here.

@runningcode
runningcode force-pushed the no/fix-replay-muxer-leak-on-start-failure branch from 938d944 to 45b652d Compare July 23, 2026 10:24
runningcode and others added 4 commits July 23, 2026 12:52
MediaMuxer.stop() throws IllegalStateException when the muxer was started
but no sample was ever written to its track. SimpleMp4FrameMuxer.release()
only guarded against the never-started case, so a started-but-empty muxer
made release() throw.

Because release() runs from SimpleVideoEncoder's finally block, that throw
propagates out to createVideoOf, which treats release() as safe cleanup;
the encoder is left dangling and the orphan video file is never deleted.

Only call stop() when at least one sample was written; muxer.release() on a
started-but-not-stopped muxer is safe.
The finally block in SimpleVideoEncoder.release() released the codec,
surface, and muxer without guards. release() is treated by callers such as
createVideoOf as safe cleanup, but MediaMuxer.stop() (reached via
frameMuxer.release()) can still throw IllegalStateException on a genuine
file-finalization failure even when samples were written. That would skip
the remaining releases and propagate out of release().

Guard each release independently so failing to free one resource neither
skips the others nor escapes to the caller.
@runningcode
runningcode force-pushed the no/fix-replay-muxer-leak-on-start-failure branch from 3af58a2 to 3066764 Compare July 23, 2026 10:53

@romtsn romtsn 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!

@runningcode
runningcode enabled auto-merge (squash) July 23, 2026 14:00
@runningcode
runningcode merged commit bbcf99a into main Jul 23, 2026
69 of 72 checks passed
@runningcode
runningcode deleted the no/fix-replay-muxer-leak-on-start-failure branch July 23, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants