Skip to content

fix(cli): first render on Windows no longer fails while a new browser or ffmpeg is scanned - #5625

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/cli-timeout-reason-on-windows
Oct 15, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix/cli-timeout-reason-on-windows

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What changes

On Windows, the first render after Chrome or ffmpeg is freshly installed no longer fails its startup checks. When a check does time out, the error now says it timed out.

Why

Windows antivirus scans a freshly written executable on its first launch, and the --version / -version / -encoders probes gave it 5 s. Measured on a Windows 11 x64 PC with Microsoft Defender (each on a fresh copy of the binary, 3 tries each):

Binary First launch Second launch
chrome-headless-shell 152 (209 MB), CPU busy 6.7 s, 6.7 s, 6.8 s 41 to 46 ms
ffmpeg (79 MB), CPU idle 8.8 s, 10.9 s, 13.4 s 47 to 117 ms

CPU load on its own is not the cause: during a 4K x265 encode at 93 to 100% CPU, chrome-headless-shell --version on an already-scanned binary took 42 to 55 ms across 10 runs.

A real render hit this and failed with Chrome cannot start: Failed to run "...chrome-headless-shell.exe" --version., with no reason given. That was a second bug. On timeout the runner kills the process tree with taskkill /T /F. If the process exits before taskkill gets to it, taskkill exits with 128, and its error replaced the timeout. So the user saw neither the timeout nor its cause.

How

  • FIRST_LAUNCH_TIMEOUT_MS = 60_000 (in browser/ffmpeg.ts) is the one budget for the probes of these binaries: Chrome --version, ffmpeg/ffprobe -version and both -encoders probes. 60 s is about 4.5x the worst first launch measured. A hung binary still fails, only later.
  • runCancellableProcess: once the child has closed, a failed tree kill no longer replaces the reason it was stopped (timeout, abort, output limit).

What I measured

  • cancellableProcess.killFailure.test.ts (new): the tree kill fails and the run still rejects with ETIMEDOUT. On the old code it rejects with taskkill exited with status 128.
  • The preflight tests pin the new budget. Putting it back to 5 s fails 4 of them.
  • preflight, ffmpeg, cancellableProcess and processTree tests: 42 pass, 3 runs in a row on Linux. oxfmt, oxlint and tsc are clean for the changed files.

What I did NOT exercise

  • I did not rerun a full render on Windows with this build. The Windows numbers above come from timing the binaries directly.
  • No macOS or Linux first-launch timing. The budget only matters where a first launch is slow.

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 180 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Somu review: head ea6cf731. Clean, approving.

What I checked

  • Kill failure no longer replaces the real reason. In runCancellableProcess, both the close handler and finishReject now ignore a rejected terminationTask. A tree-kill failure therefore can't overwrite the timeout, abort or ENOBUFS reason.
    • This doesn't introduce a new hang. Both paths still settle only on close or error, as before. If a kill really fails and the child stays alive, the old code hung in the same place.
  • Regression test is red-first. cancellableProcess.killFailure.test.ts mocks terminateProcessTree to reject while the real child exits at 400 ms.
    • On the old code, the close handler routed the kill error through finishReject, so the message was taskkill exited with status 128.
    • At this head it is Process timed out after 50 ms / ETIMEDOUT, which is what the test asserts.
  • One shared 60 s budget. FIRST_LAUNCH_TIMEOUT_MS replaces all six 5 s probes: Chrome --version, ffmpeg/ffprobe -version, and both -encoders paths. The worst first launch you measured was 13.4 s, so 60 s leaves headroom, and a binary that truly hangs still fails.
  • CI: all required checks green, including Tests and Render on windows-latest. Mergeable.

Nits (non-blocking)

  • Kill failure is now silent. When the parent has exited but taskkill still fails, any surviving grandchildren go unreported. That's fine for --version probes, but a debug-level log in the .catch would keep the signal.
  • Sync encoder probe can block longer. detectH264EncoderMode uses execFileSync, so a hung ffmpeg now blocks the event loop for up to 60 s instead of 5 s. render.ts uses the async variant, so this only affects sync callers.

— Somu

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 15, 2026
@miguel-heygen
miguel-heygen removed this pull request from the merge queue due to a manual request Oct 15, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 03a9fab Oct 15, 2026
66 checks passed
@miguel-heygen
miguel-heygen deleted the fix/cli-timeout-reason-on-windows branch October 15, 2026 01:09
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.

2 participants