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
117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
/**
|
|
* Compares measured perf metrics against baseline.json with an allowed regression ratio.
|
|
*
|
|
* Mirrors packages/producer/src/perf-gate.ts: each metric has a baseline value, the
|
|
* gate computes `max = baseline * (1 + allowedRegressionRatio)`, and any measured
|
|
* value above max counts as a regression. In "measure" mode the script logs but
|
|
* never exits non-zero — useful for the first runs while we collect realistic
|
|
* baselines on the CI runner. Flip to "enforce" once baselines are committed.
|
|
*/
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const DEFAULT_BASELINE_PATH = resolve(HERE, "baseline.json");
|
|
|
|
export type Direction = "lower-is-better" | "higher-is-better";
|
|
|
|
export type Metric = {
|
|
/** Display name, e.g. "comp_load_cold_p95_ms" */
|
|
name: string;
|
|
/** Key into baseline.json, e.g. "compLoadColdP95Ms" */
|
|
baselineKey: keyof PerfBaseline;
|
|
value: number;
|
|
unit: string;
|
|
direction: Direction;
|
|
samples?: number[];
|
|
};
|
|
|
|
export type PerfBaseline = {
|
|
compLoadColdP95Ms: number;
|
|
compLoadWarmP95Ms: number;
|
|
/**
|
|
* Floor on `(compositionTime advanced) / (wallClock elapsed)` over a sustained
|
|
* playback window — see packages/player/tests/perf/scenarios/02-fps.ts. A
|
|
* healthy player keeps up with its intended speed and reads ~1.0; values
|
|
* below 1.0 mean the composition clock fell behind real time, which is the
|
|
* actual user-visible jank we want to gate against. Refresh-rate independent
|
|
* by construction, so it does not saturate to display refresh on high-Hz
|
|
* runners the way the previous `fpsMin` did. Direction: higher-is-better.
|
|
*/
|
|
compositionTimeAdvancementRatioMin: number;
|
|
scrubLatencyP95IsolatedMs: number;
|
|
scrubLatencyP95InlineMs: number;
|
|
driftMaxMs: number;
|
|
driftP95Ms: number;
|
|
paritySsimMin: number;
|
|
allowedRegressionRatio: number;
|
|
};
|
|
|
|
export type GateMode = "measure" | "enforce";
|
|
|
|
export type GateResult = {
|
|
metric: Metric;
|
|
baseline: number;
|
|
threshold: number;
|
|
passed: boolean;
|
|
ratio: number;
|
|
};
|
|
|
|
export function loadBaseline(path?: string): PerfBaseline {
|
|
const baselinePath = path ?? process.env.PLAYER_PERF_BASELINE_PATH ?? DEFAULT_BASELINE_PATH;
|
|
const raw = readFileSync(baselinePath, "utf-8");
|
|
return JSON.parse(raw) as PerfBaseline;
|
|
}
|
|
|
|
export function evaluateMetric(metric: Metric, baseline: PerfBaseline): GateResult {
|
|
const baselineValue = baseline[metric.baselineKey];
|
|
if (typeof baselineValue !== "number") {
|
|
throw new Error(`[player-perf] baseline missing numeric key: ${String(metric.baselineKey)}`);
|
|
}
|
|
const allowed = baseline.allowedRegressionRatio;
|
|
const threshold =
|
|
metric.direction === "lower-is-better"
|
|
? baselineValue * (1 + allowed)
|
|
: baselineValue * (1 - allowed);
|
|
const passed =
|
|
metric.direction === "lower-is-better" ? metric.value <= threshold : metric.value >= threshold;
|
|
const ratio = baselineValue === 0 ? 0 : metric.value / baselineValue;
|
|
return { metric, baseline: baselineValue, threshold, passed, ratio };
|
|
}
|
|
|
|
export type GateReport = {
|
|
passed: boolean;
|
|
rows: GateResult[];
|
|
};
|
|
|
|
export function reportAndGate(
|
|
metrics: Metric[],
|
|
// `mode` is resolved upstream in packages/player/tests/perf/index.ts
|
|
// (`parseArgs`): the default comes from PLAYER_PERF_MODE env or "measure", and
|
|
// the CLI flag `--mode=measure|enforce` overrides it. The "flip to enforce"
|
|
// TODO lives at that call site so it is a one-line change.
|
|
mode: GateMode,
|
|
baselinePath?: string,
|
|
): GateReport {
|
|
const baseline = loadBaseline(baselinePath);
|
|
const rows = metrics.map((m) => evaluateMetric(m, baseline));
|
|
console.log("[PerfGate] mode=" + mode);
|
|
for (const row of rows) {
|
|
const status = row.passed ? "PASS" : "FAIL";
|
|
const dir = row.metric.direction === "lower-is-better" ? "≤" : "≥";
|
|
console.log(
|
|
`[PerfGate] ${status} ${row.metric.name} = ${row.metric.value.toFixed(2)}${row.metric.unit} (baseline=${row.baseline}${row.metric.unit}, threshold ${dir} ${row.threshold.toFixed(2)}${row.metric.unit}, ratio=${row.ratio.toFixed(3)})`,
|
|
);
|
|
}
|
|
const failed = rows.filter((r) => !r.passed);
|
|
if (failed.length === 0) return { passed: true, rows };
|
|
if (mode === "measure") {
|
|
console.log(`[PerfGate] ${failed.length} regression(s) detected — measure mode, not failing`);
|
|
return { passed: true, rows };
|
|
}
|
|
console.error(`[PerfGate] ${failed.length} regression(s) detected — enforce mode, failing`);
|
|
return { passed: false, rows };
|
|
}
|