fix(engine): cap auto workers by parent heap budget - #3803
Conversation
jrusso1020
left a comment
There was a problem hiding this comment.
Comment-only: this is a draft, so no stamp from me. Read the full parallelCoordinator.ts sizing path and the new test at head.
Your own description already names the central risk — that 640MB/worker and the 1024MB reserve come from one field report, that PRINFRA-341 deliberately deferred enforcement, and that this can materially reduce concurrency on low-default-heap hosts. I'm not going to restate that back at you. Everything below is what I found on top of it.
1. The in-source comment will contradict the code the moment this merges
The replacement comment on HEAP_PER_WORKER_MB reads "Validate this estimate against the workers_heap_ fleet telemetry before rollout (PRINFRA-341)"* while :409-412 now enforces the cap. The merge gate lives in the PR description, but the description is not what the next reader sees — the comment is, and it will say validation is still pending while enforcement is live. The old comment was self-consistent because it said "advisory-only" and the code was advisory. Two ways to keep that property: gate enforcement behind a config knob so the comment stays true until you flip it, or land it with the comment replaced by what validated the figure.
2. The fleet-visible thresholds are wider than the reported host's 6 → 4
The cap is Math.max(1, Math.floor((heapLimitMb - 1024) / 640)) (:323-326), so:
- On a default ~4GB Node heap it is 4 workers for every auto-sized render, regardless of core count. A 32-core host that the contention path sizes to 10 (per the comment's own "32 cores → 10 workers") also lands at 4. That is a much broader statement than the 18-core host going 6 → 4.
- Any host whose
heap_size_limitis below ~1664MB gets exactly 1 worker, because that is where the floor takes over. That also overrides the two-worker parallel floor by design (:403-412, pinned by "allows one worker below the heap reserve despite the parallel floor"), so a long render on such a host becomes fully serial.
Both numbers are worth putting in the benchmark plan explicitly, since they are what the fleet will actually experience.
3. exceedsHeapAdvisory becomes unreachable for auto-sized renders
:347 computes workers > heapBasedWorkers. Once enforcement guarantees finalWorkers <= heapBasedWorkers, that can only be true via the explicit-requested path, so the captureCost.ts warning is live only for explicit --workers. Your updated comment says as much, and keeping it is reasonable — flagging only because "defensive warning for a future alternate policy" is exactly the shape that reads as dead code to whoever touches it next. A one-line note naming the explicit path as its only live caller would prevent that.
4. Interaction with #3801, since these were sent together
Safe, and worth stating why: #3801's initialization retry sets currentWorkers = getNextRetryWorkerCount(currentWorkers) directly and never re-enters computeWorkerSizing, and it only ever halves — so the heap cap cannot be re-raised on retry, and the two changes compose in the same direction. The thing to notice is that they stack: on a default-heap host this PR sizes to 4, and a Network.enable timeout then halves to 2, so the retry concurrency the field sees is set by both PRs together rather than by either one.
5. The test is genuinely deterministic — I checked the two things that usually make this shape lie
parallelCoordinator.ts:8importscpusfrom bare"os", which is whatvi.mock("os", …)intercepts. Had it importednode:os, the mock would silently not apply and the CPU-cap case would be scored against this host's real core count.getHeapStatisticsis called exactly once percomputeWorkerSizing(:322), somockReturnValueOnceis sufficient and no assertion falls through to the real host heap.
Both fine as written. Mentioning it so the next person doesn't have to re-derive it.
Also minor: WorkerSizingBound is publicly exported (packages/engine/src/index.ts:258) and "heap" is a new member. The only consumer is telemetry passthrough (packages/cli/src/commands/render.ts:1525) — no exhaustive switch to break — so it is additive for JS consumers and mildly breaking only for a TS consumer switching exhaustively over the union.
Head reviewed: 5c8e8752d30ecf7f347e785271acd7792547fb7c
Verdict: COMMENT
Reasoning: The mechanism is correct, the floor prevents a zero-worker path, and the tests control their inputs properly — but it is a draft and the enforcement decision is the open question you already gated it on. Items 1 and 2 are the ones I would want resolved before it leaves draft.
— Rames Jusso
Review feedback on #3803. The HEAP_PER_WORKER_MB comment still read "validate this estimate before rollout" while the sizing path now enforces the cap, so the next reader would see pending validation next to live enforcement. State that the cap is enforced, that an explicit `--workers N` bypasses it, and record the two fleet-wide thresholds the constants imply: a default ~4GB heap selects 4 workers for every auto-sized render regardless of core count, and a heap_size_limit under ~1664MB selects 1 worker, overriding the two-worker parallel floor. PRINFRA-341 remains the pre-merge validation gate. Also correct the buildHeapAdvisoryWarning doc block. It claimed the warning stayed live as a defensive path, but its `requestedWorkers !== undefined` guard and the newly enforced cap are mutually exclusive: the auto path can no longer exceed heapBasedWorkers and the explicit path is guarded out, so the warning is unreachable for every real render. Keep it, because reverting the cap to advisory is PRINFRA-341's open question, and note that the exceedsHeapAdvisory flag itself stays live through CLI telemetry. Comments only; no behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reported 18-core/24GB host selected six capture workers despite a roughly 4GB parent V8 heap. Raising the Chrome RSS budget alone still selects six. Apply the existing parent-heap estimate after the parallel floor and contention cap; that host now selects four workers, and a heap below the reserve selects one. Explicit
--workersremains authoritative, and sizing telemetry reportsboundBy: heap.Related: PRINFRA-338, PRINFRA-341.
Why this is still a draft: the gate ran but the instrument was blind
Both draft blockers were executed. The honest outcome is "not decidable yet" — not a pass, and not the "this is wrong" I briefly concluded and have since retracted on PRINFRA-341.
The original report is worker-causal
From PRINFRA-300 fault mode #8: darwin/arm64 18c/24GB,
--quality high --fps 30 --strict-all, auto-selected 6 workers,FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory, and--workers 1produced correct output. Lowering the worker count fixed it. The mechanism this PR targets is real.What the fleet could and could not tell us
Queried the Hyperframes project (2,676,589
render_complete, 317,253render_error, 2026-07-21 → 2026-09-11):--quality highworkload.What I cannot tell you is whether 640MB/worker is the right magnitude, because
peak_memory_mbisprocess.memoryUsage.rss()sampled at completion — after per-frame buffering has drained. Worker-driven parent buffering is a mid-render phenomenon, so the field is blind to it by construction. An earlier revision of this description leaned on that data to argue the constant was wrong by ~250×; that claim is withdrawn.Telemetry gap, now fixed separately
workers_bound_by,workers_heap_based,workers_heap_limit_mbandworkers_exceed_heap_advisoryship onrender_completeonly — 0 of 317,253render_errorevents carry them, so advisory-true renders can never be correlated with failures.Both problems are addressed in
fix/render-error-sizing-telemetry: sizing on the failure path, and the existing sampler's true running peak instead of the teardown snapshot (pluspeak_heap_used_mb). That branch is independent of this one and worth landing regardless of what happens here.What would settle this PR
--quality high, long render, 6 workers, ~4GB heap — reading the true peak. That directly tests the constant and needs no fleet data.HEAP_PER_WORKER_MBto whatever that measures, then merge or drop this.The mechanism is sound and the code implements it correctly; the open question is purely the magnitude of the constant.
Validation performed
56 coordinator tests passed, including five deterministic heap-budget cases; engine and producer typechecks, scoped oxlint/oxfmt, and the fallow new-issue gate passed. CI green 60/60.
Review feedback applied in
df205869: theHEAP_PER_WORKER_MBcomment no longer says "validate before rollout" next to live enforcement, and thebuildHeapAdvisoryWarningdoc block is corrected — itsrequestedWorkers !== undefinedguard and the newly enforced cap are mutually exclusive, so the warning is unreachable rather than "live only for explicit--workers". TheexceedsHeapAdvisoryflag it reads stays live via CLI telemetry.What the constants would do fleet-wide, for the record