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
252 lines
10 KiB
TypeScript
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 },
|
|
),
|
|
};
|
|
}
|