From d047d28bb402170be3d993d316ad92ae1d8e08e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 14 Jul 2026 22:04:49 -0400 Subject: [PATCH] fix(capture): bound static dedup verification time (#2457) ## What - cap static-dedup verification at 15 seconds of wall-clock time - disable the optimization and continue normal capture when the verification budget is exhausted - add a regression that models the reported 350-second / ~8,400-frame alpha render ## Why Static-frame verification uses full-page screenshots. Its existing screenshot-count budget still scales with composition duration, so a long composition can spend minutes proving an optimization before frame capture starts. The reported 350.35-second ProRes alpha render spent about eight minutes in this phase before safely disabling dedup. ## How The verifier now records a deadline before seeking verification frames. It checks the deadline before every full-page capture and returns the existing fail-closed `budgetExhausted` result when time is exhausted. This keeps the existing density-based safety checks while bounding their startup cost. ## Test plan - [x] Unit tests added/updated - [ ] Manual testing performed - [ ] Documentation updated (not applicable) - [x] Focused engine test: 9/9 passed - [x] Engine typecheck passed - [x] Pre-commit lint, format, tracked-artifact, fallow, and typecheck gates passed - [x] Full engine suite: 988 passed, 3 skipped; 2 pre-existing environment failures because host FFmpeg 4.2 lacks `-fps_mode` --- ...meCapture-staticDedupVerifyDensity.test.ts | 31 +++++++++++++++++++ packages/engine/src/services/frameCapture.ts | 10 ++++++ 2 files changed, 41 insertions(+) diff --git a/packages/engine/src/services/frameCapture-staticDedupVerifyDensity.test.ts b/packages/engine/src/services/frameCapture-staticDedupVerifyDensity.test.ts index 43824272f..3035654ad 100644 --- a/packages/engine/src/services/frameCapture-staticDedupVerifyDensity.test.ts +++ b/packages/engine/src/services/frameCapture-staticDedupVerifyDensity.test.ts @@ -183,4 +183,35 @@ describe("verifyStaticFramesSafe catches drift the old fixed-point density would expect(seekCalls.map((call) => Math.round(call.t * fps))).toEqual([0, 1, 2, 0]); expect(seekCalls.every((call) => call.options?.suppressEvents === true)).toBe(true); }); + + it("disarms within a wall-clock budget instead of spending minutes verifying a long static run", async () => { + const fps = 24; + let nowMs = 0; + const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => nowMs); + const page = { + evaluate: vi.fn(async () => undefined), + }; + vi.mocked(pageScreenshotCapture).mockImplementation(async () => { + nowMs += 8_000; + return Buffer.from("same"); + }); + + // Mirrors the reported 350.35s / 23.976fps alpha composition closely: + // ~8,400 predicted-static frames used to schedule ~352 full-page PNG + // screenshots before capture could begin. + const staticFrames = new Set(); + for (let frame = 1; frame < 8_400; frame++) staticFrames.add(frame); + + const result = await verifyStaticFramesSafe( + { options: {} } as unknown as CaptureSession, + page as unknown as Parameters[1], + staticFrames, + fps, + 24, + ); + nowSpy.mockRestore(); + + expect(result?.budgetExhausted).toBe(true); + expect(pageScreenshotCapture).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index a443e8abe..56cff28fc 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -2359,6 +2359,13 @@ export async function computeStaticFrameSet( // sampleCount, so that knob's effect on density stays monotonic (see below). const STATIC_VERIFY_REFERENCE_STRIDE = 24; +// Verification uses full-page screenshots, whose cost scales with canvas size +// and page complexity rather than frame count. Bound wall time as well as the +// capture count so a long composition cannot spend minutes proving an +// optimization before the real render starts. Exhaustion fails closed: dedup is +// disabled and normal capture proceeds. +const STATIC_VERIFY_MAX_MS = 15_000; + /** * Interior verification points for a run [a..b], plus the always-included end `b`. * Density used to be a flat point-count cap (min(sampleCount, 8)), so a run's @@ -2414,6 +2421,7 @@ export async function verifyStaticFramesSafe( ): Promise<{ badFrame: number; budgetExhausted: boolean } | null> { const frames = [...staticFrames].sort((a, b) => a - b); if (frames.length === 0) return null; + const deadline = Date.now() + STATIC_VERIFY_MAX_MS; // Runs are maximal-contiguous (adjacent frames merge), so a run's anchor a-1 is // guaranteed NOT static — always a freshly-captured frame. const runs: Array<{ a: number; b: number }> = []; @@ -2460,9 +2468,11 @@ export async function verifyStaticFramesSafe( for (const { a, b } of runs) { const anchor = a - 1; if (anchor < 0) continue; + if (Date.now() >= deadline) return { badFrame: a, budgetExhausted: true }; const anchorBuf = await seekCapture(anchor); spent++; for (const f of computeStaticVerificationPoints(a, b, sampleCount)) { + if (Date.now() >= deadline) return { badFrame: f, budgetExhausted: true }; const cur = await seekCapture(f); spent++; if (!anchorBuf.equals(cur)) return { badFrame: f, budgetExhausted: false };