fix(engine): self-verify parallel disk drawElement samples#2749
Conversation
|
Smoke-verified the integration live on darwin/arm64 in the exact reported configuration ( The damage itself is not reproducible on a high-memory machine (needs GPU/memory pressure — 16GB class). Detection is mechanism-agnostic though: any damage class diverging >32dB from ground truth at a sampled frame now trips the existing screenshot-fallback retry, and the reports describe whole-worker-range damage, which sampled verification catches with near-certainty. First field self-verify failure log on a ≥this-PR build will double as the repro confirmation. |
|
Fault-injection results (HF_DE_VERIFY_MIN_DB=60 forces healthy ~47dB frames to breach) — found and fixed a recovery gap:
Two honest open items from the same testing:
Both noted so reviewers weigh this PR as: parallel-disk verify + recovery proven end-to-end by injection; sequential-disk verify still open. |
|
Both open items closed in Sequential disk verify — Routing nondeterminism mooted — the anomalous runs were the sequential/probe-reuse branch, which previously skipped verification entirely. Every disk-path DE configuration (parallel workers, sequential, probe-reused, distributed chunk) now verifies against the same armed samples. Whichever branch a run lands on, unverified DE frames can no longer ship. Suites green: engine 46/46, producer render stages 296+. |
Self-review (max) caught a blocker in my own fix — fixed in c85cfae🔴 The parallel-disk recovery was a silent no-op. Fix: rethrow on Now genuinely proven — fault injection ( Also from the review: deduped the PSNR-threshold clamp onto the shared Suites: engine parallelCoordinator 46/46, producer render (vitest-visible) 296/296 + capturePlan 9/9, streaming stage 10/10 under bun. The 10 vitest 'file failures' are pre-existing |
miga-heygen
left a comment
There was a problem hiding this comment.
SSOT Review: disk-path drawElement self-verification (PRINFRA-352)
PR: fix(engine): self-verify parallel disk drawElement samples — 9 files, +403/-80
The bug
Parallel disk drawElement capture armed pre-injection ground-truth samples (resolveParallelDeVerifySamples) but only the streaming drain ever CHECKED them. The disk path armed samples and never compared — damaged frames shipped silently. On a 16GB host with two concurrent hardware-GPU Chromes, compositor-tile-eviction damage produces frames with vertical black slabs for one worker's entire range.
Architecture
The fix follows the existing streaming-drain verification shape exactly:
- Verify: after a worker's range completes, re-read captured frame files at the armed sample indices and PSNR-compare against the session's ground truth.
- Signal: a breach throws
DrawElementVerificationError(same error type, same message phrase forclassifyCaptureFailure). - Recover: the orchestrator's existing pinned-fallback retry converts it into a screenshot re-render — identical recovery to the streaming drain.
SSOT checks
-
psnrDb— single source. Moved fromcaptureStreamingStage.tsto@hyperframes/engineutils/psnr.ts. The streaming drain and the disk verify both import from the same module. The old local copy is deleted entirely — not left as a re-export or alias. ✓ -
resolveDeVerifyMinDb— single source. Same move. The inline clamp logic ([10, 60]range, 32 default) now lives in one function. The streaming drain's warn (which needs a logger) stays local but uses the shared function for the value. "The disk and streaming verify paths can never apply different PSNR floors to the same composition." ✓ -
selectVerifySampleIndicesForTask— pure, tested. Filters armed sample indices to those inside[startFrame, endFrame)and on the task's stride lattice. MirrorscaptureFrameRange'si += strideloop exactly. Tests cover: contiguous 2-worker split (the exact 3032-frame repro), interleaved stride, and empty result. ✓ -
verifyDiskDrawElementSamplesguards are exhaustive:streaming→ return (drain already verifies, no double-check). ✓captureMode !== "drawelement"→ return. ✓- No truths armed → return. ✓
- Infrastructure failure (missing file, ffmpeg error) → skip sample, not a breach. ✓
- PSNR below threshold → throw
DrawElementVerificationError. ✓
-
executeDiskCaptureWithAdaptiveRetryrethrows DE verify errors. Critical invariant:findMissingFrameRangeschecks presence/size only — damaged frames are complete files with wrong pixels. Without the rethrow, the missing-frame check would count them present and wrongly return success. ✓ -
replanAfterFailurehandlessdr_disk+draw_element_verification. Returns a new plan withforceScreenshot: true, preservingworkerCount. The test pins this:sdr_disk,forceScreenshot: true,workerCount: 2, no streaming. ✓ -
Disk-path retry is thorough:
- Wipes
framesDir(damaged frames are untrusted but satisfy completeness). ✓ - Resets capture state (
dedupPerfs.length = 0,probeSession = null,cfg.useDrawElement = false). ✓ - Replans and syncs (
syncCapturePlan). ✓ - Clears the failure on successful retry (
observability.clearFailure). ✓ - Telemetry via shared
deVerifyFallbackTelemetry. ✓
- Wipes
-
deVerifyFallbackTelemetry— shared. Both streaming and disk verify catches extract structured details through the same function.kindis read structurally off the error, not from message text. ✓ -
Sequential disk path also verified.
captureStage.tscallsverifyDiskDrawElementSampleswith a synthetic task matching the sequential range. Same function, same recovery. This closes the gap for the sequential path too (reachable via probe-session reuse under the fast-capture opt-in). ✓
Stale concept scan
psnrDblocal copy incaptureStreamingStage.ts— deleted, replaced by import. ✓resolveDeVerifyMinDbinline logic in the drain guard — replaced by shared function call, only the warn stays local. ✓- No "TODO: verify disk path" comments survive. ✓
Blocking SSOT issues
None found.
Verdict
Approve. The fix plugs the enforcement gap by reusing the exact same machinery the streaming drain already has — same PSNR comparison, same threshold, same error type, same recovery chain. The PSNR function consolidation into utils/psnr.ts ensures both paths can never diverge. The findMissingFrameRanges bypass for correctness failures is the critical safety detail, and the framesDir wipe before retry is the correct consequence of that. Ship it.
— Miga
… mocks don't crash at import
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 4889b779c (Graphite stack root, latest commit fix(engine): lazy-promisify execFile in psnr…).
Sound diagnosis + narrow fix. The design — arm samples uniformly, check them at range-completion, throw a typed error, ride the existing pinned-fallback retry — is right, and the commit history shows self-correction on the sharp edges (structural rethrow past findMissingFrameRanges, lazy promisify for the CLI mock, sequential-path parity, telemetry helper extraction). One resource-leak concern on the retry catch, everything else verified-safe.
Concerns
-
🟠 Probe session leaked on the disk-verify retry path (
renderOrchestrator.ts:3246).
WheninvokeDiskCapturethrowsDrawElementVerificationError, the catch handler doesprobeSession = null;without first closing the session. IfrunCaptureStage's parallel branch threw from insideexecuteDiskCaptureWithAdaptiveRetry(the common case for this fallback), control leavescaptureStage.tsBEFORE the success-pathcloseCaptureSession(probeSession)atcaptureStage.ts:243-245. The orchestrator still holds the reference — but nulling it here severs it. Theexecution.defer("close probe session", …)closure atrenderOrchestrator.ts:1761-1766checksif (!probeSession) return;against the SAME variable the catch just nulled, so the deferred cleanup then skips. The Chrome process is orphaned until Node exits.Reachable only via
--experimental-fast-capture --workers N>1+ a verify breach + non-consumed probe session, and the leak is bounded to render-process lifetime (~minutes). But the fix is a one-liner — close before nulling:if (probeSession) { await closeCaptureSession(probeSession).catch(() => {}); } probeSession = null;
Worth checking
renderOrchestrator.ts:3093(streaming-path retry) for the same pattern — I didn't verify it end-to-end but the shape looks identical.
Nits
- 🔵 The sequential disk verify at
captureStage.ts:342-352builds a synthetic single-worker task inline. Not wrong — the field-mapping is unambiguous — but a named helper (createSequentialVerifyTask(rangeStart, rangeEnd, framesDir)) would document intent and let both call sites share the same shape guarantee. Follow-up if you agree; not required to land this.
Verified-safe
- The load-order bug the CLI test tripped is genuinely fixed.
psnrDb's lazypromisify(execFile)inside the function body (vs the earlier top-levelconst execFileP = promisify(execFile)) meanspackages/cli/src/telemetry/client.test.ts's partialvi.mock("node:child_process", …)no longer crashes any transitive importer ofpsnr.tsat Vitest module-load time. The prior CI red at92ffd0476reproduces the exact failure and4889b779cis the right narrow fix. Broader rule worth codifying: no top-level builtin promise wrappers in shared engine modules that show up in the CLI test import graph. isDrawElementVerificationErrorrethrow atrenderOrchestrator.ts:1032— closes thefindMissingFrameRangesfalse-positive hole (damaged frames are complete-on-disk and would pass the size/presence check). Structural detection walks the aggregatedCaptureFailure → worker CaptureFailure → DrawElementVerificationErrorcause chain, so a translated / reworded / cross-realm error can't sneak past.deVerifyFallbackTelemetryhelper atrenderOrchestrator.ts:1583-1594— clean extraction; the "kind is read STRUCTURALLY off the error, never from message text" invariant from the pre-existing streaming-path code is preserved verbatim in the doc-comment. Both call sites (streaming retry + new disk retry) now share one implementation. Adding a new field to the mapping updates one place.psnrDb+resolveDeVerifyMinDbcentralized in@hyperframes/engine/utils/psnr.ts. Producer's local copies deleted, streaming drain (captureStreamingStage.ts:64-65) and parallel disk verify (parallelCoordinator.ts:28) both import from engine. Threshold clamp[10, 60] → default 32matches the pre-existing streaming-path clamp behavior; no drift possible.selectVerifySampleIndicesForTask— pure.(idx - startFrame) % stride !== 0filter mirrorscaptureFrameRange'si += strideloop shape exactly. Sort at end ensures deterministic verify order. 3 test cases (contiguous / interleaved stride 3 / empty range) cover the branches the loop actually takes.verifyDiskDrawElementSamples— early-returns onstreaming || session.captureMode !== "drawelement", so the streaming-drain path (which already runs the guard) can't get double-verified, and non-drawElement sessions skip.psnrForDiskSamplecatches ffmpeg spawn/tmpdir infra failures and returnsnull(skipped sample, NOT damage evidence) — only genuine below-floor PSNR throwsDrawElementVerificationError. Message keeps the"drawElement self-verify"phrase soclassifyCaptureFailure'sVERIFICATION_ERROR_PATTERNSstill classifies asverification.replanAfterFailuredisk-verify transition (capturePlan.ts:127-138) + its test (capturePlan.test.ts:143-163) — locked exactly:sdr_disk + draw_element_verification → sdr_diskwithforceScreenshot: true,workerCountpreserved (2 in the test),forceParallelStream: false. No cross-plan drift (sdr_streaming + draw_element_verificationstill throws the pre-existing invariant error).- Retry-path idempotency — the catch handler:
rmSync(framesDir, recursive)(wipes untrusted-but-complete files),mkdirSync(framesDir),resetCaptureAttemptProgress(job),dedupPerfs.length = 0(DE-session perf must not bleed into screenshot telemetry),cfg.useDrawElement = false,capturePlan = replanAfterFailure(…),syncCapturePlan(),updateCaptureObservability(…), andobservability.clearFailure("capture_disk")after the recovery succeeds. Every mutation the retry needs is accounted for; nothing gets carried over that would mislead the recovered attempt. - Sequential disk-path parity (
captureStage.ts:342-352). The PR body only names the parallel disk gap, but commit9fc1c2f15 fix(producer): verify sequential disk drawElement samples toocovers the sequential fast-capture path (also reachable under the explicit opt-in, including via probe-session reuse) with the same synthetic-task shape. Silent scope expansion vs the PR title — worth a body-note in the final Slack rollup so the merge audit trail catches it — but the code is right.
Questions
- Under what timeline are you planning to remove the
--experimental-fast-capturegate? The whole reason this verify gap existed was that the disk path was opt-in-only, so drain-time verification never made it here. Once the gate is dropped and the disk path becomes reachable via--workerswithout the flag, this verify becomes load-bearing (and the leak concern above becomes more consequential per render). Just calibration, not something you need to answer here.
What I didn't verify
- The streaming-retry pattern at
renderOrchestrator.ts:3093for the same probe-session-null-before-close shape. Worth eyeballing when you touch the disk-retry fix. - Actual runtime behavior of the
psnrDblazy fix under a real drain — the CI jobTestwas re-running against4889b779cat review time and hadn't yet reported; the fix is right by inspection and the earlier failure trace at92ffd0476reproduces on the exact call site the fix addresses. - Repro of PRINFRA-352 field conditions (darwin/arm64, 16GB, hardware-GPU Chrome,
--workers 2). Verified by design + test coverage, not by field repro.
CI at 4889b779c is running (Test, Windows tests, regression shards, typecheck all in progress); the earlier failures at 92ffd0476 were the load-time mock crash and its Windows echo, both addressed by the lazy-promisify fix. LGTM from my side — leaving as a comment. Fix the probe-session close before nulling and you're ready from where I sit.
miguel-heygen
left a comment
There was a problem hiding this comment.
The disk self-verification and recovery design is sound, but the new parallel-disk retry path leaks its probe Chrome session.
In renderOrchestrator.ts, invokeDiskCapture() enters runCaptureStage() with the existing probeSession. For workerCount > 1, that stage only closes the probe after executeDiskCaptureWithAdaptiveRetry() returns. A verification breach throws before reaching that cleanup, then the catch around line 3246 does probeSession = null and immediately starts the screenshot retry. The original probe browser is therefore orphaned until process exit, increasing memory pressure precisely on the GPU/memory-failure recovery path.
Please close the still-owned probe session before clearing it (with cleanup semantics consistent with the surrounding code), and add a focused regression that makes a parallel disk verify failure assert the probe is closed before the retry. The analogous streaming catch is different: its stage consumes/closes the probe in its own failure path.
Non-blocking: the default verification grid remains capped at 8 samples even though explicit/auto concurrency can exceed 8, so supported high-worker runs can leave entire contiguous worker ranges unsampled. The reported 2-worker case is well-covered, but this is worth a follow-up if high-worker disk fast capture is expected.
Verdict: REQUEST CHANGES
Reasoning: close the leaked probe session before the screenshot retry; the core PSNR, typed-failure, frames-dir wipe, and fallback mechanics otherwise check out.
— Magi
…etries On a parallel-capture disk-verify or streaming-drain breach, the outer catch cleared probeSession without first closing the still-owned session, orphaning the probe Chrome process precisely when the retry was recovering from GPU/memory pressure. Introduce closeOrphanedProbeForRetry so both retry catches close the session (with defensive .catch that logs on close error) before releasing the reference, and cover it with a focused unit test asserting closure-before-clear and the swallow-and-warn behaviour. Addresses Magi's REQUEST_CHANGES on #2749; also closes Rames' sibling concern at the streaming-retry path (renderOrchestrator.ts:3093). — Via
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review at exact head 5fd28e1149b4258ab16b3282c495d7fc54325280.
Verified: packages/producer/src/services/renderOrchestrator.ts:3122-3126 and :3285-3289 now snapshot the owned probe, preserve its browser console, clear the shared reference, and await closeOrphanedProbeForRetry before either screenshot retry begins. The extracted helper at :1481-1494 makes cleanup failure non-fatal while retaining an actionable warning. This closes both the parallel-disk leak I blocked on and the sibling streaming-retry shape Rames identified.
The final 5fd28e1149 delta over b8e1015476 is formatter-only: two files, whitespace wrapping only. Exact-head CI is fully green, including Test, Windows tests/render, all 8 regression shards, CodeQL, CLI smoke, and global install.
Important follow-up: packages/producer/src/services/renderOrchestrator.test.ts:2135-2191 tests the helper in isolation, but does not drive a disk verification failure through the orchestrator and therefore would not fail if the call at :3289 were later removed or reordered after the retry. Please add the originally requested integration-scoped ordering regression in a follow-up; the production wiring is correct by inspection, so I am not holding the fixed leak on that test gap.
Verdict: APPROVE
Reasoning: The resource leak is fixed at every matching retry site, the cleanup ordering is correct, and exact-head CI is green. The remaining gap is regression strength, not a live correctness defect.
— Magi
Summary
Fixes the enforcement gap behind PRINFRA-352 (parallel-capture "worker-boundary" corruption, darwin/arm64 0.7.68, n=2).
Diagnosis
The corrupt repro ran
--experimental-fast-capture --workers 2on a 16GB machine. The explicit fast-capture opt-in bypasses the parallel clamp (by design), producing parallel disk drawElement with zero runtime verification:resolveParallelDeVerifySampleseven raises density for multi-worker capture, citing exactly this compositor risk)…Two concurrent hardware-GPU Chromes on a 16GB host is the documented compositor-tile-eviction damage class (the wild 0.7.52 "vertical black slabs" report — the reason the DE router has a 24GB RAM floor). The "corruption starting exactly at the worker-boundary midpoint" is one worker's entire range being damaged: workers share no state; the boundary is simply where the damaged worker's output begins. Not a handoff bug.
Fix
After a disk-path worker completes its range, re-read its captured frame files at the armed sample indices and PSNR-compare against the session's pre-injection ground truth (
verifyDiskDrawElementSamples). A breach throwsDrawElementVerificationError(message keeps the contiguous "drawElement self-verify" phrase soclassifyCaptureFailurelabels itverification), and the orchestrator's existing pinned-fallback retry converts it into a screenshot re-render — identical recovery to the streaming drain. No new recovery machinery.psnrDb+resolveDeVerifyMinDbmoved to@hyperframes/engine(utils/psnr.ts) — one comparison implementation shared by the drain guard (producer) and the disk verify; producer's local copy deleted.selectVerifySampleIndicesForTaskexported pure: armed samples inside a task's[start, end)on its stride lattice (mirrorscaptureFrameRange's loop) — each sample checked by exactly the worker that owns the frame.Test plan
selectVerifySampleIndicesForTasktests: contiguous range (incl. the report's exact 3032/2-worker split), stride lattice, emptyparallelCoordinatorsuite 46/46; producer render-stage suites 296/296tsc --noEmitclean (engine, producer); engine build clean;oxlint/oxfmtcleanRelated: PRINFRA-352, PRINFRA-300, PRINFRA-340.
🤖 Generated with Claude Code