mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(engine,producer,cli): drawElement release telemetry on render_complete (#2002)
Default-on drawElement ships with a runtime self-verification net (#1998); this makes its in-the-wild behavior observable. Every render_complete event now answers: which capture mode actually ran, why drawElement disengaged when it did (compile gate / producer clamp / engine init gate), whether the self-verify net fired and why, and how much margin verification had. Follows the static-dedup telemetry pattern: engine session fields → CapturePerfSummary → RenderPerfSummary.drawElement → snake_case props on render_complete. New event props: de_capture_mode, de_compile_gate, de_clamp_reason, de_gate_reason, de_worker_encode, de_verify_armed, de_verify_checked, de_verify_min_db (margin above the 32dB threshold — drift here is the early-warning signal before fallbacks spike), de_verify_init_ms, de_self_verify_fallback, de_fallback_reason, de_blank_suspects, de_blank_deterministic_accepts, de_blank_recaptures, de_boundary_frames, de_ncpr_fallbacks. Validated end-to-end on live renders: drawelement path reports mode/verify counters/minDb/init cost; a blur-gated comp reports mode=screenshot + gate_reason=css_effect:filter; a forced verification failure reports self_verify_fallback=true + fallback_reason=psnr. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
906c8d04f8
commit
1005703441
@@ -154,6 +154,12 @@ export interface CaptureSession {
|
||||
* DrawElementVerificationError and the orchestrator re-renders via the
|
||||
* screenshot path. */
|
||||
deVerifyFrames?: Map<number, Buffer>;
|
||||
/** Low-cardinality init-gate reason when drawElement routed to baseline (telemetry). */
|
||||
deGateReason?: string;
|
||||
/** Wall-clock ms spent capturing self-verification ground truth at init (telemetry). */
|
||||
deVerifyInitMs?: number;
|
||||
/** Count of per-frame "No cached paint record" screenshot fallbacks (telemetry). */
|
||||
deNcprFallbacks?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,12 +476,14 @@ async function initDrawElementOrTransparentBackground(
|
||||
!supersampling &&
|
||||
(!forceScreenshot || forceDE);
|
||||
if ((session.config?.useDrawElement ?? false) && supersampling) {
|
||||
session.deGateReason = "supersampling";
|
||||
console.log(
|
||||
"[engine] --experimental-fast-capture disabled for this render: drawElementImage " +
|
||||
"ignores deviceScaleFactor, so supersampled (DPR > 1) output uses screenshot capture.",
|
||||
);
|
||||
}
|
||||
if ((session.config?.useDrawElement ?? false) && !supersampling && forceScreenshot) {
|
||||
session.deGateReason = "render_mode_hint";
|
||||
console.log(
|
||||
"[engine] fast capture: falling back to screenshot — render-mode compatibility " +
|
||||
"hint forced screenshot capture (e.g. raw requestAnimationFrame composition).",
|
||||
@@ -532,6 +540,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
// PSNR=inf; efb59c5b 24.5→47.4 dB, 0 damaged frames). 151 is the pinned floor.
|
||||
const mode = resolveDrawElementCaptureMode(session.isSwiftShader, transparent);
|
||||
if (mode === "screenshot") {
|
||||
session.deGateReason = "swiftshader";
|
||||
// Fall back to the browser's LAUNCH mode, not unconditionally to
|
||||
// "screenshot": on a BeginFrame-launched browser (Linux fast capture)
|
||||
// Page.captureScreenshot hangs for the full protocol timeout, while
|
||||
@@ -551,6 +560,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
if (!forceDE && process.env.HF_FAST_CAPTURE_CSSFX !== "true") {
|
||||
const cssFx = await detectCssEffectRisk(page);
|
||||
if (cssFx) {
|
||||
session.deGateReason = `css_effect:${(cssFx.split(":")[0] ?? "").replace(/[^a-z-]/gi, "")}`;
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`${cssFx} detected (drawElementImage cannot reproduce it; see fast-capture-limitations.md)`,
|
||||
@@ -584,6 +594,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
`timeline at-risk predictor: ${atRisk.size}/${totalFrames} frames (${Math.round(atRiskFraction * 100)}%)`,
|
||||
);
|
||||
if (atRisk.size > 0 && atRiskFraction > fractionFloor) {
|
||||
session.deGateReason = "at_risk_timeline";
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`${atRisk.size}/${totalFrames} frames animate a compositor-incompatible prop ` +
|
||||
@@ -600,6 +611,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
// threeDProjection.ts. No-op for compositions without 3D content.
|
||||
const threeD = await initThreeDProjection(page);
|
||||
if (!forceDE && !threeD.ok) {
|
||||
session.deGateReason = "3d_init_failed";
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`3D projection init failed (${threeD.reason ?? "unknown"})`,
|
||||
@@ -620,7 +632,11 @@ async function initDrawElementOrTransparentBackground(
|
||||
// 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).
|
||||
await captureDeVerificationFrames(session, page, logInitPhase);
|
||||
{
|
||||
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);
|
||||
@@ -2349,6 +2365,7 @@ async function captureFrameCore(
|
||||
// is a per-frame condition, not a whole-comp one — fall back to screenshot
|
||||
// for THIS frame instead of aborting the render. See fast-capture-limitations.md.
|
||||
if (isNoCachedPaintRecordError(err)) {
|
||||
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
||||
console.log(
|
||||
`[engine] fast capture: frame ${frameIndex} — No cached paint record; ` +
|
||||
`screenshot fallback for this frame (see fast-capture-limitations.md)`,
|
||||
@@ -2530,6 +2547,7 @@ export async function captureFrameToBufferPipelined(
|
||||
// The worker isn't involved for this frame; return a resolved encodeResult so
|
||||
// the pipeline loop writes it like any other. See fast-capture-limitations.md.
|
||||
if (isNoCachedPaintRecordError(captureError)) {
|
||||
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
||||
console.log(
|
||||
`[engine] fast capture: frame ${frameIndex} — No cached paint record; ` +
|
||||
`screenshot fallback for this frame (see fast-capture-limitations.md)`,
|
||||
@@ -2934,6 +2952,13 @@ export function getCapturePerfSummary(session: CaptureSession): CapturePerfSumma
|
||||
staticDedupArmed: (session.staticFrames?.size ?? 0) > 0,
|
||||
staticDedupPredicted: session.staticFrames?.size ?? 0,
|
||||
staticDedupSkipReason: session.staticDedupSkipReason,
|
||||
captureMode: session.captureMode,
|
||||
deGateReason: session.deGateReason,
|
||||
deWorkerEncode: session.workerEncodeEnabled ?? false,
|
||||
deVerifyArmed: session.deVerifyFrames?.size ?? 0,
|
||||
deVerifyInitMs: session.deVerifyInitMs ?? 0,
|
||||
deBoundaryFrames: session.clipBoundaryFrames?.size ?? 0,
|
||||
deNcprFallbacks: session.deNcprFallbacks ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,26 @@ export interface CapturePerfSummary {
|
||||
* `|`-join distinct reasons when parallel workers diverge.)
|
||||
*/
|
||||
staticDedupSkipReason?: string;
|
||||
// ── drawElement fast-capture outcome (default-on release visibility) ──
|
||||
/** Final capture mode this session used: "drawelement" | "screenshot" | "beginframe". */
|
||||
captureMode: string;
|
||||
/**
|
||||
* Low-cardinality init-time gate that routed a drawElement-eligible session
|
||||
* to the baseline: `swiftshader` | `css_effect:<fx>` | `at_risk_timeline` |
|
||||
* `3d_init_failed` | `supersampling` | `render_mode_hint`. Undefined when
|
||||
* drawElement ran or was never attempted.
|
||||
*/
|
||||
deGateReason?: string;
|
||||
/** Worker-encode pipeline active (the drain that runs self-verification). */
|
||||
deWorkerEncode: boolean;
|
||||
/** Self-verification ground-truth samples armed at init (0 = verification off/skipped). */
|
||||
deVerifyArmed: number;
|
||||
/** Wall-clock cost of capturing the ground-truth samples at init. */
|
||||
deVerifyInitMs: number;
|
||||
/** Clip-cut boundary frames routed to per-frame screenshot (Lim 6). */
|
||||
deBoundaryFrames: number;
|
||||
/** Per-frame "No cached paint record" screenshot fallbacks during capture. */
|
||||
deNcprFallbacks: number;
|
||||
}
|
||||
|
||||
// ── Global Augmentation ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user