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`
This commit is contained in:
Miguel Ángel
2026-07-14 22:04:49 -04:00
committed by GitHub
parent da19f9a692
commit d047d28bb4
2 changed files with 41 additions and 0 deletions
@@ -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<number>();
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<typeof verifyStaticFramesSafe>[1],
staticFrames,
fps,
24,
);
nowSpy.mockRestore();
expect(result?.budgetExhausted).toBe(true);
expect(pageScreenshotCapture).toHaveBeenCalledTimes(2);
});
});
@@ -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 };