Skip to content

fix: guard render encodes with the stall watchdog and serialize them on Android#177

Merged
hm21 merged 1 commit into
stablefrom
fix/export-gate-hardening
Jul 11, 2026
Merged

fix: guard render encodes with the stall watchdog and serialize them on Android#177
hm21 merged 1 commit into
stablefrom
fix/export-gate-hardening

Conversation

@hm21

@hm21 hm21 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Context

Follow-up hardening for the process-wide export gate added in #176. That change serialized split and render re-encodes on iOS/macOS so they stop starving each other on the hardware encoder, and added a stall/timeout watchdog to the split. A review of the merged code surfaced gaps where the gate could re-introduce the very deadlock it was meant to fix, plus a missing-parity gap on Android. This PR closes them.

Since 2.6.0 was never published, these fixes fold into the 2.6.0 changelog rather than a new version.

What changed

iOS / macOS

  • Render encodes are now guarded by a shared ExportWatchdog (extracted from the split path). Previously the render held a gate slot but had no watchdog, so a wedged VideoToolbox session held the single slot forever and deadlocked every later split/render. A wedged render is now force-cancelled and releases its slot.
  • The watchdog uses a monotonic clock (immune to NTP/system-clock steps) and relaxes rather than disables the stall bound during the progress-silent container finalize (moov rewrite), so a healthy finalize isn't misread as a stall while a wedged finalize still terminates.
  • BitrateCappedExporter completes its continuation on cancel (resume-once terminator + writer teardown), so a writer pump parked on a wedged encoder can no longer hang the body and leak the gate.

Android

  • Split and render now share a process-wide ExportGate (parity with iOS/macOS). A split queued behind a running render now waits for the encoder instead of the stall watchdog failing it fast.
  • Render gained a stall watchdog with an adaptive bound: because Media3 reports whole-percent progress, a fixed bound would false-kill any render slower than 1%/bound (e.g. a ~34-min render). It now tolerates 3× the observed average time-per-percent while still catching a stuck-at-0 wedge quickly.
  • A synchronous Transformer.start() failure and the failure-path duration probe no longer leak the gate or block the main thread; the cancel/stall terminals also clean up the pre-rendered audio temp files.

Dart

  • SplitVideoModel now asserts stallTimeout < exportTimeout (otherwise the hard bound pre-empts stall detection).

Tests

  • ExportGateTest.kt — JVM unit test for the gate primitive (FIFO hand-off, single/idempotent release, no slot leak).
  • Watchdog failure-path integration test (a tight bound deterministically fires the diagnostic).
  • Dart assertion test for stallTimeout < exportTimeout.

Verification

  • Android module compiles; all unit tests pass (ExportGateTest, SplitExportDiagnosticsTest).
  • All Dart tests pass; dart analyze and dart format clean.
  • Swift files pass swiftc -parse.
  • The changes went through two rounds of adversarial multi-agent review; every confirmed finding is addressed.

⚠️ Note: the darwin SwiftPM package can't be fully compiled standalone (the Flutter/FlutterMacOS module only exists at app-build time), so the iOS/macOS concurrency paths were validated by inspection + review, not a full compile. An Xcode/device build is the final gate for the darwin changes.

…on Android

Follow-up hardening for the process-wide export gate: the gate serialized
split and render encodes on iOS/macOS but left gaps that could re-introduce
the deadlock it set out to fix.

iOS/macOS:
- Render encodes are now guarded by a shared ExportWatchdog (extracted from the
  split path), so a wedged render is force-cancelled and releases its gate slot
  instead of holding it forever and deadlocking every later export.
- The watchdog uses a monotonic clock (immune to system-clock steps) and
  *relaxes* rather than disables the stall bound during the progress-silent
  container finalize, so a healthy moov rewrite is not misread as a stall while
  a wedged finalize still terminates.
- BitrateCappedExporter completes its continuation on cancel (via a resume-once
  terminator + writer teardown), so a writer pump parked on a wedged encoder
  can no longer hang the body and leak the gate.

Android:
- Split and render now share a process-wide ExportGate (parity with iOS/macOS);
  a split queued behind a running render waits for the encoder instead of the
  stall watchdog failing it fast.
- Render gained a stall watchdog with an adaptive bound that tolerates slow long
  renders (Media3 reports whole-percent progress) while still catching a
  stuck-at-0 wedge and releasing the gate.
- A synchronous Transformer.start() failure and the failure-path duration probe
  no longer leak the gate or block the main thread; the cancel/stall terminals
  clean up the pre-rendered audio temps.

Dart: SplitVideoModel asserts stallTimeout < exportTimeout.

Tests: ExportGate JVM unit test, a watchdog failure-path integration test, and
a Dart assertion test.
@hm21 hm21 merged commit 27eb1f0 into stable Jul 11, 2026
1 check passed
@hm21 hm21 deleted the fix/export-gate-hardening branch July 11, 2026 13:17
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.

1 participant