Skip to content

fix(producer): degrade multi-worker disk capture to single-worker streaming when headroom is insufficient - #5334

Open
CasbaL wants to merge 3 commits into
heygen-com:mainfrom
CasbaL:fix/disk-headroom-streaming-fallback
Open

CasbaL wants to merge 3 commits into
heygen-com:mainfrom
CasbaL:fix/disk-headroom-streaming-fallback

Conversation

@CasbaL

@CasbaL CasbaL commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #4060. Successor to #4061, which was closed as stale and can no longer be reopened (GitHub blocks reopening a PR whose head branch was force-pushed while closed) — same branch name, rebased onto v0.8.143.

Of the original two-part fix, upstream has since absorbed the estimate half: JPEG now bills at raw/8 and a measured projection replaces the static estimate once ten frames exist. What remains valuable — and what this PR is — is the degrade.

Problem. When the pre-capture disk-headroom gate rejects a render, the render fails outright even when single-worker streaming was eligible: auto-parallel renders always take the disk path, and the gate throws inside runCaptureStage instead of feeding a replan. A machine with modest free disk therefore failed renders that single-worker streaming could finish.

What changed (two pieces, split along main's existing seams):

  1. capturePlan.ts — a disk_headroom CapturePlanFailure and the matching replanAfterFailure transition: sdr_disk → the fully hardened single-worker streaming plan, keeping the original forceScreenshot and reverting any active routing so telemetry records the lost bet. On any other plan kind it throws instead of falling through to the generic retry tail, which would silently replan a streaming plan.
  2. renderOrchestrator.ts — the policy predicate shouldDegradeDiskCaptureToStreaming (multi-worker sdr_disk + no headroom + shouldUseStreamingEncode(cfg, format, 1, duration) eligible — so png-sequence/gif, the operator duration cap, and enableStreamingEncode=false keep the gate error), hooked right after the capture_strategy checkpoint and before capture runs. The degrade emits its own disk_headroom_degraded checkpoint, so degraded renders are countable rather than indistinguishable.

Intentionally unchanged: the estimate itself. Upstream's policy of billing PNG at full raw RGBA is kept — png-sequence/gif are streaming-ineligible, so for those renders the pre-capture refusal is the only safe outcome on a tight disk, and this degrade cannot help them by design. (This also retires the frameExt blocker from #4061: the new diff never touches frameExt.)

Also corrects the streaming-gate doc comment, which still described disk frames as raw RGBA after the estimate became format-keyed (follow-up 2 from #4061).

Test plan

  • capturePlan.test.ts: degrade transition (multi-worker disk → hardened single-worker streaming, input plan never mutated), active-routing revert records the lost bet with frozen routing, and disk_headroom on a streaming plan throws.
  • renderOrchestrator.test.ts: new shouldDegradeDiskCaptureToStreaming decision table (5 cases — multi-worker disk + no headroom + streaming-eligible degrades; sufficient headroom / streaming or layered plans / ineligible streaming / single-worker plans do not).
  • capturePlan / captureStage / renderOrchestrator suites pass (333 passed; the 2 failing resolveDeviceScaleFactor cases fail identically on clean v0.8.143 — pre-existing and unrelated).
  • tsc --noEmit clean for @hyperframes/producer.

…eaming when headroom is insufficient

When the pre-capture disk-headroom gate rejects a render, the render
failed outright even when single-worker streaming was eligible:
auto-parallel renders always take the disk path, and the gate threw
inside runCaptureStage instead of feeding a replan. A machine with
modest free disk therefore failed renders that single-worker streaming
could finish.

Two pieces, split along the existing seams:

- capturePlan.ts grows a 'disk_headroom' CapturePlanFailure and the
  matching replanAfterFailure transition: sdr_disk ->
  createCapturePlan's fully hardened single-worker streaming shape,
  keeping the original forceScreenshot and reverting any active
  routing so telemetry records the lost bet. disk_headroom on any
  other plan kind throws instead of falling through to the generic
  retry tail, which would silently replan a streaming plan.
- renderOrchestrator.ts grows the policy predicate
  shouldDegradeDiskCaptureToStreaming (multi-worker sdr_disk + no
  headroom + single-worker streaming eligible, reusing
  shouldUseStreamingEncode so png-sequence/gif, the operator duration
  cap, and enableStreamingEncode=false keep the gate error) and hooks
  it right after the capture_strategy checkpoint, before capture runs.

Also corrects the streaming-gate doc comment, which still described
disk frames as raw RGBA after the estimate became format-keyed.

Refs heygen-com#4060.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. #4060 is still open and still reproduces in shape on main: a multi-worker mp4 render captures to disk, and if the up-front headroom estimate does not fit, the render fails even when a one-worker run would stream with no disk at all. Main's answer today (#5934) is an error message telling the user to re-run with --workers 1. This PR makes that same choice automatically, before capture starts.

What I checked:

  • The predicate reuses shouldUseStreamingEncode(cfg, format, 1, duration) for eligibility, so png-sequence, gif, the duration cap and PRODUCER_ENABLE_STREAMING_ENCODE=false still get the gate error. Single-worker disk plans are left alone. That is the same condition diskCaptureRemedy uses to decide whether to suggest --workers 1, so the two agree.
  • The replan goes through replanAfterFailure and syncCapturePlan, so motion blur and --resolution route checks still run on the new plan (both are supported on sdr_streaming). Active routing is reverted, the input plan is not mutated, and a disk_headroom failure on any other plan kind throws instead of falling into the generic tail.
  • framesDir already exists at that point (created near the top of the pipeline), so the statfs reads a real directory.
  • I merged this head onto current main (no conflicts) and ran capturePlan.test.ts plus renderOrchestrator.test.ts: 316 passed. tsc --noEmit for producer is clean.

Two non-blocking notes:

  • The new tests cover the predicate and the replan, but nothing drives the orchestrator hook itself. A small test that stubs free disk low on a multi-worker mp4 render and asserts the disk_headroom_degraded checkpoint would pin the wiring.
  • With this in, the --workers 1 branch of diskCaptureRemedy is only reachable from the measured mid-capture projection, not the up-front gate. That is fine, just worth knowing when reading that message later.

Need: yes, renders that one worker could finish still fail on modest disks · Size: ok · Reuse: built on the existing replanAfterFailure, inspectDiskCaptureHeadroom and shouldUseStreamingEncode seams · Simpler: already minimal

— Rames

@jrusso1020

Copy link
Copy Markdown
Collaborator

Thanks, @CasbaL. This one is approved, and it'll go into the merge queue once these are sorted:

Once it's green and signed, we'll queue it at your new head. If you push code changes beyond a rebase, we'll take another look first.

— Rames

@jrusso1020

Copy link
Copy Markdown
Collaborator

This is approved at the current head, but main requires signed commits, so GitHub won't merge it yet. Could you re-sign the commits (git rebase main --exec 'git commit --amend --no-edit -S') and force-push? We'll re-approve right after.

— Rames

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checked at 169a158. The disk-headroom replan routes through replanAfterFailure in capturePlan.ts, the one place every capture-plan fallback already goes through, and renderOrchestrator.ts decides the degrade with shouldDegradeDiskCaptureToStreaming. PNG and alpha outputs still refuse, as before.

I ran capturePlan.test.ts and renderOrchestrator.test.ts: 317 passed. With capturePlan.ts and renderOrchestrator.ts put back to the merge base, the new cases fail (8 failures, for example "degrades a multi-worker disk plan when disk lacks headroom and streaming is eligible"), so the tests guard the change.

Verdict: APPROVE
Reasoning: fixes the remaining half of #4060 at the planner that owns fallbacks, tests fail without it, CI green.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One change before this can merge: the JSDoc on shouldDegradeDiskCaptureToStreaming (renderOrchestrator.ts:1577-1584) is 8 lines, and the repository merges comments of at most 3 to 4 lines that say what the code cannot. The function's name and its four arguments already say most of it. Something like this keeps the one non-obvious point:

/** Single-worker disk plans stay on disk: they are there because streaming is not eligible. */

The rest of the reasoning reads well in the PR description. Everything else is ready: the tests pass and fail without the change, and CI is green. Please push that and sign the commit; we will merge it once it is green.

@CasbaL
CasbaL requested a review from miguel-heygen October 27, 2026 02:58

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at 0b5f9e7782d2ef0f1635b05e6f40b985bd060430.

The last source delta trims the JSDoc exactly as Miguel requested. The fallback still reuses the existing planner and only degrades a multi-worker disk plan when one-worker streaming is eligible.

Compared current source to reviewed planner and orchestration contracts, no render execution. Earlier full 317-test result is Miguel-attributed at 169a158. Two unsigned commits remain. Fresh independent fallback source pass also found no blocker; no new CI claim.

Need: yes · Size: ok · Reuse: existing implementation seam · Simpler: already minimal · Structure: fine
Risk: medium - capture planner fallback and disk headroom.

Verdict: APPROVE
Reasoning: The last source delta trims the JSDoc exactly as Miguel requested. The fallback still reuses the existing planner and only degrades a multi-worker disk plan when one-worker streaming is eligible.

Reasoning: The last source delta trims the JSDoc exactly as Miguel requested. The fallback still reuses the existing planner and only degrades a multi-worker disk plan when one-worker streaming is eligible.

This branch has not been deployed

No deployments
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.

[Windows] Disk-capture headroom gate rejects ordinary renders: uncompressed-RGBA estimate (~16 GB for ~1 min of 1080p) and no streaming fallback

3 participants