Files
heygen-com--hyperframes/packages/producer/src/services/render/perfSummary.ts
T
wehub-resource-sync 85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

252 lines
10 KiB
TypeScript

/**
* Build the `RenderPerfSummary` that lands on `job.perfSummary` and
* the `perf-summary.json` debug artifact.
*/
import { fpsToNumber } from "@hyperframes/core";
import type { CapturePerfSummary, SubTimelineWaitOutcome } from "@hyperframes/engine";
import type { CaptureCalibrationSample, CaptureCostEstimate } from "./captureCost.js";
import type {
CaptureAttemptSummary,
HdrDiagnostics,
RenderJob,
RenderPerfSummary,
} from "../renderOrchestrator.js";
import { type HdrPerfCollector, finalizeHdrPerf } from "./hdrPerf.js";
import type { RenderObservabilitySummary } from "./observability.js";
/**
* Worst sub-composition timeline wait outcome across sessions: script_failure
* > timeout > ready. Shared by the success-path perf summary and the error
* path (a fail-fast can still be followed by an unrelated downstream
* failure, e.g. in `pollVideosReady` or encode — that render fires
* `render_error`, not `render_complete`, and should carry this too).
*/
export function worstSubTimelineWaitOutcome(
perfs: CapturePerfSummary[],
): SubTimelineWaitOutcome | undefined {
const outcomes = perfs
.map((p) => p.subTimelineWaitOutcome)
.filter((o): o is SubTimelineWaitOutcome => !!o);
if (outcomes.length === 0) return undefined;
if (outcomes.includes("script_failure")) return "script_failure";
if (outcomes.includes("timeout")) return "timeout";
return "ready";
}
/**
* Append each parallel worker's static-dedup perf into the render-level sink
* (skipping workers that reported none). Shared by the disk + streaming parallel
* paths so the collection contract lives in one place.
*/
export function pushWorkerDedupPerfs(
results: ReadonlyArray<{ perf?: CapturePerfSummary }>,
sink: CapturePerfSummary[],
): void {
for (const r of results) {
if (r.perf) sink.push(r.perf);
}
}
/**
* Collapse per-session/per-worker static-dedup perf into one render-level
* outcome. enabled/armed = OR across workers (they run the same gates on the
* 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.
*/
/** Orchestrator-supplied render-level drawElement outcome (one shape, used by
* both the aggregate function and buildRenderPerfSummary's input). */
export interface DrawElementPerfInput {
compileGate?: string;
clampReason?: string;
workerInversion?: "inverted" | "reverted";
/** Auto-resolved worker count before the inversion pinned it to 1 (set only when the inversion fired). */
preInversionWorkers?: number;
parallelRouter?: "routed" | "reverted";
/** Auto-resolved worker count before the router pinned it to 3 (set only when the router fired). */
preRouterWorkers?: number;
selfVerifyFallback: boolean;
fallbackReason?: string;
drainStats?: {
verifyChecked: number;
verifyMinDb?: number;
blankSuspects: number;
blankDeterministicAccepts: number;
blankRecaptures: number;
};
}
// Flat field mapping — branches are ?? fallbacks, not logic.
// fallow-ignore-next-line complexity
function aggregateDrawElement(
perfs: CapturePerfSummary[],
de: DrawElementPerfInput,
): 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,
workerInversion: de.workerInversion ?? "none",
preInversionWorkers: de.preInversionWorkers,
parallelRouter: de.parallelRouter ?? "none",
preRouterWorkers: de.preRouterWorkers,
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);
// When unarmed, report every DISTINCT skip reason across workers (sorted, joined)
// rather than just the first — workers can diverge (e.g. one `ineligible`, one
// `capture_mode`), and dropping the rest hides why dedup didn't engage. Cardinality
// stays bounded (a handful of codes, small combinations).
const skipReasons = armed
? []
: [
...new Set(perfs.map((p) => p.staticDedupSkipReason).filter((r): r is string => !!r)),
].sort();
return {
enabled: perfs.some((p) => p.staticDedupEnabled),
armed,
predictedFrames: perfs.reduce((sum, p) => sum + (p.staticDedupPredicted ?? 0), 0),
reusedFrames: perfs.reduce((sum, p) => sum + (p.staticDedupReused ?? 0), 0),
skipReason: skipReasons.length > 0 ? skipReasons.join("|") : undefined,
};
}
/**
* Collapse per-session/per-worker BeginFrame damage counters into one
* render-level reuse outcome (SUM across workers — each worker ticks its own
* frame range). Both zero ⟺ no beginframe session ran (screenshot/drawElement
* sessions never increment these) → undefined, mirroring staticDedup's
* "undefined when it never engaged" contract. Also inherits `dedupPerfs`'
* retry semantics: a partial-capture retry resets the sink, so the sums cover
* only the final attempt's recaptured ranges (may be < totalFrames).
*/
function aggregateBeginFrameReuse(
perfs: CapturePerfSummary[],
): RenderPerfSummary["beginFrameReuse"] {
const noDamageFrames = perfs.reduce((sum, p) => sum + (p.beginFrameNoDamage ?? 0), 0);
const hasDamageFrames = perfs.reduce((sum, p) => sum + (p.beginFrameHasDamage ?? 0), 0);
if (noDamageFrames + hasDamageFrames === 0) return undefined;
return { noDamageFrames, hasDamageFrames };
}
export function buildRenderPerfSummary(input: {
job: RenderJob;
workerCount: number;
enableChunkedEncode: boolean;
chunkedEncodeSize: number;
compositionDurationSeconds: number;
totalFrames: number;
outputWidth: number;
outputHeight: number;
videoCount: number;
audioCount: number;
totalElapsedMs: number;
perfStages: Record<string, number>;
videoExtractBreakdown: RenderPerfSummary["videoExtractBreakdown"];
tmpPeakBytes: number;
captureCalibration?: {
estimate: CaptureCostEstimate;
samples: CaptureCalibrationSample[];
};
captureAttempts: CaptureAttemptSummary[];
hdrDiagnostics: HdrDiagnostics;
hdrPerf?: HdrPerfCollector;
observability?: RenderObservabilitySummary;
peakRssBytes: number;
peakHeapUsedBytes: number;
/** Per-session/per-worker static-dedup perf; aggregated into `staticDedup`. */
dedupPerfs: CapturePerfSummary[];
drawElement?: DrawElementPerfInput;
}): RenderPerfSummary {
return {
renderId: input.job.id,
totalElapsedMs: input.totalElapsedMs,
// RenderPerfSummary surfaces fps as a decimal because it lands in JSON
// payloads (CLI telemetry, regression-harness reports) where a single
// number is friendlier than `{num,den}`. Callers needing the rational
// back can read `job.config.fps`.
fps: fpsToNumber(input.job.config.fps),
quality: input.job.config.quality,
workers: input.workerCount,
chunkedEncode: input.enableChunkedEncode,
chunkSizeFrames: input.enableChunkedEncode ? input.chunkedEncodeSize : null,
compositionDurationSeconds: input.compositionDurationSeconds,
totalFrames: input.totalFrames,
resolution: { width: input.outputWidth, height: input.outputHeight },
videoCount: input.videoCount,
audioCount: input.audioCount,
stages: input.perfStages,
videoExtractBreakdown: input.videoExtractBreakdown,
tmpPeakBytes: input.tmpPeakBytes,
captureCalibration: input.captureCalibration
? {
sampledFrames: input.captureCalibration.samples.map((sample) => sample.frameIndex),
p95Ms: input.captureCalibration.estimate.p95Ms,
multiplier: input.captureCalibration.estimate.multiplier,
reasons: input.captureCalibration.estimate.reasons,
}
: undefined,
captureAttempts: input.captureAttempts.length > 0 ? input.captureAttempts : undefined,
hdrDiagnostics:
input.hdrDiagnostics.videoExtractionFailures > 0 ||
input.hdrDiagnostics.imageDecodeFailures > 0
? { ...input.hdrDiagnostics }
: undefined,
hdrPerf: input.hdrPerf ? finalizeHdrPerf(input.hdrPerf) : undefined,
observability: input.observability,
captureAvgMs:
input.totalFrames > 0
? Math.round(
(input.perfStages.captureFrameMs ?? input.perfStages.captureMs ?? 0) /
input.totalFrames,
)
: undefined,
subTimelineWait: worstSubTimelineWaitOutcome(input.dedupPerfs),
captureP50Ms: (() => {
// Per-frame median from the engine's samples; when parallel workers
// report separately, take the busiest session's median.
const withSamples = input.dedupPerfs.filter((p) => (p.p50TotalMs ?? 0) > 0);
if (withSamples.length === 0) return undefined;
return withSamples.reduce((a, b) => (b.frames > a.frames ? b : a)).p50TotalMs;
})(),
peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
staticDedup: aggregateDedup(input.dedupPerfs),
beginFrameReuse: aggregateBeginFrameReuse(input.dedupPerfs),
drawElement: aggregateDrawElement(
input.dedupPerfs,
input.drawElement ?? { selfVerifyFallback: false },
),
};
}