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
@@ -1662,6 +1662,22 @@ function trackRenderMetrics(
|
|||||||
staticDedupSkipReason: perf?.staticDedup?.skipReason,
|
staticDedupSkipReason: perf?.staticDedup?.skipReason,
|
||||||
staticDedupPredictedFrames: perf?.staticDedup?.predictedFrames,
|
staticDedupPredictedFrames: perf?.staticDedup?.predictedFrames,
|
||||||
staticDedupReusedFrames: perf?.staticDedup?.reusedFrames,
|
staticDedupReusedFrames: perf?.staticDedup?.reusedFrames,
|
||||||
|
deCaptureMode: perf?.drawElement?.mode,
|
||||||
|
deCompileGate: perf?.drawElement?.compileGate,
|
||||||
|
deClampReason: perf?.drawElement?.clampReason,
|
||||||
|
deGateReason: perf?.drawElement?.gateReason,
|
||||||
|
deWorkerEncode: perf?.drawElement?.workerEncode,
|
||||||
|
deVerifyArmed: perf?.drawElement?.verifyArmed,
|
||||||
|
deVerifyChecked: perf?.drawElement?.verifyChecked,
|
||||||
|
deVerifyMinDb: perf?.drawElement?.verifyMinDb,
|
||||||
|
deVerifyInitMs: perf?.drawElement?.verifyInitMs,
|
||||||
|
deSelfVerifyFallback: perf?.drawElement?.selfVerifyFallback,
|
||||||
|
deFallbackReason: perf?.drawElement?.fallbackReason,
|
||||||
|
deBlankSuspects: perf?.drawElement?.blankSuspects,
|
||||||
|
deBlankDeterministicAccepts: perf?.drawElement?.blankDeterministicAccepts,
|
||||||
|
deBlankRecaptures: perf?.drawElement?.blankRecaptures,
|
||||||
|
deBoundaryFrames: perf?.drawElement?.boundaryFrames,
|
||||||
|
deNcprFallbacks: perf?.drawElement?.ncprFallbacks,
|
||||||
compositionDurationMs,
|
compositionDurationMs,
|
||||||
compositionWidth: perf?.resolution.width,
|
compositionWidth: perf?.resolution.width,
|
||||||
compositionHeight: perf?.resolution.height,
|
compositionHeight: perf?.resolution.height,
|
||||||
|
|||||||
@@ -114,6 +114,24 @@ export function trackRenderComplete(
|
|||||||
staticDedupSkipReason?: string;
|
staticDedupSkipReason?: string;
|
||||||
staticDedupPredictedFrames?: number;
|
staticDedupPredictedFrames?: number;
|
||||||
staticDedupReusedFrames?: number;
|
staticDedupReusedFrames?: number;
|
||||||
|
// drawElement fast-capture outcome (default-on release visibility).
|
||||||
|
// Undefined on render paths with no capture session.
|
||||||
|
deCaptureMode?: string;
|
||||||
|
deCompileGate?: string;
|
||||||
|
deClampReason?: string;
|
||||||
|
deGateReason?: string;
|
||||||
|
deWorkerEncode?: boolean;
|
||||||
|
deVerifyArmed?: number;
|
||||||
|
deVerifyChecked?: number;
|
||||||
|
deVerifyMinDb?: number;
|
||||||
|
deVerifyInitMs?: number;
|
||||||
|
deSelfVerifyFallback?: boolean;
|
||||||
|
deFallbackReason?: string;
|
||||||
|
deBlankSuspects?: number;
|
||||||
|
deBlankDeterministicAccepts?: number;
|
||||||
|
deBlankRecaptures?: number;
|
||||||
|
deBoundaryFrames?: number;
|
||||||
|
deNcprFallbacks?: number;
|
||||||
// "cli" when triggered by `hyperframes render` (default), "studio" when
|
// "cli" when triggered by `hyperframes render` (default), "studio" when
|
||||||
// triggered by a studio preview-server render (POST /api/projects/:id/render).
|
// triggered by a studio preview-server render (POST /api/projects/:id/render).
|
||||||
source?: "cli" | "studio";
|
source?: "cli" | "studio";
|
||||||
@@ -170,6 +188,22 @@ export function trackRenderComplete(
|
|||||||
static_dedup_skip_reason: props.staticDedupSkipReason,
|
static_dedup_skip_reason: props.staticDedupSkipReason,
|
||||||
static_dedup_predicted_frames: props.staticDedupPredictedFrames,
|
static_dedup_predicted_frames: props.staticDedupPredictedFrames,
|
||||||
static_dedup_reused_frames: props.staticDedupReusedFrames,
|
static_dedup_reused_frames: props.staticDedupReusedFrames,
|
||||||
|
de_capture_mode: props.deCaptureMode,
|
||||||
|
de_compile_gate: props.deCompileGate,
|
||||||
|
de_clamp_reason: props.deClampReason,
|
||||||
|
de_gate_reason: props.deGateReason,
|
||||||
|
de_worker_encode: props.deWorkerEncode,
|
||||||
|
de_verify_armed: props.deVerifyArmed,
|
||||||
|
de_verify_checked: props.deVerifyChecked,
|
||||||
|
de_verify_min_db: props.deVerifyMinDb,
|
||||||
|
de_verify_init_ms: props.deVerifyInitMs,
|
||||||
|
de_self_verify_fallback: props.deSelfVerifyFallback,
|
||||||
|
de_fallback_reason: props.deFallbackReason,
|
||||||
|
de_blank_suspects: props.deBlankSuspects,
|
||||||
|
de_blank_deterministic_accepts: props.deBlankDeterministicAccepts,
|
||||||
|
de_blank_recaptures: props.deBlankRecaptures,
|
||||||
|
de_boundary_frames: props.deBoundaryFrames,
|
||||||
|
de_ncpr_fallbacks: props.deNcprFallbacks,
|
||||||
source: props.source ?? "cli",
|
source: props.source ?? "cli",
|
||||||
composition_duration_ms: props.compositionDurationMs,
|
composition_duration_ms: props.compositionDurationMs,
|
||||||
composition_width: props.compositionWidth,
|
composition_width: props.compositionWidth,
|
||||||
|
|||||||
@@ -154,6 +154,12 @@ export interface CaptureSession {
|
|||||||
* DrawElementVerificationError and the orchestrator re-renders via the
|
* DrawElementVerificationError and the orchestrator re-renders via the
|
||||||
* screenshot path. */
|
* screenshot path. */
|
||||||
deVerifyFrames?: Map<number, Buffer>;
|
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 &&
|
!supersampling &&
|
||||||
(!forceScreenshot || forceDE);
|
(!forceScreenshot || forceDE);
|
||||||
if ((session.config?.useDrawElement ?? false) && supersampling) {
|
if ((session.config?.useDrawElement ?? false) && supersampling) {
|
||||||
|
session.deGateReason = "supersampling";
|
||||||
console.log(
|
console.log(
|
||||||
"[engine] --experimental-fast-capture disabled for this render: drawElementImage " +
|
"[engine] --experimental-fast-capture disabled for this render: drawElementImage " +
|
||||||
"ignores deviceScaleFactor, so supersampled (DPR > 1) output uses screenshot capture.",
|
"ignores deviceScaleFactor, so supersampled (DPR > 1) output uses screenshot capture.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ((session.config?.useDrawElement ?? false) && !supersampling && forceScreenshot) {
|
if ((session.config?.useDrawElement ?? false) && !supersampling && forceScreenshot) {
|
||||||
|
session.deGateReason = "render_mode_hint";
|
||||||
console.log(
|
console.log(
|
||||||
"[engine] fast capture: falling back to screenshot — render-mode compatibility " +
|
"[engine] fast capture: falling back to screenshot — render-mode compatibility " +
|
||||||
"hint forced screenshot capture (e.g. raw requestAnimationFrame composition).",
|
"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.
|
// PSNR=inf; efb59c5b 24.5→47.4 dB, 0 damaged frames). 151 is the pinned floor.
|
||||||
const mode = resolveDrawElementCaptureMode(session.isSwiftShader, transparent);
|
const mode = resolveDrawElementCaptureMode(session.isSwiftShader, transparent);
|
||||||
if (mode === "screenshot") {
|
if (mode === "screenshot") {
|
||||||
|
session.deGateReason = "swiftshader";
|
||||||
// Fall back to the browser's LAUNCH mode, not unconditionally to
|
// Fall back to the browser's LAUNCH mode, not unconditionally to
|
||||||
// "screenshot": on a BeginFrame-launched browser (Linux fast capture)
|
// "screenshot": on a BeginFrame-launched browser (Linux fast capture)
|
||||||
// Page.captureScreenshot hangs for the full protocol timeout, while
|
// 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") {
|
if (!forceDE && process.env.HF_FAST_CAPTURE_CSSFX !== "true") {
|
||||||
const cssFx = await detectCssEffectRisk(page);
|
const cssFx = await detectCssEffectRisk(page);
|
||||||
if (cssFx) {
|
if (cssFx) {
|
||||||
|
session.deGateReason = `css_effect:${(cssFx.split(":")[0] ?? "").replace(/[^a-z-]/gi, "")}`;
|
||||||
console.log(
|
console.log(
|
||||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||||
`${cssFx} detected (drawElementImage cannot reproduce it; see fast-capture-limitations.md)`,
|
`${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)}%)`,
|
`timeline at-risk predictor: ${atRisk.size}/${totalFrames} frames (${Math.round(atRiskFraction * 100)}%)`,
|
||||||
);
|
);
|
||||||
if (atRisk.size > 0 && atRiskFraction > fractionFloor) {
|
if (atRisk.size > 0 && atRiskFraction > fractionFloor) {
|
||||||
|
session.deGateReason = "at_risk_timeline";
|
||||||
console.log(
|
console.log(
|
||||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||||
`${atRisk.size}/${totalFrames} frames animate a compositor-incompatible prop ` +
|
`${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.
|
// threeDProjection.ts. No-op for compositions without 3D content.
|
||||||
const threeD = await initThreeDProjection(page);
|
const threeD = await initThreeDProjection(page);
|
||||||
if (!forceDE && !threeD.ok) {
|
if (!forceDE && !threeD.ok) {
|
||||||
|
session.deGateReason = "3d_init_failed";
|
||||||
console.log(
|
console.log(
|
||||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||||
`3D projection init failed (${threeD.reason ?? "unknown"})`,
|
`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
|
// Self-verification ground truth: must ALSO run pre-injection — after the
|
||||||
// canvas wraps the root, a page screenshot shows the canvas's last-drawn
|
// 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).
|
// bitmap, not the live DOM (see the Lim 6 boundary-screenshot note).
|
||||||
|
{
|
||||||
|
const verifyStart = Date.now();
|
||||||
await captureDeVerificationFrames(session, page, logInitPhase);
|
await captureDeVerificationFrames(session, page, logInitPhase);
|
||||||
|
session.deVerifyInitMs = Date.now() - verifyStart;
|
||||||
|
}
|
||||||
await injectDrawElementCanvas(page, session.options.width, session.options.height);
|
await injectDrawElementCanvas(page, session.options.width, session.options.height);
|
||||||
if (transparent) {
|
if (transparent) {
|
||||||
await initTransparentBackground(session.page);
|
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
|
// 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.
|
// for THIS frame instead of aborting the render. See fast-capture-limitations.md.
|
||||||
if (isNoCachedPaintRecordError(err)) {
|
if (isNoCachedPaintRecordError(err)) {
|
||||||
|
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
||||||
console.log(
|
console.log(
|
||||||
`[engine] fast capture: frame ${frameIndex} — No cached paint record; ` +
|
`[engine] fast capture: frame ${frameIndex} — No cached paint record; ` +
|
||||||
`screenshot fallback for this frame (see fast-capture-limitations.md)`,
|
`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 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.
|
// the pipeline loop writes it like any other. See fast-capture-limitations.md.
|
||||||
if (isNoCachedPaintRecordError(captureError)) {
|
if (isNoCachedPaintRecordError(captureError)) {
|
||||||
|
session.deNcprFallbacks = (session.deNcprFallbacks ?? 0) + 1;
|
||||||
console.log(
|
console.log(
|
||||||
`[engine] fast capture: frame ${frameIndex} — No cached paint record; ` +
|
`[engine] fast capture: frame ${frameIndex} — No cached paint record; ` +
|
||||||
`screenshot fallback for this frame (see fast-capture-limitations.md)`,
|
`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,
|
staticDedupArmed: (session.staticFrames?.size ?? 0) > 0,
|
||||||
staticDedupPredicted: session.staticFrames?.size ?? 0,
|
staticDedupPredicted: session.staticFrames?.size ?? 0,
|
||||||
staticDedupSkipReason: session.staticDedupSkipReason,
|
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.)
|
* `|`-join distinct reasons when parallel workers diverge.)
|
||||||
*/
|
*/
|
||||||
staticDedupSkipReason?: string;
|
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 ────────────────────────────────────────────────────────
|
// ── Global Augmentation ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -35,6 +35,58 @@ export function pushWorkerDedupPerfs(
|
|||||||
* same composition); predicted/reused = SUM (each worker dedups its own frame
|
* same composition); predicted/reused = SUM (each worker dedups its own frame
|
||||||
* range); skipReason = the distinct reasons (sorted, `|`-joined) when not armed.
|
* range); skipReason = the distinct reasons (sorted, `|`-joined) when not armed.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Collapse per-session capture perf + producer-side decisions into the
|
||||||
|
* render-level drawElement outcome. mode/gateReason |-join distinct values
|
||||||
|
* across workers (bounded cardinality); counters SUM.
|
||||||
|
*/
|
||||||
|
// Flat field mapping — branches are ?? fallbacks, not logic.
|
||||||
|
// fallow-ignore-next-line complexity
|
||||||
|
function aggregateDrawElement(
|
||||||
|
perfs: CapturePerfSummary[],
|
||||||
|
de: {
|
||||||
|
compileGate?: string;
|
||||||
|
clampReason?: string;
|
||||||
|
selfVerifyFallback: boolean;
|
||||||
|
fallbackReason?: string;
|
||||||
|
drainStats?: {
|
||||||
|
verifyChecked: number;
|
||||||
|
verifyMinDb?: number;
|
||||||
|
blankSuspects: number;
|
||||||
|
blankDeterministicAccepts: number;
|
||||||
|
blankRecaptures: number;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
): RenderPerfSummary["drawElement"] {
|
||||||
|
if (perfs.length === 0) return undefined;
|
||||||
|
const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
|
||||||
|
const gateReasons = [
|
||||||
|
...new Set(perfs.map((p) => p.deGateReason).filter((r): r is string => !!r)),
|
||||||
|
].sort();
|
||||||
|
const drain = de.drainStats;
|
||||||
|
return {
|
||||||
|
mode: modes.join("|") || "unknown",
|
||||||
|
compileGate: de.compileGate,
|
||||||
|
clampReason: de.clampReason,
|
||||||
|
gateReason: gateReasons.length > 0 ? gateReasons.join("|") : undefined,
|
||||||
|
workerEncode: perfs.some((p) => p.deWorkerEncode),
|
||||||
|
verifyArmed: perfs.reduce((sum, p) => sum + (p.deVerifyArmed ?? 0), 0),
|
||||||
|
verifyChecked: drain?.verifyChecked ?? 0,
|
||||||
|
verifyMinDb:
|
||||||
|
drain?.verifyMinDb === undefined
|
||||||
|
? undefined
|
||||||
|
: Math.round(Math.min(drain.verifyMinDb, 999) * 10) / 10,
|
||||||
|
verifyInitMs: perfs.reduce((sum, p) => sum + (p.deVerifyInitMs ?? 0), 0),
|
||||||
|
selfVerifyFallback: de.selfVerifyFallback,
|
||||||
|
fallbackReason: de.fallbackReason,
|
||||||
|
blankSuspects: drain?.blankSuspects ?? 0,
|
||||||
|
blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
|
||||||
|
blankRecaptures: drain?.blankRecaptures ?? 0,
|
||||||
|
boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
|
||||||
|
ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function aggregateDedup(perfs: CapturePerfSummary[]): RenderPerfSummary["staticDedup"] {
|
function aggregateDedup(perfs: CapturePerfSummary[]): RenderPerfSummary["staticDedup"] {
|
||||||
if (perfs.length === 0) return undefined;
|
if (perfs.length === 0) return undefined;
|
||||||
const armed = perfs.some((p) => p.staticDedupArmed);
|
const armed = perfs.some((p) => p.staticDedupArmed);
|
||||||
@@ -83,6 +135,19 @@ export function buildRenderPerfSummary(input: {
|
|||||||
peakHeapUsedBytes: number;
|
peakHeapUsedBytes: number;
|
||||||
/** Per-session/per-worker static-dedup perf; aggregated into `staticDedup`. */
|
/** Per-session/per-worker static-dedup perf; aggregated into `staticDedup`. */
|
||||||
dedupPerfs: CapturePerfSummary[];
|
dedupPerfs: CapturePerfSummary[];
|
||||||
|
drawElement?: {
|
||||||
|
compileGate?: string;
|
||||||
|
clampReason?: string;
|
||||||
|
selfVerifyFallback: boolean;
|
||||||
|
fallbackReason?: string;
|
||||||
|
drainStats?: {
|
||||||
|
verifyChecked: number;
|
||||||
|
verifyMinDb?: number;
|
||||||
|
blankSuspects: number;
|
||||||
|
blankDeterministicAccepts: number;
|
||||||
|
blankRecaptures: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
}): RenderPerfSummary {
|
}): RenderPerfSummary {
|
||||||
return {
|
return {
|
||||||
renderId: input.job.id,
|
renderId: input.job.id,
|
||||||
@@ -130,5 +195,9 @@ export function buildRenderPerfSummary(input: {
|
|||||||
peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
|
peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
|
||||||
peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
|
peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
|
||||||
staticDedup: aggregateDedup(input.dedupPerfs),
|
staticDedup: aggregateDedup(input.dedupPerfs),
|
||||||
|
drawElement: aggregateDrawElement(
|
||||||
|
input.dedupPerfs,
|
||||||
|
input.drawElement ?? { selfVerifyFallback: false },
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,15 @@ export interface CaptureStreamingStageInput {
|
|||||||
dedupPerfs: CapturePerfSummary[];
|
dedupPerfs: CapturePerfSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Drain-side safety-net counters for the worker-encode loop (telemetry). */
|
||||||
|
export interface DeDrainStats {
|
||||||
|
verifyChecked: number;
|
||||||
|
verifyMinDb?: number;
|
||||||
|
blankSuspects: number;
|
||||||
|
blankDeterministicAccepts: number;
|
||||||
|
blankRecaptures: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type CaptureStreamingStageResult =
|
export type CaptureStreamingStageResult =
|
||||||
| {
|
| {
|
||||||
/** Streaming path ran successfully — sequencer should skip the disk path AND Stage 5 encode. */
|
/** Streaming path ran successfully — sequencer should skip the disk path AND Stage 5 encode. */
|
||||||
@@ -139,6 +148,8 @@ export type CaptureStreamingStageResult =
|
|||||||
workerCount: number;
|
workerCount: number;
|
||||||
/** Engine-resolved screenshot flag from the consumed sequential/probe session, when observed. */
|
/** Engine-resolved screenshot flag from the consumed sequential/probe session, when observed. */
|
||||||
captureBeyondViewport?: boolean;
|
captureBeyondViewport?: boolean;
|
||||||
|
/** Safety-net drain counters (worker-encode loop only; undefined elsewhere). */
|
||||||
|
deDrainStats?: DeDrainStats;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
/** Spawn failed (non-abort) — sequencer should fall back to the disk path. */
|
/** Spawn failed (non-abort) — sequencer should fall back to the disk path. */
|
||||||
@@ -176,6 +187,7 @@ async function runWorkerEncodePipelineLoop(
|
|||||||
assertNotAborted: () => void,
|
assertNotAborted: () => void,
|
||||||
onProgress: CaptureStreamingStageInput["onProgress"],
|
onProgress: CaptureStreamingStageInput["onProgress"],
|
||||||
log: CaptureStreamingStageInput["log"],
|
log: CaptureStreamingStageInput["log"],
|
||||||
|
stats: DeDrainStats,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let prev: { idx: number; encodeResult: Promise<Buffer> } | null = null;
|
let prev: { idx: number; encodeResult: Promise<Buffer> } | null = null;
|
||||||
const frameTime = (i: number) => (i * job.config.fps.den) / job.config.fps.num;
|
const frameTime = (i: number) => (i * job.config.fps.den) / job.config.fps.num;
|
||||||
@@ -228,9 +240,13 @@ async function runWorkerEncodePipelineLoop(
|
|||||||
if (process.env.HF_FORCE_DRAWELEMENT !== "1") {
|
if (process.env.HF_FORCE_DRAWELEMENT !== "1") {
|
||||||
const floor = blankFloor();
|
const floor = blankFloor();
|
||||||
if (floor > 0 && buf.length < floor && acceptedSmall?.equals(buf)) {
|
if (floor > 0 && buf.length < floor && acceptedSmall?.equals(buf)) {
|
||||||
|
stats.blankSuspects += 1;
|
||||||
|
stats.blankDeterministicAccepts += 1;
|
||||||
// Identical to a small frame already proven deterministic (dark
|
// Identical to a small frame already proven deterministic (dark
|
||||||
// clip-gap runs repeat the same bytes) — skip the recapture.
|
// clip-gap runs repeat the same bytes) — skip the recapture.
|
||||||
} else if (floor > 0 && buf.length < floor) {
|
} else if (floor > 0 && buf.length < floor) {
|
||||||
|
stats.blankSuspects += 1;
|
||||||
|
stats.blankRecaptures += 1;
|
||||||
log.warn("[Render] drawElement blank-frame suspect; re-capturing", {
|
log.warn("[Render] drawElement blank-frame suspect; re-capturing", {
|
||||||
frame: idx,
|
frame: idx,
|
||||||
bytes: buf.length,
|
bytes: buf.length,
|
||||||
@@ -255,6 +271,7 @@ async function runWorkerEncodePipelineLoop(
|
|||||||
// transient blank drop (those are intermittent by nature) — the
|
// transient blank drop (those are intermittent by nature) — the
|
||||||
// frame is legitimately small (dark / low-detail). Accept it;
|
// frame is legitimately small (dark / low-detail). Accept it;
|
||||||
// deterministic damage classes are the PSNR self-verify's job.
|
// deterministic damage classes are the PSNR self-verify's job.
|
||||||
|
stats.blankDeterministicAccepts += 1;
|
||||||
log.info("[Render] drawElement small frame is deterministic; accepted", {
|
log.info("[Render] drawElement small frame is deterministic; accepted", {
|
||||||
frame: idx,
|
frame: idx,
|
||||||
bytes: buf.length,
|
bytes: buf.length,
|
||||||
@@ -299,6 +316,8 @@ async function runWorkerEncodePipelineLoop(
|
|||||||
`drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
|
`drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
stats.verifyChecked += 1;
|
||||||
|
stats.verifyMinDb = stats.verifyMinDb === undefined ? db : Math.min(stats.verifyMinDb, db);
|
||||||
log.info("[Render] drawElement self-verify passed", {
|
log.info("[Render] drawElement self-verify passed", {
|
||||||
frame: idx,
|
frame: idx,
|
||||||
psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1)),
|
psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1)),
|
||||||
@@ -426,6 +445,7 @@ export async function runCaptureStreamingStage(
|
|||||||
} = input;
|
} = input;
|
||||||
let { workerCount, probeSession } = input;
|
let { workerCount, probeSession } = input;
|
||||||
let lastBrowserConsole: string[] = [];
|
let lastBrowserConsole: string[] = [];
|
||||||
|
let deDrainStats: DeDrainStats | undefined;
|
||||||
let captureBeyondViewport: boolean | undefined = probeSession?.options.captureBeyondViewport;
|
let captureBeyondViewport: boolean | undefined = probeSession?.options.captureBeyondViewport;
|
||||||
|
|
||||||
// Derive a local cfg view rather than reading `forceScreenshot` from the
|
// Derive a local cfg view rather than reading `forceScreenshot` from the
|
||||||
@@ -546,6 +566,12 @@ export async function runCaptureStreamingStage(
|
|||||||
if (session.workerEncodeEnabled) {
|
if (session.workerEncodeEnabled) {
|
||||||
// Worker-encode pipeline: depth-2. Frame N's in-page Worker encodes
|
// Worker-encode pipeline: depth-2. Frame N's in-page Worker encodes
|
||||||
// while frame N+1's main thread does seek+paint+drawElement+kick.
|
// while frame N+1's main thread does seek+paint+drawElement+kick.
|
||||||
|
deDrainStats = {
|
||||||
|
verifyChecked: 0,
|
||||||
|
blankSuspects: 0,
|
||||||
|
blankDeterministicAccepts: 0,
|
||||||
|
blankRecaptures: 0,
|
||||||
|
};
|
||||||
await runWorkerEncodePipelineLoop(
|
await runWorkerEncodePipelineLoop(
|
||||||
session,
|
session,
|
||||||
totalFrames,
|
totalFrames,
|
||||||
@@ -555,6 +581,7 @@ export async function runCaptureStreamingStage(
|
|||||||
assertNotAborted,
|
assertNotAborted,
|
||||||
onProgress,
|
onProgress,
|
||||||
log,
|
log,
|
||||||
|
deDrainStats,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < totalFrames; i++) {
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
@@ -613,6 +640,7 @@ export async function runCaptureStreamingStage(
|
|||||||
probeSession,
|
probeSession,
|
||||||
lastBrowserConsole,
|
lastBrowserConsole,
|
||||||
workerCount,
|
workerCount,
|
||||||
|
deDrainStats,
|
||||||
captureBeyondViewport,
|
captureBeyondViewport,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ export interface CompileStageResult {
|
|||||||
* instead of relying on `cfg.forceScreenshot` mutations.
|
* instead of relying on `cfg.forceScreenshot` mutations.
|
||||||
*/
|
*/
|
||||||
forceScreenshot: boolean;
|
forceScreenshot: boolean;
|
||||||
|
/** Low-cardinality compile-time gate that disabled default drawElement:
|
||||||
|
* `3d` | `mix_blend_mode` | `shader_transitions`. Undefined when none fired. */
|
||||||
|
deCompileGate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runCompileStage(input: CompileStageInput): Promise<CompileStageResult> {
|
export async function runCompileStage(input: CompileStageInput): Promise<CompileStageResult> {
|
||||||
@@ -162,12 +165,14 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
|||||||
// Detection runs inside the compiler on PRE-CDN-inline HTML — GSAP's own
|
// Detection runs inside the compiler on PRE-CDN-inline HTML — GSAP's own
|
||||||
// source contains `transformPerspective`, so scanning compiled.html here
|
// source contains `transformPerspective`, so scanning compiled.html here
|
||||||
// would flag every composition that loads GSAP.
|
// would flag every composition that loads GSAP.
|
||||||
|
let deCompileGate: string | undefined;
|
||||||
if (
|
if (
|
||||||
cfg.useDrawElement &&
|
cfg.useDrawElement &&
|
||||||
process.env.HF_FAST_CAPTURE_3D !== "true" &&
|
process.env.HF_FAST_CAPTURE_3D !== "true" &&
|
||||||
compiled.usesThreeDTransforms
|
compiled.usesThreeDTransforms
|
||||||
) {
|
) {
|
||||||
cfg.useDrawElement = false;
|
cfg.useDrawElement = false;
|
||||||
|
deCompileGate = "3d";
|
||||||
log.info(
|
log.info(
|
||||||
"[Render] Fast capture: composition uses a CSS 3D rendering context " +
|
"[Render] Fast capture: composition uses a CSS 3D rendering context " +
|
||||||
"(perspective / preserve-3d / backface-visibility) — disabling drawElementImage " +
|
"(perspective / preserve-3d / backface-visibility) — disabling drawElementImage " +
|
||||||
@@ -186,6 +191,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
|||||||
compiled.usesMixBlendMode
|
compiled.usesMixBlendMode
|
||||||
) {
|
) {
|
||||||
cfg.useDrawElement = false;
|
cfg.useDrawElement = false;
|
||||||
|
deCompileGate = "mix_blend_mode";
|
||||||
log.info(
|
log.info(
|
||||||
"[Render] Fast capture: composition uses mix-blend-mode — disabling drawElementImage " +
|
"[Render] Fast capture: composition uses mix-blend-mode — disabling drawElementImage " +
|
||||||
"for this render. Capture uses the platform's baseline route.",
|
"for this render. Capture uses the platform's baseline route.",
|
||||||
@@ -202,6 +208,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
|||||||
compiled.hasShaderTransitions
|
compiled.hasShaderTransitions
|
||||||
) {
|
) {
|
||||||
cfg.useDrawElement = false;
|
cfg.useDrawElement = false;
|
||||||
|
deCompileGate = "shader_transitions";
|
||||||
log.info(
|
log.info(
|
||||||
"[Render] Fast capture: composition uses shader transitions — disabling drawElementImage " +
|
"[Render] Fast capture: composition uses shader transitions — disabling drawElementImage " +
|
||||||
"so page-side compositing stays available.",
|
"so page-side compositing stays available.",
|
||||||
@@ -269,5 +276,6 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
|||||||
outputHeight,
|
outputHeight,
|
||||||
compileOnlyMs,
|
compileOnlyMs,
|
||||||
forceScreenshot,
|
forceScreenshot,
|
||||||
|
deCompileGate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,6 +352,42 @@ export interface RenderPerfSummary {
|
|||||||
reusedFrames: number;
|
reusedFrames: number;
|
||||||
skipReason?: string;
|
skipReason?: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* drawElement fast-capture outcome for this render (default-on release
|
||||||
|
* visibility). Undefined when no capture session ran.
|
||||||
|
*/
|
||||||
|
drawElement?: {
|
||||||
|
/** Final capture mode: "drawelement" | "screenshot" | "beginframe" (|-joined if workers diverge). */
|
||||||
|
mode: string;
|
||||||
|
/** Compile-time gate that disabled default DE: 3d | mix_blend_mode | shader_transitions. */
|
||||||
|
compileGate?: string;
|
||||||
|
/** Producer clamp that disabled default DE: parallel | disk_path. */
|
||||||
|
clampReason?: string;
|
||||||
|
/** Engine init-time gate: swiftshader | css_effect:* | at_risk_timeline | 3d_init_failed | supersampling | render_mode_hint. */
|
||||||
|
gateReason?: string;
|
||||||
|
/** Worker-encode drain (the verified path) was active. */
|
||||||
|
workerEncode: boolean;
|
||||||
|
/** Self-verification ground-truth samples armed at init. */
|
||||||
|
verifyArmed: number;
|
||||||
|
/** Samples actually compared at drain time. */
|
||||||
|
verifyChecked: number;
|
||||||
|
/** Minimum PSNR across checked samples (dB; margin above the 32dB threshold). */
|
||||||
|
verifyMinDb?: number;
|
||||||
|
/** Init cost of capturing ground truth (ms). */
|
||||||
|
verifyInitMs: number;
|
||||||
|
/** Self-verification tripped and the render re-ran via screenshot. */
|
||||||
|
selfVerifyFallback: boolean;
|
||||||
|
/** What tripped it: psnr | blank. */
|
||||||
|
fallbackReason?: string;
|
||||||
|
/** Blank-guard counters. */
|
||||||
|
blankSuspects: number;
|
||||||
|
blankDeterministicAccepts: number;
|
||||||
|
blankRecaptures: number;
|
||||||
|
/** Clip-cut boundary frames captured via per-frame screenshot. */
|
||||||
|
boundaryFrames: number;
|
||||||
|
/** Per-frame "No cached paint record" screenshot fallbacks. */
|
||||||
|
ncprFallbacks: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HdrDiagnostics {
|
export interface HdrDiagnostics {
|
||||||
@@ -1167,6 +1203,13 @@ export async function executeRenderJob(
|
|||||||
// via the explicit `forceScreenshot` parameter rather than reading
|
// via the explicit `forceScreenshot` parameter rather than reading
|
||||||
// `cfg.forceScreenshot` directly.
|
// `cfg.forceScreenshot` directly.
|
||||||
let captureForceScreenshot = compileResult.forceScreenshot;
|
let captureForceScreenshot = compileResult.forceScreenshot;
|
||||||
|
// drawElement release telemetry: why default DE disengaged (if it did),
|
||||||
|
// whether self-verify fell back, and the drain-side counters.
|
||||||
|
const deCompileGate = compileResult.deCompileGate;
|
||||||
|
let deClampReason: string | undefined;
|
||||||
|
let deSelfVerifyFallback = false;
|
||||||
|
let deFallbackReason: string | undefined;
|
||||||
|
let deDrainStats: import("./render/stages/captureStreamingStage.js").DeDrainStats | undefined;
|
||||||
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
||||||
observability.checkpoint("compile", "composition metadata resolved", {
|
observability.checkpoint("compile", "composition metadata resolved", {
|
||||||
width,
|
width,
|
||||||
@@ -1584,6 +1627,7 @@ export async function executeRenderJob(
|
|||||||
(!useStreamingEncode || workerCount > 1)
|
(!useStreamingEncode || workerCount > 1)
|
||||||
) {
|
) {
|
||||||
cfg.useDrawElement = false;
|
cfg.useDrawElement = false;
|
||||||
|
deClampReason = workerCount > 1 ? "parallel" : "disk_path";
|
||||||
log.info(
|
log.info(
|
||||||
"[Render] Fast capture: default-on drawElement disabled for this render — " +
|
"[Render] Fast capture: default-on drawElement disabled for this render — " +
|
||||||
(workerCount > 1 ? "parallel capture" : "the disk capture path") +
|
(workerCount > 1 ? "parallel capture" : "the disk capture path") +
|
||||||
@@ -1837,6 +1881,10 @@ export async function executeRenderJob(
|
|||||||
// session was closed by the stage's finally; probeSession (if any)
|
// session was closed by the stage's finally; probeSession (if any)
|
||||||
// was consumed by it, so a fresh session spawns on retry.
|
// was consumed by it, so a fresh session spawns on retry.
|
||||||
if (!isDrawElementVerificationError(err)) throw err;
|
if (!isDrawElementVerificationError(err)) throw err;
|
||||||
|
deSelfVerifyFallback = true;
|
||||||
|
deFallbackReason = /blank/i.test(err instanceof Error ? err.message : "")
|
||||||
|
? "blank"
|
||||||
|
: "psnr";
|
||||||
log.warn("[Render] drawElement self-verification failed; re-rendering via screenshot", {
|
log.warn("[Render] drawElement self-verification failed; re-rendering via screenshot", {
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
@@ -1858,6 +1906,7 @@ export async function executeRenderJob(
|
|||||||
const captureFrameMs = Date.now() - captureFrameStart;
|
const captureFrameMs = Date.now() - captureFrameStart;
|
||||||
if (streamingRes.success) {
|
if (streamingRes.success) {
|
||||||
streamingHandled = true;
|
streamingHandled = true;
|
||||||
|
deDrainStats = streamingRes.deDrainStats;
|
||||||
workerCount = streamingRes.workerCount;
|
workerCount = streamingRes.workerCount;
|
||||||
updateCaptureObservability({ workerCount });
|
updateCaptureObservability({ workerCount });
|
||||||
if (streamingRes.captureBeyondViewport !== undefined) {
|
if (streamingRes.captureBeyondViewport !== undefined) {
|
||||||
@@ -2026,6 +2075,13 @@ export async function executeRenderJob(
|
|||||||
captureCalibration,
|
captureCalibration,
|
||||||
captureAttempts,
|
captureAttempts,
|
||||||
dedupPerfs,
|
dedupPerfs,
|
||||||
|
drawElement: {
|
||||||
|
compileGate: deCompileGate,
|
||||||
|
clampReason: deClampReason,
|
||||||
|
selfVerifyFallback: deSelfVerifyFallback,
|
||||||
|
fallbackReason: deFallbackReason,
|
||||||
|
drainStats: deDrainStats,
|
||||||
|
},
|
||||||
hdrDiagnostics,
|
hdrDiagnostics,
|
||||||
hdrPerf,
|
hdrPerf,
|
||||||
observability: observabilitySummary,
|
observability: observabilitySummary,
|
||||||
|
|||||||
Reference in New Issue
Block a user