')
.replace('', `Fields List(${count(state.payload.summary?.fields?.total ?? state.payload.fields.length)})
`)
@@ -1793,6 +1803,10 @@ export function startDifferentialTestingLiveUpdates(root, {
historyImpl = globalThis.history,
mount = mountDifferentialTestingDashboard,
repoKey = null,
+ dataOvenId = "differential-testing",
+ adaptOven = (value) => value,
+ adaptPayload = (value) => value,
+ mountOptions = {},
refreshMs = DIFFERENTIAL_TESTING_REFRESH_MS,
onError = (error, hasDashboard) => {
if (!hasDashboard) root.innerHTML = `${escapeHtml(String(error?.message || error))}
`;
@@ -1828,7 +1842,7 @@ export function startDifferentialTestingLiveUpdates(root, {
searchParams.set("pageSize", String(fieldViewQuery.pageSize));
}
const query = searchParams.toString();
- return `/api/oven-data/differential-testing${query ? `?${query}` : ""}`;
+ return `/api/oven-data/${encodeURIComponent(dataOvenId)}${query ? `?${query}` : ""}`;
};
const read = async (url, key, fallbackMessage) => {
@@ -1851,7 +1865,7 @@ export function startDifferentialTestingLiveUpdates(root, {
const json = await response.json();
if (!response.ok) throw new Error(json.error || "Could not load Differential Testing data.");
const result = {
- payload: differentialPagedPayload(json.payload, json.fieldPage),
+ payload: differentialPagedPayload(adaptPayload(json.payload), json.fieldPage),
transport: json.transport ?? null,
fieldPage: json.fieldPage ?? null,
frameDeltaMetrics: json.frameDeltaMetrics ?? null,
@@ -1878,7 +1892,7 @@ export function startDifferentialTestingLiveUpdates(root, {
const requestPayloadUrl = payloadUrl(requestScenarioId);
try {
const [nextOven, payloadResult] = await Promise.all([
- oven ?? read("/api/ovens/differential-testing", "oven", "Could not load Differential Testing Oven."),
+ oven ?? read(`/api/ovens/${encodeURIComponent(dataOvenId)}`, "oven", `Could not load ${dataOvenId} Oven.`),
readPayload(requestPayloadUrl),
]);
if (stopped || requestGeneration !== scenarioGeneration) return;
@@ -1888,9 +1902,10 @@ export function startDifferentialTestingLiveUpdates(root, {
return;
}
const nextRevision = payloadResult.etag || differentialPayloadRevision(payload);
- oven = nextOven;
+ oven = adaptOven(nextOven);
if (!dashboard) {
dashboard = mount(root, oven, payload, {
+ ...mountOptions,
onScenarioChange: selectScenario,
onFieldViewChange: selectFieldView,
fieldPage: payloadResult.fieldPage,
diff --git a/ovens/performance-tracing/detail.json b/ovens/performance-tracing/detail.json
new file mode 100644
index 0000000..6d1040a
--- /dev/null
+++ b/ovens/performance-tracing/detail.json
@@ -0,0 +1,85 @@
+{
+ "version": 1,
+ "columns": 12,
+ "rows": 14,
+ "rowHeight": 64,
+ "cells": [
+ {
+ "id": "title",
+ "title": "Performance Tracing",
+ "widget": "status",
+ "source": "/",
+ "format": "plain",
+ "column": 1,
+ "row": 1,
+ "columnSpan": 4,
+ "rowSpan": 1
+ },
+ {
+ "id": "burns",
+ "title": "Results",
+ "widget": "pie-chart",
+ "source": "/log",
+ "format": "plain",
+ "column": 5,
+ "row": 1,
+ "columnSpan": 2,
+ "rowSpan": 1
+ },
+ {
+ "id": "fields",
+ "title": "Fields",
+ "widget": "metric",
+ "source": "/summary/fields",
+ "format": "plain",
+ "column": 7,
+ "row": 1,
+ "columnSpan": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "frames",
+ "title": "Frames",
+ "widget": "metric",
+ "source": "/summary/frames",
+ "format": "plain",
+ "column": 10,
+ "row": 1,
+ "columnSpan": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "progress",
+ "title": "Trace Progress",
+ "widget": "line-chart",
+ "source": "/progress",
+ "format": "number",
+ "column": 5,
+ "row": 2,
+ "columnSpan": 8,
+ "rowSpan": 5
+ },
+ {
+ "id": "log",
+ "title": "Run History",
+ "widget": "log",
+ "source": "/log",
+ "format": "plain",
+ "column": 1,
+ "row": 2,
+ "columnSpan": 4,
+ "rowSpan": 5
+ },
+ {
+ "id": "field-details",
+ "title": "Budget Details",
+ "widget": "comparison",
+ "source": "/fields",
+ "format": "plain",
+ "column": 1,
+ "row": 7,
+ "columnSpan": 12,
+ "rowSpan": 7
+ }
+ ]
+}
diff --git a/ovens/performance-tracing/engine/performance-tracing-contract.mjs b/ovens/performance-tracing/engine/performance-tracing-contract.mjs
new file mode 100644
index 0000000..8745f4d
--- /dev/null
+++ b/ovens/performance-tracing/engine/performance-tracing-contract.mjs
@@ -0,0 +1,209 @@
+export const PERFORMANCE_TRACING_DATA_SCHEMA = "performance-tracing-oven@1";
+export const PERFORMANCE_TRACING_HISTORY_SCHEMA = "performance-history-point@1";
+export const PERFORMANCE_TRACING_DIAGNOSTICS_SCHEMA = "performance-diagnostics@1";
+const MAX_HISTORY_POINTS = 120;
+
+const METRIC_KEYS = Object.freeze([
+ "runCount",
+ "startupReadyMs",
+ "p95FrameMs",
+ "p99FrameMs",
+ "maxFrameMs",
+ "over33msRatio",
+ "p95StepCallMs",
+ "pageErrorCount",
+ "nativeRequestCount",
+ "runtimeConstructionCount",
+]);
+
+export function assertPerformanceTracingData(value, label = "Performance Tracing data") {
+ if (!value || value.schema !== PERFORMANCE_TRACING_DATA_SCHEMA) {
+ throw new Error(label + " must use " + PERFORMANCE_TRACING_DATA_SCHEMA + ".");
+ }
+ if (!value.runId || !isIsoTimestamp(value.generatedAt) || !["pass", "fail"].includes(value.status)) {
+ throw new Error(label + " must identify one timestamped pass/fail run.");
+ }
+ if (value.trust?.classification !== "browser-output-performance-evidence"
+ || value.trust.preparedRoute !== true || value.trust.nativeParityClaim !== false
+ || value.trust.visualParityClaim !== false) {
+ throw new Error(label + " must retain its browser-output-only trust boundary.");
+ }
+ for (const key of METRIC_KEYS) {
+ if (!Number.isFinite(value.metrics?.[key]) || value.metrics[key] < 0) {
+ throw new Error(label + ".metrics." + key + " must be finite and non-negative.");
+ }
+ }
+ if (!value.browser?.engine || !value.browser?.version || !value.scenario?.id
+ || typeof value.scenario.route !== "string" || !value.scenario.route.startsWith("/")) {
+ throw new Error(label + " must bind browser and canonical scenario identity.");
+ }
+ const checks = value.verdict?.checks;
+ if (!Array.isArray(checks) || checks.length === 0 || value.verdict.status !== value.status) {
+ throw new Error(label + " must publish one reconciled budget verdict.");
+ }
+ const ids = new Set();
+ for (const check of checks) {
+ if (!check?.id || ids.has(check.id) || !Number.isFinite(check.actual) || !Number.isFinite(check.limit)
+ || check.operator !== "<=" || !["pass", "fail"].includes(check.status)
+ || check.status !== (check.actual <= check.limit ? "pass" : "fail")) {
+ throw new Error(label + " contains an invalid or contradictory budget check.");
+ }
+ ids.add(check.id);
+ }
+ const expectedStatus = checks.every((check) => check.status === "pass") ? "pass" : "fail";
+ if (value.status !== expectedStatus
+ || value.verdict.passCount !== checks.filter((check) => check.status === "pass").length
+ || value.verdict.failCount !== checks.filter((check) => check.status === "fail").length) {
+ throw new Error(label + " budget counts and status do not reconcile.");
+ }
+ if (!value.artifacts?.report || !value.provenance?.files || typeof value.provenance.files !== "object") {
+ throw new Error(label + " must bind retained artifacts and source provenance.");
+ }
+ if (value.runs !== undefined) assertRuns(value.runs, label + ".runs");
+ if (value.history !== undefined) assertHistory(value.history, value, label + ".history");
+ assertDiagnostics(value.diagnostics, value, label + ".diagnostics");
+ return value;
+}
+
+function assertDiagnostics(diagnostics, current, label) {
+ if (!diagnostics || diagnostics.schema !== PERFORMANCE_TRACING_DIAGNOSTICS_SCHEMA
+ || diagnostics.runId !== current.runId || diagnostics.generatedAt !== current.generatedAt) {
+ throw new Error(label + " must identify the current report with " + PERFORMANCE_TRACING_DIAGNOSTICS_SCHEMA + ".");
+ }
+ if (!Array.isArray(diagnostics.actionItems)
+ || (diagnostics.actionItems.length === 0 && diagnostics.primaryTarget !== null)
+ || (diagnostics.actionItems.length > 0 && diagnostics.primaryTarget?.id !== diagnostics.actionItems[0].id)) {
+ throw new Error(label + " must reconcile its optional measured primary target with the action plan.");
+ }
+ for (const [index, item] of diagnostics.actionItems.entries()) {
+ if (!item?.id || item.priority !== index + 1 || !item.target || !item.producer || !item.signal
+ || !item.nextAction || !item.evidence || typeof item.evidence !== "object"
+ || !Array.isArray(item.verifyMetrics) || item.verifyMetrics.length === 0) {
+ throw new Error(label + " contains an action item without measured evidence, a source producer, or verification metrics.");
+ }
+ }
+ if (!Array.isArray(diagnostics.budgetGaps) || !diagnostics.comparison || typeof diagnostics.comparison !== "object"
+ || !Array.isArray(diagnostics.phaseBottlenecks) || !Array.isArray(diagnostics.cameraPhaseBottlenecks)
+ || !Array.isArray(diagnostics.traceGroups)
+ || !Array.isArray(diagnostics.residencySpikes) || !Array.isArray(diagnostics.runs) || diagnostics.runs.length === 0) {
+ throw new Error(label + " must retain budget gaps, comparison context, bottlenecks, and per-run evidence.");
+ }
+ for (const gap of diagnostics.budgetGaps) {
+ if (!gap?.id || !Number.isFinite(gap.actual) || !Number.isFinite(gap.limit) || !Number.isFinite(gap.excess)) {
+ throw new Error(label + " contains an invalid budget gap.");
+ }
+ }
+ for (const run of diagnostics.runs) {
+ if (!run?.runId || !Array.isArray(run.frameSpikes) || !Array.isArray(run.stepSpikes)
+ || !Array.isArray(run.phaseBottlenecks) || !Array.isArray(run.traceGroups)
+ || !Array.isArray(run.cameraPhaseBottlenecks)
+ || !Array.isArray(run.hotWindows) || !Array.isArray(run.topEvents)
+ || !run.structure?.integrity || typeof run.structure.integrity !== "object") {
+ throw new Error(label + " contains incomplete per-run spike, phase, trace-window, or integrity evidence.");
+ }
+ for (const phase of run.phaseBottlenecks) assertPhase(phase, label + ".runs phase");
+ for (const phase of run.cameraPhaseBottlenecks) assertPhase(phase, label + ".runs camera phase");
+ for (const window of run.hotWindows) {
+ if (!Number.isFinite(window?.startMs) || !Number.isFinite(window.endMs)
+ || !Number.isFinite(window.classifiedThreadTimeMs) || !Array.isArray(window.contributors)) {
+ throw new Error(label + " contains an invalid hot trace window.");
+ }
+ }
+ }
+ const rerun = diagnostics.rerun;
+ const latestComparisonKey = current.history?.at(-1)?.comparisonKey;
+ if (typeof rerun?.command !== "string" || !rerun.command.trim() || rerun.compareAgainstRunId !== current.runId
+ || !rerun.comparisonKey || (latestComparisonKey && rerun.comparisonKey !== latestComparisonKey)
+ || !Array.isArray(rerun.protocol) || rerun.protocol.length === 0
+ || rerun.requiredIntegrity?.pageErrorCount !== 0 || rerun.requiredIntegrity?.nativeRequestCount !== 0
+ || rerun.requiredIntegrity?.runtimeConstructionCount !== 0 || !Array.isArray(rerun.successCriteria)
+ || !Array.isArray(diagnostics.caveats) || diagnostics.caveats.length < 3) {
+ throw new Error(label + " must retain the exact comparable rerun, integrity gate, success criteria, and caveats.");
+ }
+}
+
+function assertHistory(history, current, label) {
+ if (!Array.isArray(history) || history.length === 0 || history.length > MAX_HISTORY_POINTS) {
+ throw new Error(label + " must contain 1-" + MAX_HISTORY_POINTS + " retained trace points.");
+ }
+ const identities = new Set();
+ let priorTime = -Infinity;
+ for (const point of history) {
+ const time = Date.parse(point?.generatedAt);
+ const identity = point?.runId + "\n" + point?.generatedAt;
+ if (point?.schema !== PERFORMANCE_TRACING_HISTORY_SCHEMA || !point.runId || !Number.isFinite(time)
+ || !["pass", "fail"].includes(point.status) || !point.comparisonKey
+ || !point.context || typeof point.context !== "object" || identities.has(identity)
+ || time < priorTime) {
+ throw new Error(label + " contains invalid, duplicate, or unordered points.");
+ }
+ identities.add(identity);
+ priorTime = time;
+ for (const key of ["startupReadyMs", "p95FrameMs", "p99FrameMs", "maxFrameMs", "over33msRatio", "p95StepCallMs", "maxStepCallMs", "residencyTransitionStepCount"]) {
+ if (!Number.isFinite(point.metrics?.[key]) || point.metrics[key] < 0) {
+ throw new Error(label + " contains an invalid " + key + " metric.");
+ }
+ }
+ if (!point.budgets || !Number.isFinite(point.budgets.p95FrameMs)
+ || !point.traceGroups || typeof point.traceGroups !== "object") {
+ throw new Error(label + " must retain budgets and trace groups.");
+ }
+ for (const group of Object.values(point.traceGroups)) {
+ if (!group?.label || !Number.isFinite(group.durationMs) || group.durationMs < 0
+ || !Number.isFinite(group.count) || group.count < 0 || !Number.isFinite(group.maxMs) || group.maxMs < 0) {
+ throw new Error(label + " contains an invalid trace group.");
+ }
+ }
+ }
+ const latest = history.at(-1);
+ if (latest.runId !== current.runId || latest.generatedAt !== current.generatedAt
+ || latest.status !== current.status || latest.metrics.p95FrameMs !== current.metrics.p95FrameMs) {
+ throw new Error(label + " latest point must reconcile with the current report.");
+ }
+}
+
+function assertRuns(runs, label) {
+ if (!Array.isArray(runs) || runs.length === 0) throw new Error(label + " must contain retained browser runs.");
+ for (const run of runs) {
+ if (run?.status !== "passed" || !Number.isFinite(run.frameTiming?.p95FrameMs)
+ || !Array.isArray(run.frameTiming?.series) || run.frameTiming.series.length === 0
+ || !Number.isFinite(run.stepTiming?.p95StepCallMs) || !Array.isArray(run.stepTiming?.slowestSteps)
+ || !Array.isArray(run.stepTiming?.series) || run.stepTiming.series.length === 0
+ || run.stepTiming?.phaseTiming?.schema !== "runtime-dispatch-phase-summary@1"
+ || !Number.isSafeInteger(run.stepTiming.phaseTiming.sampleCount) || run.stepTiming.phaseTiming.sampleCount < 1
+ || !run.stepTiming.phaseTiming.phases || typeof run.stepTiming.phaseTiming.phases !== "object"
+ || run.stepTiming?.cameraPhaseTiming?.schema !== "camera-publication-phase-summary@1"
+ || !Number.isSafeInteger(run.stepTiming.cameraPhaseTiming.sampleCount) || run.stepTiming.cameraPhaseTiming.sampleCount < 1
+ || !run.stepTiming.cameraPhaseTiming.phases || typeof run.stepTiming.cameraPhaseTiming.phases !== "object"
+ || run.trace?.schema !== "browser-performance-trace-summary@2"
+ || run.trace?.attributionMode !== "exclusive-classified-thread-time@1"
+ || run.trace?.measurementWindow?.status !== "bounded"
+ || !run.trace?.groups || typeof run.trace.groups !== "object") {
+ throw new Error(label + " contains an incomplete retained browser run.");
+ }
+ for (const phase of Object.entries(run.stepTiming.phaseTiming.phases).map(([id, value]) => ({ id, ...value }))) {
+ assertPhase(phase, label + " phase");
+ }
+ for (const phase of Object.entries(run.stepTiming.cameraPhaseTiming.phases).map(([id, value]) => ({ id, ...value }))) {
+ assertPhase(phase, label + " camera phase");
+ }
+ for (const group of Object.values(run.trace.groups)) {
+ if (!Array.isArray(group?.timeline?.values) || group.timeline.mode !== "exclusive-classified-thread-time"
+ || !Number.isFinite(group.timeline.bucketDurationMs) || !Number.isFinite(group.inclusiveDurationMs)) {
+ throw new Error(label + " contains a trace group without a source timeline.");
+ }
+ }
+ }
+}
+
+function assertPhase(phase, label) {
+ if (!phase?.id || !phase.label || !phase.producer || !phase.nextProbe || !Number.isSafeInteger(phase.sampleCount)
+ || phase.sampleCount < 0 || !Number.isFinite(phase.totalMs) || phase.totalMs < 0
+ || !Number.isFinite(phase.p95Ms) || phase.p95Ms < 0 || !Number.isFinite(phase.maxMs) || phase.maxMs < 0) {
+ throw new Error(label + " is missing bounded timing, source producer, or next-probe evidence.");
+ }
+}
+
+function isIsoTimestamp(value) {
+ return typeof value === "string" && Number.isFinite(Date.parse(value)) && /T/u.test(value);
+}
diff --git a/ovens/performance-tracing/engine/performance-tracing-contract.test.mjs b/ovens/performance-tracing/engine/performance-tracing-contract.test.mjs
new file mode 100644
index 0000000..3b4cbc9
--- /dev/null
+++ b/ovens/performance-tracing/engine/performance-tracing-contract.test.mjs
@@ -0,0 +1,240 @@
+import assert from "node:assert/strict";
+import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+import { adaptPerformanceTracingReport } from "../../../dashboard/src/lib/performance-tracing.mjs";
+import { startDifferentialTestingLiveUpdates } from "../../differential-testing/renderer/differential-testing-renderer.js";
+import { assertPerformanceTracingData } from "./performance-tracing-contract.mjs";
+import { assertPerformanceTracingProvenanceCurrent } from "./performance-tracing-handler.mjs";
+import { createHash } from "node:crypto";
+
+test("Performance Tracing validates reconciled browser-output reports", () => {
+ assert.equal(assertPerformanceTracingData(fixture()).status, "fail");
+ const genericProject = fixture();
+ genericProject.scenario.route = "/benchmark";
+ genericProject.diagnostics.rerun.command = "npm run benchmark";
+ assert.equal(assertPerformanceTracingData(genericProject).scenario.route, "/benchmark");
+});
+
+test("Performance Tracing rejects weakened trust and contradictory budgets", () => {
+ const weakened = fixture();
+ weakened.trust.nativeParityClaim = true;
+ assert.throws(() => assertPerformanceTracingData(weakened), /browser-output-only trust boundary/u);
+ const contradictory = fixture();
+ contradictory.verdict.checks[0].status = "pass";
+ assert.throws(() => assertPerformanceTracingData(contradictory), /invalid or contradictory budget/u);
+});
+
+test("Performance Tracing rejects unordered or unreconciled history", () => {
+ const unordered = fixture();
+ unordered.history.unshift({ ...structuredClone(unordered.history[0]), runId: "older", generatedAt: "2026-07-15T13:00:00.000Z" });
+ assert.throws(() => assertPerformanceTracingData(unordered), /unordered points/u);
+ const unreconciled = fixture();
+ unreconciled.history[0].metrics.p95FrameMs = 39;
+ assert.throws(() => assertPerformanceTracingData(unreconciled), /latest point must reconcile/u);
+});
+
+test("Performance Tracing blocks a report after a measured input changes", () => {
+ const root = mkdtempSync(join(tmpdir(), "performance-provenance-"));
+ try {
+ const binding = join(root, ".local", "performance", "report.json");
+ const source = join(root, "source.mjs");
+ mkdirSync(join(root, ".local", "performance"), { recursive: true });
+ writeFileSync(binding, "{}\n");
+ writeFileSync(source, "export const value = 1;\n");
+ const bytes = Buffer.from("export const value = 1;\n");
+ const payload = { provenance: { files: { "source.mjs": { bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") } } } };
+ assert.equal(assertPerformanceTracingProvenanceCurrent(payload, binding), payload);
+ writeFileSync(source, "export const value = 2;\n");
+ assert.throws(() => assertPerformanceTracingProvenanceCurrent(payload, binding), /report is stale/u);
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test("Performance Tracing live view reads its own Oven and adapts its report", async () => {
+ const requests = [];
+ const oven = { id: "performance-tracing", name: "Performance Tracing", detail: { cells: [] } };
+ let mounted = null;
+ const controller = startDifferentialTestingLiveUpdates({ innerHTML: "" }, {
+ dataOvenId: "performance-tracing",
+ repoKey: "repo-key",
+ adaptPayload: adaptPerformanceTracingReport,
+ mountOptions: { initialChart: "current", initialProgressChart: "delta" },
+ fetchImpl: async (url) => {
+ requests.push(url);
+ return {
+ ok: true,
+ status: 200,
+ headers: { get: () => null },
+ async json() {
+ return url === "/api/ovens/performance-tracing" ? { oven } : { payload: fixture() };
+ },
+ };
+ },
+ setIntervalImpl: () => 17,
+ clearIntervalImpl() {},
+ mount: (_root, mountedOven, payload, options) => {
+ mounted = { mountedOven, payload, options };
+ return { update() {} };
+ },
+ });
+
+ await controller.ready;
+ controller.stop();
+ assert.deepEqual(requests, [
+ "/api/ovens/performance-tracing",
+ "/api/oven-data/performance-tracing?repoKey=repo-key",
+ ]);
+ assert.equal(mounted.mountedOven, oven);
+ assert.equal(mounted.payload.subtitle, "fixture");
+ assert.equal(mounted.payload.fields.length, 6);
+ assert.equal(mounted.options.initialChart, "current");
+});
+
+function fixture() {
+ const value = {
+ schema: "performance-tracing-oven@1",
+ status: "fail",
+ runId: "fixture",
+ generatedAt: "2026-07-15T12:00:00.000Z",
+ trust: {
+ classification: "browser-output-performance-evidence",
+ preparedRoute: true,
+ nativeParityClaim: false,
+ visualParityClaim: false,
+ },
+ browser: { engine: "chromium", version: "1" },
+ scenario: { id: "prepared", route: "/" },
+ metrics: {
+ runCount: 1,
+ startupReadyMs: 1000,
+ p95FrameMs: 40,
+ p99FrameMs: 50,
+ maxFrameMs: 100,
+ over33msRatio: 0.1,
+ p95StepCallMs: 1,
+ pageErrorCount: 0,
+ nativeRequestCount: 0,
+ runtimeConstructionCount: 0,
+ },
+ verdict: {
+ status: "fail",
+ passCount: 0,
+ failCount: 1,
+ checks: [{ id: "p95FrameMs", actual: 40, limit: 25, operator: "<=", status: "fail" }],
+ },
+ artifacts: { report: "report.json" },
+ provenance: { files: { "source.mjs": { sha256: "a", bytes: 1 } } },
+ runs: [{
+ id: "run-01",
+ status: "passed",
+ frameTiming: { p95FrameMs: 40, series: [{ frame: 0, elapsedMs: 40, frameMs: 40 }] },
+ stepTiming: {
+ p95StepCallMs: 1,
+ slowestSteps: [],
+ series: [{ tick: 0, stepCallMs: 1 }],
+ phaseTiming: {
+ schema: "runtime-dispatch-phase-summary@1",
+ sampleCount: 1,
+ phases: { cameraResidency: phase() },
+ },
+ cameraPhaseTiming: {
+ schema: "camera-publication-phase-summary@1",
+ sampleCount: 1,
+ phases: { assetResidencyDiscovery: phase() },
+ },
+ },
+ trace: {
+ schema: "browser-performance-trace-summary@2",
+ attributionMode: "exclusive-classified-thread-time@1",
+ measurementWindow: { status: "bounded" },
+ groups: { scripting: { label: "JS / scripting", durationMs: 100, inclusiveDurationMs: 120, count: 10, maxMs: 4, timeline: { mode: "exclusive-classified-thread-time", bucketDurationMs: 50, values: [1] } } },
+ },
+ integrity: { pageErrorCount: 0, nativeRequestCount: 0, runtimeConstructionCount: 0 },
+ }],
+ };
+ value.history = [{
+ schema: "performance-history-point@1",
+ runId: value.runId,
+ generatedAt: value.generatedAt,
+ status: value.status,
+ comparisonKey: "fixture-context",
+ context: { browserTarget: "fixture", scenarioId: "prepared" },
+ metrics: {
+ startupReadyMs: 1000,
+ p95FrameMs: 40,
+ p99FrameMs: 50,
+ maxFrameMs: 100,
+ over33msRatio: 0.1,
+ p95StepCallMs: 1,
+ maxStepCallMs: 4,
+ residencyTransitionStepCount: 1,
+ },
+ budgets: { p95FrameMs: 25 },
+ traceGroups: { scripting: { label: "JS / scripting", durationMs: 100, count: 10, maxMs: 4 } },
+ }];
+ value.diagnostics = {
+ schema: "performance-diagnostics@1",
+ runId: value.runId,
+ generatedAt: value.generatedAt,
+ primaryTarget: action(),
+ budgetGaps: [{ id: "p95FrameMs", actual: 40, limit: 25, excess: 15, ratioToLimit: 1.6, percentOverLimit: 60 }],
+ comparison: { comparable: false, previousRunId: null, previousGeneratedAt: null, metricChanges: {} },
+ runs: [{
+ runId: "run-01",
+ frameSpikes: [{ frame: 0, elapsedMs: 40, frameMs: 40, overBudgetMs: 15 }],
+ stepSpikes: [],
+ phaseBottlenecks: [{ id: "cameraResidency", ...phase() }],
+ cameraPhaseBottlenecks: [{ id: "assetResidencyDiscovery", ...phase() }],
+ traceGroups: [],
+ hotWindows: [{ bucket: 0, startMs: 0, endMs: 50, classifiedThreadTimeMs: 1, contributors: [] }],
+ topEvents: [],
+ structure: { integrity: value.runs[0].integrity },
+ }],
+ phaseBottlenecks: [{ id: "cameraResidency", ...phase(), runId: "run-01" }],
+ cameraPhaseBottlenecks: [{ id: "assetResidencyDiscovery", ...phase(), runId: "run-01" }],
+ traceGroups: [],
+ residencySpikes: [],
+ actionItems: [action()],
+ rerun: {
+ command: "pnpm perf:trace -- --runs 1 --no-fail-on-budget",
+ comparisonKey: value.history[0].comparisonKey,
+ compareAgainstRunId: value.runId,
+ protocol: ["Change one producer."],
+ requiredIntegrity: { pageErrorCount: 0, nativeRequestCount: 0, runtimeConstructionCount: 0 },
+ successCriteria: [{ id: "p95FrameMs", operator: "<=", limit: 25 }],
+ },
+ caveats: ["inclusive", "instrumented", "browser output"],
+ };
+ return value;
+}
+
+function phase() {
+ return {
+ label: "camera / city residency",
+ producer: "src/runtime/scene.mjs",
+ nextProbe: "Separate camera and residency work.",
+ sampleCount: 1,
+ totalMs: 4,
+ averageMs: 4,
+ p50Ms: 4,
+ p95Ms: 4,
+ maxMs: 4,
+ attributedShare: 1,
+ };
+}
+
+function action() {
+ return {
+ id: "dispatch-phase-cameraResidency",
+ priority: 1,
+ target: "camera / city residency",
+ producer: "src/runtime/scene.mjs",
+ signal: "dispatch phase max 4 ms",
+ evidence: { maxMs: 4 },
+ nextAction: "Separate camera and residency work.",
+ verifyMetrics: ["p95StepCallMs"],
+ };
+}
diff --git a/ovens/performance-tracing/engine/performance-tracing-handler.mjs b/ovens/performance-tracing/engine/performance-tracing-handler.mjs
new file mode 100644
index 0000000..fc0c341
--- /dev/null
+++ b/ovens/performance-tracing/engine/performance-tracing-handler.mjs
@@ -0,0 +1,76 @@
+import { createHash } from "node:crypto";
+import { readFileSync } from "node:fs";
+import { dirname, isAbsolute, relative, resolve } from "node:path";
+import { registerOvenHandler } from "../../../src/ovens/oven-registry.mjs";
+import { readTextFileWithLimit, safeStat } from "../../../src/server/fs-safe.mjs";
+import { assertPerformanceTracingData } from "./performance-tracing-contract.mjs";
+
+export const performanceTracingHandler = Object.freeze({
+ id: "performance-tracing",
+
+ serveData({ bindingPath, maxOvenDataBytes }) {
+ if (!safeStat(bindingPath)?.isFile()) {
+ throw Object.assign(new Error("configured Performance Tracing data is missing"), { status: 404 });
+ }
+ const payload = JSON.parse(readTextFileWithLimit(
+ bindingPath,
+ maxOvenDataBytes,
+ "Performance Tracing Oven data",
+ ));
+ assertPerformanceTracingProvenanceCurrent(payload, bindingPath, maxOvenDataBytes);
+ assertPerformanceTracingData(payload);
+ return { ovenId: "performance-tracing", path: bindingPath, payload, validated: true };
+ },
+});
+
+export function assertPerformanceTracingProvenanceCurrent(payload, bindingPath, maxBytes = 512 * 1024 * 1024) {
+ const files = payload?.provenance?.files;
+ if (!files || typeof files !== "object" || !Object.keys(files).length) {
+ throw staleError("Performance Tracing report has no source provenance to validate.");
+ }
+ const paths = Object.keys(files);
+ for (const path of paths) {
+ if (!path || isAbsolute(path) || path.split(/[\\/]/u).includes("..")) {
+ throw staleError("Performance Tracing provenance contains an unsafe project path: " + path);
+ }
+ }
+ const projectRoot = findProjectRoot(bindingPath, paths);
+ if (!projectRoot) throw staleError("Performance Tracing report is stale: its project inputs cannot be found beside the bound report.");
+ const changed = [];
+ for (const path of paths) {
+ const absolute = resolve(projectRoot, path);
+ const stat = safeStat(absolute);
+ const expected = files[path];
+ if (!stat?.isFile()) {
+ changed.push(path + " (missing)");
+ continue;
+ }
+ if (stat.size > maxBytes) throw staleError("Performance Tracing provenance input exceeds the read limit: " + path);
+ const digest = createHash("sha256").update(readFileSync(absolute)).digest("hex");
+ if (stat.size !== Number(expected?.bytes) || digest !== expected?.sha256) changed.push(path);
+ }
+ if (changed.length) {
+ throw staleError("Performance Tracing report is stale; rerun the configured trace command. Changed inputs: " + changed.join(", "));
+ }
+ return payload;
+}
+
+function findProjectRoot(bindingPath, paths) {
+ let candidate = dirname(resolve(bindingPath));
+ while (true) {
+ if (paths.every((path) => {
+ const absolute = resolve(candidate, path);
+ const inside = relative(candidate, absolute);
+ return inside && !inside.startsWith("..") && !isAbsolute(inside) && safeStat(absolute)?.isFile();
+ })) return candidate;
+ const parent = dirname(candidate);
+ if (parent === candidate) return null;
+ candidate = parent;
+ }
+}
+
+function staleError(message) {
+ return Object.assign(new Error(message), { status: 409 });
+}
+
+registerOvenHandler("performance-tracing", performanceTracingHandler);
diff --git a/ovens/performance-tracing/instructions.md b/ovens/performance-tracing/instructions.md
new file mode 100644
index 0000000..5edef77
--- /dev/null
+++ b/ovens/performance-tracing/instructions.md
@@ -0,0 +1,26 @@
+# Performance Tracing
+
+Performance Tracing renders retained browser-output timing evidence from a project-owned trace run. It shows frame pacing, synchronous step cost, budget checks, renderer trace groups, slow steps, browser identity, and source provenance without treating instrumentation as gameplay or visual-equivalence authority.
+
+## State Contract
+
+The project publishes one atomic `performance-tracing-oven@1` JSON report. The report owns capture, deterministic replay, raw Chrome trace retention, samples, machine and browser provenance, and budget evaluation. Burnlist validates and renders that normalized report; it does not execute the trace command or rewrite project evidence.
+
+The report must preserve:
+
+- canonical prepared route and scenario identity
+- browser, viewport, machine, and source-file provenance
+- startup, frame, synchronous step, trace-group, and residency measurements
+- bounded per-dispatch phase attribution with source producer and next-probe metadata
+- ranked frame spikes, residency-changing step spikes, trace hot windows, and top complete events
+- a measured optimization queue whose items name the producer, evidence, next action, and verification metrics
+- comparable-history context plus the exact command and integrity gate for the next rerun
+- every declared budget with its actual value and pass/fail result
+- raw trace and sample artifact bindings
+- explicit browser-output trust that makes no native-execution or visual-equivalence claim
+
+Missing, malformed, partially written, contradictory, or unactionable evidence is blocked. Never guess a producer and never loosen a budget to make a run green. Fix the capture boundary when observer overhead dominates; otherwise change one measured source producer, run the retained comparable command again, require the same comparison key and zero integrity violations, and keep the change only when the named acceptance metrics improve without structural regressions.
+
+## Execution Boundary
+
+The Oven is read-only. Project tooling runs traces and atomically publishes the report. Burnlist only validates and displays it.
diff --git a/package.json b/package.json
index b989bf0..80a95da 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"scripts": {
"build:dashboard": "vite build --config dashboard/vite.config.mjs",
"test:differential-testing": "node --test ovens/differential-testing/engine/differential-testing-adapter-sdk.test.mjs ovens/differential-testing/engine/differential-testing-contract.test.mjs ovens/differential-testing/engine/differential-testing-data-contract.test.mjs ovens/differential-testing/engine/differential-testing-transport-renderer.test.mjs ovens/differential-testing/engine/differential-testing-transport-server.test.mjs",
+ "test:performance-tracing": "node --test ovens/performance-tracing/engine/performance-tracing-contract.test.mjs",
"verify": "node scripts/verify.mjs",
"verify:clean": "node scripts/verify-clean.mjs",
"verify:package": "node scripts/verify-package.mjs",
diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs
index 733854d..9025ca1 100755
--- a/scripts/verify-package.mjs
+++ b/scripts/verify-package.mjs
@@ -54,6 +54,10 @@ const required = [
"ovens/differential-testing/renderer/differential-testing.css",
"ovens/differential-testing/renderer/differential-testing-progress-chart.js",
"ovens/differential-testing/renderer/differential-testing-renderer.js",
+ "ovens/performance-tracing/instructions.md",
+ "ovens/performance-tracing/detail.json",
+ "ovens/performance-tracing/engine/performance-tracing-contract.mjs",
+ "ovens/performance-tracing/engine/performance-tracing-handler.mjs",
"dashboard/dist/index.html",
];
diff --git a/scripts/verify-test-files.mjs b/scripts/verify-test-files.mjs
index 28007b3..ffedb2f 100644
--- a/scripts/verify-test-files.mjs
+++ b/scripts/verify-test-files.mjs
@@ -12,6 +12,7 @@ export const verificationTestFiles = [
"ovens/differential-testing/engine/differential-testing-contract.test.mjs",
"ovens/differential-testing/engine/differential-testing-data-contract.test.mjs",
"ovens/differential-testing/engine/differential-testing-transport-server.test.mjs",
+ "ovens/performance-tracing/engine/performance-tracing-contract.test.mjs",
"ovens/streaming-diff/engine/streaming-diff-data-contract.test.mjs",
"ovens/streaming-diff/engine/streaming-diff-capture.test.mjs",
"ovens/streaming-diff/engine/streaming-diff-capture-git.test.mjs",
diff --git a/scripts/verify.mjs b/scripts/verify.mjs
index 8ca6788..9b3e338 100755
--- a/scripts/verify.mjs
+++ b/scripts/verify.mjs
@@ -219,6 +219,7 @@ const jsFiles = [...new Set([
...walkFiles(resolve(repoRoot, "scripts"), (path) => path.endsWith(".mjs")),
...walkFiles(resolve(repoRoot, "src"), (path) => path.endsWith(".mjs")),
...walkFiles(resolve(repoRoot, "ovens/differential-testing/engine"), (path) => path.endsWith(".mjs")),
+ ...walkFiles(resolve(repoRoot, "ovens/performance-tracing/engine"), (path) => path.endsWith(".mjs")),
...walkFiles(resolve(repoRoot, "ovens/streaming-diff/engine"), (path) => path.endsWith(".mjs")),
resolve(repoRoot, "src/ovens/oven-registry.mjs"),
resolve(repoRoot, "src/ovens/built-in-handlers.mjs"),
@@ -250,6 +251,7 @@ assertSourceIncludes(".github/workflows/publish.yml", "git fetch origin main", "
assertSourceIncludes(".github/workflows/publish.yml", '"refs/tags/${VERSION}^{}"', "Publish tag verification must request annotated-tag peeled refs.");
assertSourceIncludes("src/server/burnlist-dashboard-server.mjs", "ovenId(record.ovenId);", "Burn run reads do not require the canonical ovenId.");
assertSourceIncludes("ovens/differential-testing/engine/differential-testing-handler.mjs", "assertDifferentialTestingData(payload)", "Differential Testing data is not validated at the server boundary.");
+assertSourceIncludes("ovens/performance-tracing/engine/performance-tracing-handler.mjs", "assertPerformanceTracingData(payload)", "Performance Tracing data is not validated at the server boundary.");
assertSourceIncludes("ovens/differential-testing/engine/differential-testing-handler.mjs", 'ovenName: "Differential Testing"', "Differential Testing scenarios are missing from the shared dashboard table.");
assertSourceIncludes("ovens/differential-testing/engine/differential-testing-handler.mjs", "queryDifferentialTestingFieldPage", "Differential Testing server is missing bounded field-page transport.");
assertSourceExcludes("src/server/burnlist-dashboard-server.mjs", 'id === "differential-testing"', "Dashboard server still hardcodes the Differential Testing Oven.");
@@ -370,9 +372,10 @@ assertSourceExcludes("src/server/burnlist-dashboard-server.mjs", '"/targets"', "
assertSourceExcludes("dashboard/src/App.tsx", '"/targets"', "React dashboard still exposes the removed Targets route.");
assertSourceExcludes("src/ovens/oven-contract.mjs", '"target"', "Oven contract still accepts the removed Target widget.");
assertSkillSet(repoRoot, ["burnlist"]);
-assertBuiltInOvenSet(repoRoot, ["checklist", "differential-testing", "streaming-diff"]);
+assertBuiltInOvenSet(repoRoot, ["checklist", "differential-testing", "performance-tracing", "streaming-diff"]);
assertBuiltInOven(repoRoot, "checklist", "Checklist");
assertBuiltInOven(repoRoot, "differential-testing", "Differential Testing");
+assertBuiltInOven(repoRoot, "performance-tracing", "Performance Tracing");
assertBuiltInOven(repoRoot, "streaming-diff", "Streaming Diff");
assertDifferentialTestingContractAssets();
assertPublishablePackage();
diff --git a/src/ovens/built-in-handlers.mjs b/src/ovens/built-in-handlers.mjs
index 5d0ee77..84c56cc 100644
--- a/src/ovens/built-in-handlers.mjs
+++ b/src/ovens/built-in-handlers.mjs
@@ -1,3 +1,4 @@
import "./handlers/checklist.mjs";
import "../../ovens/differential-testing/engine/differential-testing-handler.mjs";
+import "../../ovens/performance-tracing/engine/performance-tracing-handler.mjs";
import "../../ovens/streaming-diff/engine/streaming-diff-handler.mjs";