Skip to content

fix(render): bound and recover capture stalls - #3700

Open
miguel-heygen wants to merge 4 commits into
mainfrom
magi/fix-sequential-capture-stall
Open

fix(render): bound and recover capture stalls#3700
miguel-heygen wants to merge 4 commits into
mainfrom
magi/fix-sequential-capture-stall

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What

Wedged capture operations now fail on bounded worker phases and retry through the existing fresh screenshot plan. Sequential watchdogs name their resolved mode, while parallel drawElement workers report whether they stopped during browser launch/probing, session initialization, first-frame capture, or pipelined frame encoding.

Why

The original 60-second parallel watchdog measured only completed frames. A stall at 0/N could therefore mean browser launch, Chrome probing, drawElement initialization, first capture, or the in-page encoder, and logs did not identify the executable/build, CanvasDrawElement result, GPU backend, or worker.

Router-pinned parallel fallback was already correct: it aborts peers, closes every worker session, and retries once via screenshot. This change reduces the default drawElement worker-operation bound to 30 seconds and makes that recovery attributable; it does not add another retry layer.

How

  • The existing drawElement serial path retains its 15-second renderer-specific deadline.
  • Sequential screenshot/BeginFrame/drawElement stalls retain the conservative 60-second no-progress bound and exactly-once fresh-session retry.
  • Parallel drawElement streaming wraps browser launch/probe, session initialization, capture, and pipelined encoding with HF_DE_PARALLEL_PHASE_TIMEOUT_MS (30 seconds by default; 0 disables).
  • Phase events carry worker/frame, actual browser executable/version when available, CanvasDrawElement activation, and GPU backend. The aggregate watchdog includes the last phase for every worker if it remains the final safety net.
  • Parent cancellation and encoder interruption still bypass fallback.

System Chrome causality remains unproven because the report did not include an exact browser build, CanvasDrawElement capability, or GPU backend comparison.

Test plan

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (not applicable)

  • Engine parallel deadline/coordinator/peer-abort suites: 53 tests passed.

  • Producer streaming-stage lifecycle/watchdog suite: 12 tests passed.

  • Producer sequential-stall retry policy: 4 tests passed.

  • Engine and producer package typechecks passed.

  • Changed-file oxlint, oxfmt, and diff checks passed.

The full orchestrator suite previously passed 203/204 tests. The remaining alpha + outputResolution contract-drift assertion is unrelated to capture-stall recovery.

miguel-heygen and others added 3 commits September 5, 2026 12:35
… instead of failing (#3172)

* fix(engine): bound drawElement frames so a wedged renderer falls back instead of failing

A single drawElement frame could kill an entire render. On one comp
(caption-editorial-emphasis) drawElementImage returns normally and the renderer
then stops draining its task queue: the setTimeout(…, 0) that schedules
toDataURL never fires, the capture page.evaluate never settles, and 60s later
the stage watchdog fails the whole render. Deterministic on 152.0.7977.30,
always the same frame. Root-cause detail in PRINFRA-488.

Two gaps, both closed here.

1. Nothing below the 60s stage watchdog bounded a frame. Adds a per-frame
   deadline (HF_DE_FRAME_TIMEOUT_MS, default 15000, 0 disables) around the WHOLE
   frame operation, not just the drawElementImage call — the stall surfaces at
   whichever page round-trip comes next, and was observed at both the capture
   and the seek's background-image decode. Bounding one call missed it.

   Deliberately NO per-frame screenshot fallback for this error: once the
   renderer stops scheduling it is wedged for every subsequent round-trip on
   that page. Measured — the screenshot fallback blew the same deadline. The
   frame fails fast instead, so the recovery that actually works can run.

2. That recovery was ineligible. shouldRetryViaPinnedFallback only retried a
   generic capture failure when the worker count was PINNED by the inversion or
   the router; a comp that engaged drawElement on the ordinary single-worker
   path had no whole-render fallback at all. A renderer stall is now retryable
   on any routing — the failure is a property of drawElement itself, and the
   retry re-renders on a fresh page via screenshot. Reported as
   de_fallback_reason "de_renderer_stall" so it is distinguishable from
   capture_error in telemetry, and counted per-session as deFrameTimeouts.

Verified end to end on the repro: previously exit 1 with "stalled: no frame
progress for 60000ms"; now the deadline fires, the render retries via
screenshot, and RENDER_OK with a valid 1920x1080 / 240-frame / 8.0s MP4.

Tests cover the predicate both ways plus the cross-package error match. Engine
1,481 and producer 587 green (audioPadTrim.integration flakes only under
full-suite parallel load — passes in isolation, and on clean HEAD it is green
too, so it is not from this change).

Refs PRINFRA-488

* fix(engine,producer,cli): count drawElement frame timeouts where they happen

`session.deFrameTimeouts` was incremented inside `captureFrameCore`'s catch,
but the deadline races that function from OUTSIDE it — `withFrameDeadline` is
applied by `captureFrameToBuffer`, and `DeFrameTimeoutError` is constructed
nowhere else. So the branch was unreachable: on a wedged renderer the counter
stayed 0, the engine-side stall log never printed, and
`CapturePerfSummary.deFrameTimeouts` reported 0 on every affected render. The
end-to-end recovery worked throughout — `isDeRendererStallError` matches by
name at the producer — only the observability was dead.

Moved the increment and the log into an `onTimeout` hook that fires from the
deadline itself, deleted the unreachable branch and the now-unused
`isDeFrameTimeoutError`, and threaded the count out to
`render_completed.de_frame_timeouts` so the rate is graphable apart from the
other `capture_error` fallbacks. A stall always costs a whole-render re-run,
which is why it deserves its own series.

Also corrected the `fallbackReason` docstring, which still listed only
`psnr | blank | oom | capture_error` after this branch added
`de_renderer_stall`.

`withFrameDeadline` is exported for a two-case fake-timer test: the deadline
fires and calls `onTimeout` once, and a resolving inner promise passes its
value through with no late timer. The first case fails if the counter moves
back inside the work promise.

(cherry picked from commit ec2b2b5)
@miguel-heygen miguel-heygen changed the title fix(render): recover sequential drawElement stalls fix(render): recover sequential capture stalls Sep 5, 2026
@miguel-heygen miguel-heygen changed the title fix(render): recover sequential capture stalls fix(render): bound and recover capture stalls Sep 5, 2026
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