feat(engine,producer,cli): verify video comps via deferred DE init + capture p50 (#2015)

* feat(engine,producer,cli): verify video comps via deferred DE init + capture p50

Closes the two biggest gaps in the first day of v0.7.38 wild data: 88% of
drawElement renders (video comps initialized via probe sessions) ran with
self-verification unarmed, and speedup was measurable on only 3 of 76 renders.

- Deferred drawElement init: probe sessions initialize before video
  extraction, so they have no frame injector — ground-truth screenshots
  would capture black <video> boxes, and verification skipped the whole
  comp. DE init now stops after the gates for injector-less video comps
  (deInitDeferred; autoAlpha flag retracted in case no path completes it)
  and completeDeferredDrawElementInit finishes verification + canvas
  injection + worker-encode at capture time, once
  prepareCaptureSessionForReuse has attached the injector. Validated
  end-to-end: a probe-path video comp now arms 4 ground-truth frames with
  real video pixels (3x inf + 64.7dB) and renders drawElement verified.
- capture_p50_ms: per-frame capture durations are sampled
  (capturePerf.frameMs; batch frames get the batch mean) and the median
  ships as CapturePerfSummary.p50TotalMs -> RenderPerfSummary.captureP50Ms
  -> render_complete capture_p50_ms. Unlike capture_avg_ms it is immune
  to first-frame warmup and stage-setup amortization — smoke: avg 15ms vs
  p50 8ms on the same render, p50 matching the measured steady-state
  floor. Dashboard speedup tiles can drop their frame-count floor once
  this ships.
- video_count on render_complete: segments speedup by video-injection
  comps (whose per-frame gain is legitimately lower) vs pure-graphics.

Canary suite 7/7; engine suite 905 passed (1 pre-existing upstream
failure); tsc/oxlint/oxfmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(producer): complete deferred drawElement init on the disk capture path

Review (miga): a probe-initialized video comp falling back to the disk path
kept deInitDeferred and silently stayed in screenshot mode — a regression
for PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true renders that previously ran
drawElement there. Complete the deferred init on the sequential disk path
under the same explicit-opt-in test the orchestrator clamp uses; default-on
renders stay on the screenshot baseline (this path has no drain-time
self-verification, per the #1998 confinement rule).

Validated: video comp + PRODUCER_ENABLE_STREAMING_ENCODE=false + explicit
opt-in logs "(deferred drawElement init)" completion on capture_disk and
renders correct video pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-06 22:47:29 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 47276edcc6
commit b26c27576b
9 changed files with 166 additions and 36 deletions
+1
View File
@@ -87,6 +87,7 @@ export {
DrawElementVerificationError,
isDrawElementVerificationError,
recaptureDrawElementFrameForVerify,
completeDeferredDrawElementInit,
writeCapturedFrame,
discardWarmupCapture,
getCompositionDuration,
+124 -36
View File
@@ -104,6 +104,9 @@ export interface CaptureSession {
beforeCaptureMs: number;
screenshotMs: number;
totalMs: number;
/** Per-frame capture durations (batch frames get the batch mean). Basis for
* the warmup-robust p50 in the perf summary. */
frameMs: number[];
};
captureMode: CaptureMode;
/**
@@ -160,6 +163,15 @@ export interface CaptureSession {
deVerifyInitMs?: number;
/** Count of per-frame "No cached paint record" screenshot fallbacks (telemetry). */
deNcprFallbacks?: number;
/**
* drawElement init passed every gate but stopped before verification +
* canvas injection: the session has no video-frame injector yet (probe
* sessions initialize before extraction) and the comp has <video> elements,
* so ground-truth screenshots would capture black video boxes. The capture
* stage completes the init via completeDeferredDrawElementInit once
* prepareCaptureSessionForReuse attaches the injector.
*/
deInitDeferred?: boolean;
}
/**
@@ -629,48 +641,104 @@ async function initDrawElementOrTransparentBackground(
// passed) and the DOM is still normal (canvas not yet injected, so the
// verification screenshots are valid). drawElement-path only; see armStaticDedup.
await armStaticDedup(session, page, logInitPhase);
// Self-verification ground truth: must ALSO run pre-injection — after the
// canvas wraps the root, a page screenshot shows the canvas's last-drawn
// bitmap, not the live DOM (see the Lim 6 boundary-screenshot note).
{
const verifyStart = Date.now();
await captureDeVerificationFrames(session, page, logInitPhase);
session.deVerifyInitMs = Date.now() - verifyStart;
}
await injectDrawElementCanvas(page, session.options.width, session.options.height);
if (transparent) {
await initTransparentBackground(session.page);
}
session.captureMode = "drawelement";
session.drawElementReady = true;
logInitPhase("drawElement canvas injected");
// Lim 6: clip-cut boundary frames — screenshot these instead of drawElement.
if (process.env.HF_FAST_CAPTURE_BOUNDARY_SS !== "false" && !forceDE) {
const fps = fpsToNumber(session.options.fps);
const boundaryFrames = await computeClipBoundaryFrames(page, fps);
if (boundaryFrames.size > 0) {
session.clipBoundaryFrames = boundaryFrames;
logInitPhase(`screenshot fallback: ${boundaryFrames.size} clip-boundary frame(s)`);
// Video comps on injector-less sessions (probe sessions initialize before
// video extraction) DEFER the rest of the drawElement init: ground-truth
// screenshots would capture black <video> boxes, and once the canvas is
// injected they can never be retaken. The capture stage completes the
// init after prepareCaptureSessionForReuse attaches the injector.
// Retract the autoAlpha rewrite flag while deferred — if no path ever
// completes the init (e.g. the disk path takes over), the session
// captures via screenshot, where the armed flag causes measured damage.
if (!session.onBeforeCapture && !forceDE) {
const hasVideos = await page.evaluate(() => document.querySelector("video") !== null);
if (hasVideos) {
session.deInitDeferred = true;
await retractAutoAlphaFlag();
logInitPhase("drawElement init deferred: video comp awaiting frame injector");
return;
}
}
// Worker-encode pipeline: macOS hardware GPU path only (syncToPaintEvent=true,
// beginFrameTimeTicks=0). Skip for BeginFrame (Linux/Docker) and transparent
// (PNG) output — those use the existing synchronous path unchanged.
const workerEncodeEnabled =
(session.config?.enableDrawElementWorkerEncode ?? false) &&
!transparent &&
session.beginFrameTimeTicks === 0;
if (workerEncodeEnabled) {
await initDrawElementWorkerEncode(page);
session.workerEncodeEnabled = true;
logInitPhase("drawElement worker encode initialized");
}
await finalizeDrawElementInit(session, page, logInitPhase, { transparent, forceDE });
}
} else if (session.options.format === "png") {
await initTransparentBackground(session.page);
}
}
/**
* The tail of drawElement init: self-verification ground truth (pre-injection),
* canvas injection, capture-mode flip, clip-boundary predictor, worker-encode.
* Runs inline when the session already has its video-frame injector (or the
* comp has no videos); runs deferred via completeDeferredDrawElementInit for
* probe-initialized video comps.
*/
async function finalizeDrawElementInit(
session: CaptureSession,
page: Page,
logInitPhase: (phase: string) => void,
opts: { transparent: boolean; forceDE: boolean },
): Promise<void> {
const { transparent, forceDE } = opts;
// Self-verification ground truth: must run pre-injection — after the canvas
// wraps the root, a page screenshot shows the canvas's last-drawn bitmap,
// not the live DOM (see the Lim 6 boundary-screenshot note).
{
const verifyStart = Date.now();
await captureDeVerificationFrames(session, page, logInitPhase);
session.deVerifyInitMs = Date.now() - verifyStart;
}
await injectDrawElementCanvas(page, session.options.width, session.options.height);
if (transparent) {
await initTransparentBackground(session.page);
}
session.captureMode = "drawelement";
session.drawElementReady = true;
logInitPhase("drawElement canvas injected");
// Lim 6: clip-cut boundary frames — screenshot these instead of drawElement.
if (process.env.HF_FAST_CAPTURE_BOUNDARY_SS !== "false" && !forceDE) {
const fps = fpsToNumber(session.options.fps);
const boundaryFrames = await computeClipBoundaryFrames(page, fps);
if (boundaryFrames.size > 0) {
session.clipBoundaryFrames = boundaryFrames;
logInitPhase(`screenshot fallback: ${boundaryFrames.size} clip-boundary frame(s)`);
}
}
// Worker-encode pipeline: macOS hardware GPU path only (syncToPaintEvent=true,
// beginFrameTimeTicks=0). Skip for BeginFrame (Linux/Docker) and transparent
// (PNG) output — those use the existing synchronous path unchanged.
const workerEncodeEnabled =
(session.config?.enableDrawElementWorkerEncode ?? false) &&
!transparent &&
session.beginFrameTimeTicks === 0;
if (workerEncodeEnabled) {
await initDrawElementWorkerEncode(page);
session.workerEncodeEnabled = true;
logInitPhase("drawElement worker encode initialized");
}
}
/**
* Complete a deferred drawElement init (see CaptureSession.deInitDeferred).
* Call after prepareCaptureSessionForReuse has attached the video-frame
* injector; no-op when the session is not deferred or still has no injector.
* Re-asserts the autoAlpha rewrite flag retracted at deferral time.
*/
export async function completeDeferredDrawElementInit(session: CaptureSession): Promise<void> {
if (!session.deInitDeferred || !session.onBeforeCapture) return;
const page = session.page;
await page.evaluate(() => {
(window as Window & { __HF_FAST_CAPTURE_AUTOALPHA__?: boolean }).__HF_FAST_CAPTURE_AUTOALPHA__ =
true;
});
const logInitPhase = (phase: string) =>
console.log(`[initSession:${session.captureMode}] ${phase} (deferred drawElement init)`);
await finalizeDrawElementInit(session, page, logInitPhase, {
transparent: session.options.format === "png",
forceDE: process.env.HF_FORCE_DRAWELEMENT === "1",
});
session.deInitDeferred = false;
}
// fallow-ignore-next-line unit-size
export async function createCaptureSession(
serverUrl: string,
@@ -865,6 +933,7 @@ export async function createCaptureSession(
beforeCaptureMs: 0,
screenshotMs: 0,
totalMs: 0,
frameMs: [],
},
captureMode,
launchCaptureMode: captureMode,
@@ -2387,6 +2456,7 @@ async function captureFrameCore(
session.capturePerf.beforeCaptureMs += beforeCaptureMs;
session.capturePerf.screenshotMs += screenshotMs;
session.capturePerf.totalMs += captureTimeMs;
session.capturePerf.frameMs.push(captureTimeMs);
// Retain this freshly-captured buffer so the following static frames can reuse it.
if (session.staticFrames) session.lastFrameBuffer = screenshotBuffer;
@@ -2510,7 +2580,11 @@ export async function captureFrameToBufferPipelined(
session.capturePerf.frames += 1;
session.capturePerf.seekMs += seekMs;
session.capturePerf.beforeCaptureMs += beforeCaptureMs;
session.capturePerf.totalMs += Date.now() - startTime;
{
const boundaryMs = Date.now() - startTime;
session.capturePerf.totalMs += boundaryMs;
session.capturePerf.frameMs.push(boundaryMs);
}
const boundaryResult = Promise.resolve(buffer);
if (session.staticFrames) session.lastEncodeResult = boundaryResult;
return { encodeResult: boundaryResult, captureTimeMs: Date.now() - startTime };
@@ -2536,6 +2610,7 @@ export async function captureFrameToBufferPipelined(
// screenshotMs reflects produce time only (encode is async, not tracked here)
session.capturePerf.screenshotMs += captureTimeMs - seekMs - beforeCaptureMs;
session.capturePerf.totalMs += captureTimeMs;
session.capturePerf.frameMs.push(captureTimeMs);
// Task B: retain this encode result so a following static frame can reuse it.
if (session.staticFrames) session.lastEncodeResult = encodeResult;
@@ -2635,9 +2710,14 @@ export async function captureFramesBatchPipelined(
const okCount = failedAt === null ? frameIndices.length : failedAt;
const elapsed = Date.now() - startTime;
session.capturePerf.frames += okCount;
// Round-trips are fused — attribute the whole batch to produce time.
// Round-trips are fused — attribute the whole batch to produce time; each
// frame gets the batch mean for the per-frame sample series.
session.capturePerf.screenshotMs += elapsed;
session.capturePerf.totalMs += elapsed;
if (okCount > 0) {
const perFrame = elapsed / okCount;
for (let s2 = 0; s2 < okCount; s2++) session.capturePerf.frameMs.push(perFrame);
}
const results: Array<{ frameIndex: number; encodeResult: Promise<Buffer> }> = [];
for (let i = 0; i < okCount; i++) {
@@ -2811,6 +2891,7 @@ export function prepareCaptureSessionForReuse(
beforeCaptureMs: 0,
screenshotMs: 0,
totalMs: 0,
frameMs: [],
};
session.beginFrameHasDamageCount = 0;
session.beginFrameNoDamageCount = 0;
@@ -2938,6 +3019,12 @@ async function captureDeVerificationFrames(
);
}
function medianOf(samples: number[]): number {
if (samples.length === 0) return 0;
const sorted = [...samples].sort((a, b) => a - b);
return Math.round(sorted[Math.floor(sorted.length / 2)] ?? 0);
}
export function getCapturePerfSummary(session: CaptureSession): CapturePerfSummary {
const frames = Math.max(1, session.capturePerf.frames);
return {
@@ -2946,6 +3033,7 @@ export function getCapturePerfSummary(session: CaptureSession): CapturePerfSumma
avgSeekMs: Math.round(session.capturePerf.seekMs / frames),
avgBeforeCaptureMs: Math.round(session.capturePerf.beforeCaptureMs / frames),
avgScreenshotMs: Math.round(session.capturePerf.screenshotMs / frames),
p50TotalMs: medianOf(session.capturePerf.frameMs),
staticDedupReused: session.staticDedupCount ?? 0,
staticDedupEnabled: session.staticDedupEnabled ?? false,
// armed ⟺ a non-empty static set survived verification; predicted === its size.
+6
View File
@@ -177,6 +177,12 @@ export interface CapturePerfSummary {
avgSeekMs: number;
avgBeforeCaptureMs: number;
avgScreenshotMs: number;
/**
* Median per-frame capture time warmup-robust, unlike `avgTotalMs`
* (first frames pay font/image decode + GC that swamps short renders'
* averages). Basis for in-the-wild speedup estimates. 0 when no frames.
*/
p50TotalMs: number;
/**
* Frames served from the static-dedup cache instead of a real seek+screenshot
* (opt-out HF_STATIC_DEDUP=false). 0 when dedup was off or never armed. NOT counted