From 1b95af8bbba63e914e99eef29f56ec71f9a13551 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sat, 11 Jul 2026 12:21:08 -0700 Subject: [PATCH] fix(engine): densify drawelement self-verify with parallel worker count --- packages/engine/src/services/frameCapture.ts | 4 ++- .../src/services/parallelCoordinator.test.ts | 22 +++++++++++++++ .../src/services/parallelCoordinator.ts | 28 ++++++++++++++++++- packages/engine/src/types.ts | 10 +++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index d6c20d6ba1..fbde3e5c12 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -3125,7 +3125,9 @@ async function captureDeVerificationFrames( page: Page, logInitPhase: (phase: string) => void, ): Promise { - const kRaw = Number(process.env.HF_DE_VERIFY ?? "4"); + // Explicit HF_DE_VERIFY wins; otherwise the session's own sample count + // (raised by the parallel coordinator for multi-worker DE), then default 4. + const kRaw = Number(process.env.HF_DE_VERIFY ?? session.options.deVerifySamples ?? "4"); const k = Number.isFinite(kRaw) ? Math.max(0, Math.min(8, Math.floor(kRaw))) : 4; if (k === 0 || process.env.HF_FORCE_DRAWELEMENT === "1") return; if (session.options.format === "png") return; // worker-encode drain (the consumer) is jpeg-only diff --git a/packages/engine/src/services/parallelCoordinator.test.ts b/packages/engine/src/services/parallelCoordinator.test.ts index ce86bbbecd..1c1a6952bf 100644 --- a/packages/engine/src/services/parallelCoordinator.test.ts +++ b/packages/engine/src/services/parallelCoordinator.test.ts @@ -6,6 +6,7 @@ import { selectWorkerDiagnostics, shouldDisableBrowserPoolForParallelWorker, shouldVerifyWorkerGpu, + resolveParallelDeVerifySamples, } from "./parallelCoordinator.js"; import type { EngineConfig } from "../config.js"; @@ -176,3 +177,24 @@ describe("shouldVerifyWorkerGpu", () => { expect(shouldVerifyWorkerGpu(3, undefined)).toBe(false); }); }); + +describe("resolveParallelDeVerifySamples", () => { + it("densifies with worker count: 4 base + 2 per extra worker", () => { + expect(resolveParallelDeVerifySamples(undefined, 2)).toBe(6); + expect(resolveParallelDeVerifySamples(undefined, 3)).toBe(8); + }); + + it("clamps at the verify path's max of 8", () => { + expect(resolveParallelDeVerifySamples(undefined, 5)).toBe(8); + expect(resolveParallelDeVerifySamples(undefined, 16)).toBe(8); + }); + + it("leaves single-worker capture on the session default", () => { + expect(resolveParallelDeVerifySamples(undefined, 1)).toBeUndefined(); + expect(resolveParallelDeVerifySamples(undefined, 0)).toBeUndefined(); + }); + + it("passes a caller-set value through untouched", () => { + expect(resolveParallelDeVerifySamples(2, 3)).toBe(2); + }); +}); diff --git a/packages/engine/src/services/parallelCoordinator.ts b/packages/engine/src/services/parallelCoordinator.ts index f2b766a49c..d2111a6903 100644 --- a/packages/engine/src/services/parallelCoordinator.ts +++ b/packages/engine/src/services/parallelCoordinator.ts @@ -459,6 +459,24 @@ async function executeWorkerTask( } } +/** + * drawElement self-verify sample count for multi-worker capture. Each worker + * arms the same shared sample grid but drains only ~1/N of it, and N + * concurrent hardware-GPU browsers are exactly where compositor-tile damage + * shows up (wild 0.7.52 black-slab report) — so density rises with worker + * count: 4 base + 2 per extra worker, clamped to the verify path's max of 8. + * A caller-set value passes through untouched, and explicit HF_DE_VERIFY + * still overrides inside the session. + */ +export function resolveParallelDeVerifySamples( + callerValue: number | undefined, + workerCount: number, +): number | undefined { + if (callerValue !== undefined) return callerValue; + if (workerCount <= 1) return undefined; + return Math.min(8, 4 + 2 * (workerCount - 1)); +} + export async function executeParallelCapture( serverUrl: string, workDir: string, @@ -499,12 +517,20 @@ export async function executeParallelCapture( }; const parallel = tasks.length > 1; + const deVerifySamples = resolveParallelDeVerifySamples( + captureOptions.deVerifySamples, + tasks.length, + ); + const workerCaptureOptions: CaptureOptions = + deVerifySamples === captureOptions.deVerifySamples + ? captureOptions + : { ...captureOptions, deVerifySamples }; const results = await Promise.all( tasks.map((task) => executeWorkerTask( task, serverUrl, - captureOptions, + workerCaptureOptions, createBeforeCaptureHook, signal, onFrameCaptured, diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts index fc267821fe..24507d7ea9 100644 --- a/packages/engine/src/types.ts +++ b/packages/engine/src/types.ts @@ -158,6 +158,16 @@ export interface CaptureOptions { * warmup loop). */ lockWarmupTicks?: boolean; + /** + * drawElement self-verify ground-truth sample count for this session. + * Overrides the HF_DE_VERIFY default (4). The parallel coordinator raises + * it for multi-worker drawElement capture — N concurrent hardware-GPU + * browsers widen the damage surface (compositor tile eviction under + * GPU/memory pressure), and each worker only drains ~1/N of the shared + * sample grid, thinning effective coverage exactly when risk peaks. + * Clamped to 0..8 like the env knob; HF_DE_VERIFY, when set, still wins. + */ + deVerifySamples?: number; } export interface CaptureVideoMetadataHint {