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,
|
||||
staticDedupPredictedFrames: perf?.staticDedup?.predictedFrames,
|
||||
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,
|
||||
compositionWidth: perf?.resolution.width,
|
||||
compositionHeight: perf?.resolution.height,
|
||||
|
||||
@@ -114,6 +114,24 @@ export function trackRenderComplete(
|
||||
staticDedupSkipReason?: string;
|
||||
staticDedupPredictedFrames?: 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
|
||||
// triggered by a studio preview-server render (POST /api/projects/:id/render).
|
||||
source?: "cli" | "studio";
|
||||
@@ -170,6 +188,22 @@ export function trackRenderComplete(
|
||||
static_dedup_skip_reason: props.staticDedupSkipReason,
|
||||
static_dedup_predicted_frames: props.staticDedupPredictedFrames,
|
||||
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",
|
||||
composition_duration_ms: props.compositionDurationMs,
|
||||
composition_width: props.compositionWidth,
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -35,6 +35,58 @@ export function pushWorkerDedupPerfs(
|
||||
* same composition); predicted/reused = SUM (each worker dedups its own frame
|
||||
* 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"] {
|
||||
if (perfs.length === 0) return undefined;
|
||||
const armed = perfs.some((p) => p.staticDedupArmed);
|
||||
@@ -83,6 +135,19 @@ export function buildRenderPerfSummary(input: {
|
||||
peakHeapUsedBytes: number;
|
||||
/** Per-session/per-worker static-dedup perf; aggregated into `staticDedup`. */
|
||||
dedupPerfs: CapturePerfSummary[];
|
||||
drawElement?: {
|
||||
compileGate?: string;
|
||||
clampReason?: string;
|
||||
selfVerifyFallback: boolean;
|
||||
fallbackReason?: string;
|
||||
drainStats?: {
|
||||
verifyChecked: number;
|
||||
verifyMinDb?: number;
|
||||
blankSuspects: number;
|
||||
blankDeterministicAccepts: number;
|
||||
blankRecaptures: number;
|
||||
};
|
||||
};
|
||||
}): RenderPerfSummary {
|
||||
return {
|
||||
renderId: input.job.id,
|
||||
@@ -130,5 +195,9 @@ export function buildRenderPerfSummary(input: {
|
||||
peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
|
||||
peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
|
||||
staticDedup: aggregateDedup(input.dedupPerfs),
|
||||
drawElement: aggregateDrawElement(
|
||||
input.dedupPerfs,
|
||||
input.drawElement ?? { selfVerifyFallback: false },
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,6 +128,15 @@ export interface CaptureStreamingStageInput {
|
||||
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 =
|
||||
| {
|
||||
/** Streaming path ran successfully — sequencer should skip the disk path AND Stage 5 encode. */
|
||||
@@ -139,6 +148,8 @@ export type CaptureStreamingStageResult =
|
||||
workerCount: number;
|
||||
/** Engine-resolved screenshot flag from the consumed sequential/probe session, when observed. */
|
||||
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. */
|
||||
@@ -176,6 +187,7 @@ async function runWorkerEncodePipelineLoop(
|
||||
assertNotAborted: () => void,
|
||||
onProgress: CaptureStreamingStageInput["onProgress"],
|
||||
log: CaptureStreamingStageInput["log"],
|
||||
stats: DeDrainStats,
|
||||
): Promise<void> {
|
||||
let prev: { idx: number; encodeResult: Promise<Buffer> } | null = null;
|
||||
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") {
|
||||
const floor = blankFloor();
|
||||
if (floor > 0 && buf.length < floor && acceptedSmall?.equals(buf)) {
|
||||
stats.blankSuspects += 1;
|
||||
stats.blankDeterministicAccepts += 1;
|
||||
// Identical to a small frame already proven deterministic (dark
|
||||
// clip-gap runs repeat the same bytes) — skip the recapture.
|
||||
} else if (floor > 0 && buf.length < floor) {
|
||||
stats.blankSuspects += 1;
|
||||
stats.blankRecaptures += 1;
|
||||
log.warn("[Render] drawElement blank-frame suspect; re-capturing", {
|
||||
frame: idx,
|
||||
bytes: buf.length,
|
||||
@@ -255,6 +271,7 @@ async function runWorkerEncodePipelineLoop(
|
||||
// transient blank drop (those are intermittent by nature) — the
|
||||
// frame is legitimately small (dark / low-detail). Accept it;
|
||||
// deterministic damage classes are the PSNR self-verify's job.
|
||||
stats.blankDeterministicAccepts += 1;
|
||||
log.info("[Render] drawElement small frame is deterministic; accepted", {
|
||||
frame: idx,
|
||||
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})` : ""}`,
|
||||
);
|
||||
}
|
||||
stats.verifyChecked += 1;
|
||||
stats.verifyMinDb = stats.verifyMinDb === undefined ? db : Math.min(stats.verifyMinDb, db);
|
||||
log.info("[Render] drawElement self-verify passed", {
|
||||
frame: idx,
|
||||
psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1)),
|
||||
@@ -426,6 +445,7 @@ export async function runCaptureStreamingStage(
|
||||
} = input;
|
||||
let { workerCount, probeSession } = input;
|
||||
let lastBrowserConsole: string[] = [];
|
||||
let deDrainStats: DeDrainStats | undefined;
|
||||
let captureBeyondViewport: boolean | undefined = probeSession?.options.captureBeyondViewport;
|
||||
|
||||
// Derive a local cfg view rather than reading `forceScreenshot` from the
|
||||
@@ -546,6 +566,12 @@ export async function runCaptureStreamingStage(
|
||||
if (session.workerEncodeEnabled) {
|
||||
// Worker-encode pipeline: depth-2. Frame N's in-page Worker encodes
|
||||
// while frame N+1's main thread does seek+paint+drawElement+kick.
|
||||
deDrainStats = {
|
||||
verifyChecked: 0,
|
||||
blankSuspects: 0,
|
||||
blankDeterministicAccepts: 0,
|
||||
blankRecaptures: 0,
|
||||
};
|
||||
await runWorkerEncodePipelineLoop(
|
||||
session,
|
||||
totalFrames,
|
||||
@@ -555,6 +581,7 @@ export async function runCaptureStreamingStage(
|
||||
assertNotAborted,
|
||||
onProgress,
|
||||
log,
|
||||
deDrainStats,
|
||||
);
|
||||
} else {
|
||||
for (let i = 0; i < totalFrames; i++) {
|
||||
@@ -613,6 +640,7 @@ export async function runCaptureStreamingStage(
|
||||
probeSession,
|
||||
lastBrowserConsole,
|
||||
workerCount,
|
||||
deDrainStats,
|
||||
captureBeyondViewport,
|
||||
};
|
||||
} finally {
|
||||
|
||||
@@ -102,6 +102,9 @@ export interface CompileStageResult {
|
||||
* instead of relying on `cfg.forceScreenshot` mutations.
|
||||
*/
|
||||
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> {
|
||||
@@ -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
|
||||
// source contains `transformPerspective`, so scanning compiled.html here
|
||||
// would flag every composition that loads GSAP.
|
||||
let deCompileGate: string | undefined;
|
||||
if (
|
||||
cfg.useDrawElement &&
|
||||
process.env.HF_FAST_CAPTURE_3D !== "true" &&
|
||||
compiled.usesThreeDTransforms
|
||||
) {
|
||||
cfg.useDrawElement = false;
|
||||
deCompileGate = "3d";
|
||||
log.info(
|
||||
"[Render] Fast capture: composition uses a CSS 3D rendering context " +
|
||||
"(perspective / preserve-3d / backface-visibility) — disabling drawElementImage " +
|
||||
@@ -186,6 +191,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
compiled.usesMixBlendMode
|
||||
) {
|
||||
cfg.useDrawElement = false;
|
||||
deCompileGate = "mix_blend_mode";
|
||||
log.info(
|
||||
"[Render] Fast capture: composition uses mix-blend-mode — disabling drawElementImage " +
|
||||
"for this render. Capture uses the platform's baseline route.",
|
||||
@@ -202,6 +208,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
compiled.hasShaderTransitions
|
||||
) {
|
||||
cfg.useDrawElement = false;
|
||||
deCompileGate = "shader_transitions";
|
||||
log.info(
|
||||
"[Render] Fast capture: composition uses shader transitions — disabling drawElementImage " +
|
||||
"so page-side compositing stays available.",
|
||||
@@ -269,5 +276,6 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
outputHeight,
|
||||
compileOnlyMs,
|
||||
forceScreenshot,
|
||||
deCompileGate,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -352,6 +352,42 @@ export interface RenderPerfSummary {
|
||||
reusedFrames: number;
|
||||
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 {
|
||||
@@ -1167,6 +1203,13 @@ export async function executeRenderJob(
|
||||
// via the explicit `forceScreenshot` parameter rather than reading
|
||||
// `cfg.forceScreenshot` directly.
|
||||
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 });
|
||||
observability.checkpoint("compile", "composition metadata resolved", {
|
||||
width,
|
||||
@@ -1584,6 +1627,7 @@ export async function executeRenderJob(
|
||||
(!useStreamingEncode || workerCount > 1)
|
||||
) {
|
||||
cfg.useDrawElement = false;
|
||||
deClampReason = workerCount > 1 ? "parallel" : "disk_path";
|
||||
log.info(
|
||||
"[Render] Fast capture: default-on drawElement disabled for this render — " +
|
||||
(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)
|
||||
// was consumed by it, so a fresh session spawns on retry.
|
||||
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", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
@@ -1858,6 +1906,7 @@ export async function executeRenderJob(
|
||||
const captureFrameMs = Date.now() - captureFrameStart;
|
||||
if (streamingRes.success) {
|
||||
streamingHandled = true;
|
||||
deDrainStats = streamingRes.deDrainStats;
|
||||
workerCount = streamingRes.workerCount;
|
||||
updateCaptureObservability({ workerCount });
|
||||
if (streamingRes.captureBeyondViewport !== undefined) {
|
||||
@@ -2026,6 +2075,13 @@ export async function executeRenderJob(
|
||||
captureCalibration,
|
||||
captureAttempts,
|
||||
dedupPerfs,
|
||||
drawElement: {
|
||||
compileGate: deCompileGate,
|
||||
clampReason: deClampReason,
|
||||
selfVerifyFallback: deSelfVerifyFallback,
|
||||
fallbackReason: deFallbackReason,
|
||||
drainStats: deDrainStats,
|
||||
},
|
||||
hdrDiagnostics,
|
||||
hdrPerf,
|
||||
observability: observabilitySummary,
|
||||
|
||||
Reference in New Issue
Block a user