Skip to content

HF_DE_PARALLEL_ROUTER is on by default and hangs native WSL capture at frame 0 with no output; its circuit breaker cannot fire on a hang #3441

Closed

Description

@rnstev26

Summary

The drawElement parallel-router experiment (HF_DE_PARALLEL_ROUTER) is enabled by default. On WSL2 it hangs native capture at frame 0/300 for ~60 s with zero console output — no error, no warning, no progress.

The per-install circuit breaker that should retire the experiment after a bad render never armed across 5 consecutive failed renders. Reading dist/cli.js from the published 0.8.10 bundle, that appears structural rather than incidental: the breaker latches only when the router "reverted", and a hang produces no outcome to latch on.

Environment

  • hyperframes 0.8.10, installed globally via npm
  • WSL2 / Ubuntu on Windows 11
  • 300-frame mp4 composition using drawElement capture
  • Code analysis below is against the same 0.8.10 npm bundle, read on macOS

Default-on is unconditional

// dist/cli.js:129066
function isDeParallelRouterEnabled(env) {
  const raw = env.HF_DE_PARALLEL_ROUTER?.trim().toLowerCase();
  if (raw === void 0 || raw === "") return true;   // unset => ON
  return !(raw === "false" || raw === "0" || raw === "off" || raw === "no");
}

shouldPreferParallelDrawElement (dist/cli.js:129071) gates on worker count, output format, drawElement usage and a RAM floor. There is no install-age, render-count or trial-cohort term — every install with >1 worker doing an mp4 drawElement render is routed.

Why the safety net never armed

// dist/cli.js:145071
function maybeConsumeDeParallelRouterTrial(routerActive, job, quiet) {
  if (!routerActive) return;
  const outcome = resolveDeParallelRouterOutcome(job);
  if (outcome === void 0) return;                  // <-- a hang exits here
  ...
  const fired = outcome === "reverted";            // <-- only a graceful revert latches
  if (fired) { config.deParallelRouterTrialFired = true; ... }
}

resolveDeParallelRouterOutcome reads job.perfSummary?.drawElement?.parallelRouter ?? job.errorDetails?.observability?.capture.deParallelRouter. A render that stalls and is killed populates neither, so outcome is undefined and the function returns before writing the latch.

Both call sites — dist/cli.js:144855 in the catch, and :144868 after success — require executeRenderJob to return or throw. A stalled process the user interrupts reaches neither.

Net effect: the breaker covers the failure mode that already recovered on its own (verify-fail → revert → continue), and is inert against the one that does not (indefinite silent hang). The experiment stays armed for the next render, indefinitely.

There is a second path on which it cannot arm: when options.manageDeParallelRouterBreaker !== true, deParallelRouterActive is set to false, and the in-source comment at dist/cli.js:144809-144812 states the router still runs but the trip is "neither enforced nor recorded."

Reproduction

unset HF_DE_PARALLEL_ROUTER
cd <project> && hyperframes render > /tmp/stall.log 2>&1; echo $?

Redirect rather than pipe — a pipe reports the tail's exit status, not the render's.

Observed: stalls at frame 0/300 for ~60 s, no console errors, 5/5 renders failed.

With HF_DE_PARALLEL_ROUTER=0: the same composition renders to completion — 306.6 KB, beginFrameStalled: false, verified by extracting and viewing frames rather than trusting the exit code.

Not a Chrome or WSL capability problem: raw chrome-headless-shell --screenshot works normally in the same environment (9,887 B, exit 0).

Config after 5 failed renders: ~/.hyperframes/config.json contained no deParallelRouterTrialFired key.

Suggested fixes

  1. Latch the breaker on any non-success of a routed render, not only on "reverted".
  2. Arm a pre-render latch that a successful render clears, so a killed process still leaves evidence behind.
  3. Emit something during the stall — 60 s of silence gives the user nothing to search for, which is what made this take five renders to localize.

Possibly related

#3423 reports a drawElement capture path failing at frame 0 where a documented fallback did not trigger. Different flag and a hard error rather than silence, so likely distinct, but the "documented fallback did not happen" shape is the same.

Activity

  1. added a commit that references this issue on Aug 25, 2026
    3202f3f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions