mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
perf(engine,producer): batch N drawElement frames per CDP round-trip (HF_DE_BATCH) (#1928)
Amortizes per-frame CDP protocol overhead (~3.5-9ms/frame) by looping seek -> paint-wait -> drawElementImage -> createImageBitmap in ONE page.evaluate for runs of consecutive frames; bitmaps still post to the encode worker per frame. Validated on 19 stratified DE comps: median 1.20x on top of worker-encode (to 1.56x), zero damaged frames, edge comps (static-dedup-heavy, clip-cut) bit-identical; mid-batch failure re-captures via the per-frame path (screenshot-fallback semantics preserved). Off by default; opt in with HF_DE_BATCH=4. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
992a9b6607
commit
d5ecb013d7
@@ -39,6 +39,7 @@ import {
|
||||
initDrawElementWorkerEncode,
|
||||
cleanupDrawElementWorkerEncode,
|
||||
produceDrawElementFrame,
|
||||
produceDrawElementFrameBatch,
|
||||
} from "./drawElementService.js";
|
||||
import { initThreeDProjection, detectCssEffectRisk } from "./threeDProjection.js";
|
||||
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
|
||||
@@ -2511,6 +2512,73 @@ export async function captureFrameToBufferPipelined(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* P6 prototype (HF_DE_BATCH): capture N consecutive frames in one CDP
|
||||
* round-trip via {@link produceDrawElementFrameBatch}. The caller pre-plans the
|
||||
* batch (consecutive frame indices, none static-dedup'd, none opt-in
|
||||
* boundary-screenshot). On a mid-batch in-page failure the remaining frames are
|
||||
* re-captured through {@link captureFrameToBufferPipelined}, which owns the
|
||||
* per-frame screenshot-fallback semantics — so failure behavior is identical to
|
||||
* the unbatched path, just discovered at batch granularity.
|
||||
*/
|
||||
export async function captureFramesBatchPipelined(
|
||||
session: CaptureSession,
|
||||
frameIndices: number[],
|
||||
times: number[],
|
||||
): Promise<Array<{ frameIndex: number; encodeResult: Promise<Buffer> }>> {
|
||||
const { page, options } = session;
|
||||
if (!session.isInitialized) {
|
||||
throw new Error("[FrameCapture] Session not initialized");
|
||||
}
|
||||
const startTime = Date.now();
|
||||
const fps = fpsToNumber(options.fps);
|
||||
const quantized = times.map((t) => quantizeTimeToFrame(t, fps));
|
||||
|
||||
const { encodeResults, failedAt, error } = await produceDrawElementFrameBatch(
|
||||
page,
|
||||
quantized,
|
||||
options.width,
|
||||
options.height,
|
||||
options.quality ?? 80,
|
||||
);
|
||||
|
||||
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.
|
||||
session.capturePerf.screenshotMs += elapsed;
|
||||
session.capturePerf.totalMs += elapsed;
|
||||
|
||||
const results: Array<{ frameIndex: number; encodeResult: Promise<Buffer> }> = [];
|
||||
for (let i = 0; i < okCount; i++) {
|
||||
const frameIndex = frameIndices[i];
|
||||
const encodeResult = encodeResults[i];
|
||||
if (frameIndex === undefined || !encodeResult) break;
|
||||
results.push({ frameIndex, encodeResult });
|
||||
}
|
||||
|
||||
if (failedAt !== null) {
|
||||
console.log(
|
||||
`[engine] fast capture: batch produce failed at frame ` +
|
||||
`${frameIndices[failedAt] ?? "?"} (${error ?? "?"}); ` +
|
||||
`re-capturing ${frameIndices.length - failedAt} frame(s) per-frame`,
|
||||
);
|
||||
for (let i = failedAt; i < frameIndices.length; i++) {
|
||||
const frameIndex = frameIndices[i];
|
||||
const time = times[i];
|
||||
if (frameIndex === undefined || time === undefined) break;
|
||||
const { encodeResult } = await captureFrameToBufferPipelined(session, frameIndex, time);
|
||||
results.push({ frameIndex, encodeResult });
|
||||
}
|
||||
}
|
||||
|
||||
// Task B: retain the last encode result so a following static frame can reuse it.
|
||||
const last = results[results.length - 1];
|
||||
if (session.staticFrames && last) session.lastEncodeResult = last.encodeResult;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the "inner capture" function consumed by
|
||||
* {@link discardWarmupCapture}. Matches the real `captureFrameCore` signature
|
||||
|
||||
Reference in New Issue
Block a user