fix: close performance evidence and budget gates
This commit is contained in:
27
config/performance-budget-evidence.json
Normal file
27
config/performance-budget-evidence.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"schema": "portfolio-runtime-budget-evidence/v1",
|
||||||
|
"peakRequests": { "imageScene": 0, "video": 0 },
|
||||||
|
"tiers": [
|
||||||
|
{
|
||||||
|
"tier": "low",
|
||||||
|
"textures": [
|
||||||
|
{ "id": "route-sign-canvas", "width": 768, "height": 192, "bytesPerPixel": 4, "layers": 1, "mipmapped": true }
|
||||||
|
],
|
||||||
|
"geometryBytes": 1048576,
|
||||||
|
"renderTargets": [
|
||||||
|
{ "id": "default-framebuffer", "width": 488, "height": 1055, "bytesPerPixel": 4, "attachments": 2, "samples": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": "high",
|
||||||
|
"textures": [
|
||||||
|
{ "id": "route-sign-canvas", "width": 768, "height": 192, "bytesPerPixel": 4, "layers": 1, "mipmapped": true }
|
||||||
|
],
|
||||||
|
"geometryBytes": 2097152,
|
||||||
|
"renderTargets": [
|
||||||
|
{ "id": "default-framebuffer", "width": 2160, "height": 1350, "bytesPerPixel": 4, "attachments": 2, "samples": 4 },
|
||||||
|
{ "id": "post-processing-target", "width": 2160, "height": 1350, "bytesPerPixel": 4, "attachments": 2, "samples": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ This runbook separates executable developer evidence from the two physical relea
|
|||||||
|
|
||||||
## Automated build and transfer gate
|
## Automated build and transfer gate
|
||||||
|
|
||||||
Run `npm run build:preview`. The build ends with `npm run check:budgets`, which classifies every shipped file exactly once, measures Brotli transfer bytes (or raw bytes for already-compressed formats), verifies that the live Three.js scene remains behind a dynamic import, and applies the exact section 14 thresholds. Unknown files fail closed.
|
Run `npm run build:preview`. The build ends with `npm run check:budgets`, which classifies every shipped file exactly once, measures Brotli transfer bytes (or raw bytes for already-compressed formats), verifies that the live Three.js scene remains behind a dynamic import, and applies the exact section 14 thresholds. Unknown files fail closed. Every non-empty manifest media file is hash-checked and deterministically decoded: Sharp verifies image dimensions and `ffprobe` verifies video dimensions and duration against the manifest. Missing probe evidence, disagreement, or an unprobeable asset fails the gate.
|
||||||
|
|
||||||
|
`config/performance-budget-evidence.json` is the auditable allocation declaration. The checker derives texture bytes as `width × height × bytes-per-pixel × layers × 4/3` for mipmaps, render-target bytes as `width × height × bytes-per-pixel × attachments × samples`, adds declared geometry buffer bytes and the fixed 32 MB allowance, and enforces the Low/High GPU caps. It also enforces peak request concurrency and the 2048-pixel mobile texture bound. High-tier-only chunks use the `high-tier-*`/`environment-high-*` bucket and are capped separately. Any scene allocation or request-scheduling change must update this declaration in the same change; incomplete allocations fail closed.
|
||||||
|
|
||||||
The current empty-media preview measures approximately 7.8 KB for HTML plus critical CSS, 61.8 KB for initial application JavaScript, and 114.2 KB for the deferred first live scene. These values are build observations, not permanent allowances.
|
The current empty-media preview measures approximately 7.8 KB for HTML plus critical CSS, 61.8 KB for initial application JavaScript, and 114.2 KB for the deferred first live scene. These values are build observations, not permanent allowances.
|
||||||
|
|
||||||
@@ -32,7 +34,19 @@ They prove that the harness and interactions execute. They do not make a release
|
|||||||
|
|
||||||
Use only the Pixel 6 8 GB / Chromium Mobile Low and M1 MacBook Air 8 GB / Chromium Desktop High reference classes, or hardware with the same CPU/GPU and RAM. Fix both displays at 60 Hz; require battery above 50%, power saving off, a cool start, no foreground competition, page zoom 100%, full motion, Save-Data off, and the exact network shaping from section 14. Record OS/browser/display versions in the private lab evidence, not in shipped artifacts.
|
Use only the Pixel 6 8 GB / Chromium Mobile Low and M1 MacBook Air 8 GB / Chromium Desktop High reference classes, or hardware with the same CPU/GPU and RAM. Fix both displays at 60 Hz; require battery above 50%, power saving off, a cool start, no foreground competition, page zoom 100%, full motion, Save-Data off, and the exact network shaping from section 14. Record OS/browser/display versions in the private lab evidence, not in shipped artifacts.
|
||||||
|
|
||||||
For each profile, run five fresh temporary profiles from `about:blank`. Clear service workers, Cache Storage, HTTP cache, cookies, local/session storage, and IndexedDB before every run. Set `PORTFOLIO_REFERENCE_LAB=1` so cadence and all per-run budgets become assertions. A warm-up median outside 16.667 ±1 ms invalidates the run.
|
For each profile, run five fresh temporary profiles from `about:blank`. Clear service workers, Cache Storage, HTTP cache, cookies, local/session storage, and IndexedDB before every run.
|
||||||
|
|
||||||
|
Reference mode is fail-closed. Create one private `portfolio-reference-run-attestation/v1` JSON file per run with the exact pinned device, Chromium/OS/display, viewport, network, run index/ID, and every environmental assertion recorded. Keep device metadata outside the public repository. Compute the SHA-256 of the exact file bytes, then run one trace with all three variables set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORTFOLIO_REFERENCE_LAB=1 \
|
||||||
|
PORTFOLIO_REFERENCE_ATTESTATION_PATH=/private/evidence/desktop-run-1.json \
|
||||||
|
PORTFOLIO_REFERENCE_ATTESTATION_SHA256=<64-lowercase-hex-digest> \
|
||||||
|
npx playwright test tests/performance/drive-trace.spec.ts \
|
||||||
|
--project=performance-desktop --workers=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference mode rejects missing or extra fields, digest mismatch, wrong hardware/network/profile, failed setup assertions, or disagreement with the browser-observed viewport, visibility, reduced-motion, Save-Data, zoom, and Chromium version. Only a validated attestation can produce `gatingReferenceResult: true`; ordinary developer runs always remain false. A warm-up median outside 16.667 ±1 ms or any per-run budget failure invalidates the run.
|
||||||
|
|
||||||
Aggregate exactly five reviewed JSON paths:
|
Aggregate exactly five reviewed JSON paths:
|
||||||
|
|
||||||
@@ -40,7 +54,9 @@ Aggregate exactly five reviewed JSON paths:
|
|||||||
npm run aggregate:performance -- run-1.json run-2.json run-3.json run-4.json run-5.json
|
npm run aggregate:performance -- run-1.json run-2.json run-3.json run-4.json run-5.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Aggregation fails unless all five are marked reference evidence, every per-run gate passes, worst/median LCP are at most 3.0/2.5 seconds, and worst/median TBT are at most 300/200 ms. Do not commit result files until their metadata has passed privacy review.
|
Aggregation fails unless the set contains unique authenticated run IDs, digests, and indexes 1–5 for one profile, all five are marked reference evidence, every per-run gate passes, worst/median LCP are at most 3.0/2.5 seconds, and worst/median TBT are at most 300/200 ms. Run and aggregate the other profile separately. Do not commit result files until their metadata has passed privacy review.
|
||||||
|
|
||||||
|
The former High DPR cap of 2 produced a conservative 242 MB estimate at 1440×900 once the antialiased framebuffer, post target, route-sign texture, geometry, and fixed allowance were counted. High is therefore capped at DPR 1.5. The executable declaration now derives 153,078,016 bytes (about 146 MB) for High and 39,508,160 bytes (about 37.7 MB) for Low, below the exact 192/96 MB limits.
|
||||||
|
|
||||||
## Physical ten-minute protocol
|
## Physical ten-minute protocol
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
import { brotliCompressSync, constants as zlibConstants } from 'node:zlib';
|
import { brotliCompressSync, constants as zlibConstants } from 'node:zlib';
|
||||||
import { readFile, readdir, stat } from 'node:fs/promises';
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||||
import { relative, resolve, sep } from 'node:path';
|
import { relative, resolve, sep } from 'node:path';
|
||||||
import { classifyArtifact, evaluateBudgetTotals, PERFORMANCE_BUDGETS } from './lib/bundle-budgets.mjs';
|
import sharp from 'sharp';
|
||||||
|
import { fileTypeFromBuffer } from 'file-type';
|
||||||
|
import { probeVideo } from './lib/video-frames.mjs';
|
||||||
|
import {
|
||||||
|
classifyArtifact,
|
||||||
|
evaluateBudgetTotals,
|
||||||
|
evaluateMediaAssetBudget,
|
||||||
|
evaluateRuntimeBudgetEvidence,
|
||||||
|
} from './lib/bundle-budgets.mjs';
|
||||||
|
|
||||||
const root = resolve(process.argv[2] ?? 'dist');
|
const root = resolve(process.argv[2] ?? 'dist');
|
||||||
if (!(await stat(root)).isDirectory()) throw new Error('Bundle budget target must be a directory.');
|
if (!(await stat(root)).isDirectory()) throw new Error('Bundle budget target must be a directory.');
|
||||||
@@ -21,18 +30,74 @@ const listFiles = async (directory) => {
|
|||||||
|
|
||||||
const compressedSize = (path, bytes) => (/\.(?:avif|br|gif|jpe?g|mp4|png|webm|webp|woff2?)$/iu.test(path)
|
const compressedSize = (path, bytes) => (/\.(?:avif|br|gif|jpe?g|mp4|png|webm|webp|woff2?)$/iu.test(path)
|
||||||
? bytes.byteLength
|
? bytes.byteLength
|
||||||
: brotliCompressSync(bytes, {
|
: brotliCompressSync(bytes, { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 } }).byteLength);
|
||||||
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 },
|
|
||||||
}).byteLength);
|
|
||||||
|
|
||||||
|
const readJson = async (path, label) => {
|
||||||
|
try { return JSON.parse(await readFile(path, 'utf8')); } catch { throw new Error(`${label} must be valid JSON.`); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const manifest = await readJson(resolve('content/public/asset-manifest.json'), 'Public asset manifest');
|
||||||
|
const content = await readJson(resolve('content/public/portfolio-content.json'), 'Portfolio content');
|
||||||
|
const runtimeEvidence = await readJson(resolve('config/performance-budget-evidence.json'), 'Runtime budget evidence');
|
||||||
|
if (!Array.isArray(manifest?.assets)) throw new Error('Public asset manifest is invalid.');
|
||||||
|
|
||||||
|
const assetClassById = new Map();
|
||||||
|
for (const project of [...(content.projects?.selected ?? []), ...(content.projects?.additional ?? [])]) {
|
||||||
|
for (const media of project.media ?? []) {
|
||||||
|
assetClassById.set(media.assetId, media.kind === 'chart' ? 'chart' : 'project-still');
|
||||||
|
if (media.posterAssetId) assetClassById.set(media.posterAssetId, 'project-still');
|
||||||
|
if (media.fallbackAssetId) assetClassById.set(media.fallbackAssetId, 'project-still');
|
||||||
|
}
|
||||||
|
for (const assetId of project.assetIds ?? []) if (!assetClassById.has(assetId)) assetClassById.set(assetId, 'project-still');
|
||||||
|
}
|
||||||
|
for (const group of content.toolkit ?? []) for (const technology of group.technologies ?? []) {
|
||||||
|
if (technology.assetId) assetClassById.set(technology.assetId, 'project-still');
|
||||||
|
}
|
||||||
|
if (content.metadata?.socialPreviewAssetId) assetClassById.set(content.metadata.socialPreviewAssetId, 'project-still');
|
||||||
|
|
||||||
|
const manifestByPath = new Map(manifest.assets.map((asset) => [`assets/${asset.fileName}`, asset]));
|
||||||
const records = [];
|
const records = [];
|
||||||
|
const mediaViolations = [];
|
||||||
for (const absolute of await listFiles(root)) {
|
for (const absolute of await listFiles(root)) {
|
||||||
const path = relative(root, absolute).split(sep).join('/');
|
const path = relative(root, absolute).split(sep).join('/');
|
||||||
const bytes = await readFile(absolute);
|
const bytes = await readFile(absolute);
|
||||||
const source = /\.(?:css|html|js|json|txt|xml)$/u.test(path) ? bytes.toString('utf8') : '';
|
const source = /\.(?:css|html|js|json|txt|xml)$/u.test(path) ? bytes.toString('utf8') : '';
|
||||||
const bucket = classifyArtifact(path, source);
|
const manifestAsset = manifestByPath.get(path);
|
||||||
|
let bucket;
|
||||||
|
if (manifestAsset !== undefined) {
|
||||||
|
const digest = createHash('sha256').update(bytes).digest('hex');
|
||||||
|
if (digest !== manifestAsset.sha256) throw new Error(`Published asset hash mismatch: ${path}`);
|
||||||
|
const mediaType = manifestAsset.mediaType;
|
||||||
|
const detected = await fileTypeFromBuffer(bytes);
|
||||||
|
const detectedMediaType = detected?.mime ?? (/^\s*<svg[\s>]/u.test(bytes.toString('utf8', 0, Math.min(bytes.length, 256))) ? 'image/svg+xml' : null);
|
||||||
|
if (detectedMediaType !== mediaType) throw new Error(`Manifest media type disagrees with deterministic probing: ${manifestAsset.id}`);
|
||||||
|
let probe;
|
||||||
|
if (mediaType?.startsWith('video/')) {
|
||||||
|
const video = await probeVideo(absolute, { allowAudio: false });
|
||||||
|
probe = { mediaType, width: video.width, height: video.height, durationMs: Math.round(video.duration * 1_000) };
|
||||||
|
bucket = 'project-video';
|
||||||
|
} else if (mediaType?.startsWith('image/')) {
|
||||||
|
const image = await sharp(absolute, { failOn: 'error', limitInputPixels: 50_000_000 }).metadata();
|
||||||
|
probe = { mediaType, width: image.width, height: image.height, durationMs: null };
|
||||||
|
bucket = assetClassById.get(manifestAsset.id) === 'chart' ? 'chart' : 'project-still';
|
||||||
|
} else {
|
||||||
|
throw new Error(`Manifest asset has unsupported media type: ${manifestAsset.id}`);
|
||||||
|
}
|
||||||
|
mediaViolations.push(...evaluateMediaAssetBudget(
|
||||||
|
manifestAsset,
|
||||||
|
probe,
|
||||||
|
compressedSize(path, bytes),
|
||||||
|
bucket === 'chart' ? 'chart' : 'project-still',
|
||||||
|
).violations);
|
||||||
|
} else {
|
||||||
|
bucket = classifyArtifact(path, source);
|
||||||
|
if (['project-video', 'project-still', 'chart'].includes(bucket)) {
|
||||||
|
throw new Error(`Project media must be present in the public asset manifest: ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
records.push({ path, bucket, bytes: bytes.byteLength, transferBytes: compressedSize(path, bytes) });
|
records.push({ path, bucket, bytes: bytes.byteLength, transferBytes: compressedSize(path, bytes) });
|
||||||
}
|
}
|
||||||
|
for (const path of manifestByPath.keys()) if (!records.some((record) => record.path === path)) throw new Error(`Manifest asset is missing from the build: ${path}`);
|
||||||
|
|
||||||
const sum = (bucket) => records.filter((record) => record.bucket === bucket).reduce((total, record) => total + record.transferBytes, 0);
|
const sum = (bucket) => records.filter((record) => record.bucket === bucket).reduce((total, record) => total + record.transferBytes, 0);
|
||||||
const totals = {
|
const totals = {
|
||||||
@@ -42,25 +107,10 @@ const totals = {
|
|||||||
initialFonts: sum('initial-fonts'),
|
initialFonts: sum('initial-fonts'),
|
||||||
firstLiveScene: sum('first-live-scene'),
|
firstLiveScene: sum('first-live-scene'),
|
||||||
deferredProjectVisuals: sum('project-still') + sum('chart'),
|
deferredProjectVisuals: sum('project-still') + sum('chart'),
|
||||||
deferredNonVideo: sum('first-live-scene') + sum('project-still') + sum('chart'),
|
highTierExtras: sum('high-tier-extras'),
|
||||||
|
deferredNonVideo: sum('first-live-scene') + sum('high-tier-extras') + sum('project-still') + sum('chart'),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const record of records.filter((item) => item.bucket === 'project-video')) {
|
|
||||||
if (record.transferBytes > PERFORMANCE_BUDGETS.projectVideo) {
|
|
||||||
throw new Error(`Project video exceeds its transfer budget: ${record.path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const record of records.filter((item) => item.bucket === 'project-still')) {
|
|
||||||
if (record.transferBytes > PERFORMANCE_BUDGETS.projectStill) {
|
|
||||||
throw new Error(`Project still exceeds its transfer budget: ${record.path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const record of records.filter((item) => item.bucket === 'chart')) {
|
|
||||||
if (record.transferBytes > PERFORMANCE_BUDGETS.chart) {
|
|
||||||
throw new Error(`Chart exceeds its transfer budget: ${record.path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = records.find((record) => record.bucket === 'initial-application-js');
|
const entry = records.find((record) => record.bucket === 'initial-application-js');
|
||||||
const scene = records.find((record) => record.bucket === 'first-live-scene');
|
const scene = records.find((record) => record.bucket === 'first-live-scene');
|
||||||
if (entry === undefined || scene === undefined) throw new Error('Both initial application and deferred live-scene chunks are required.');
|
if (entry === undefined || scene === undefined) throw new Error('Both initial application and deferred live-scene chunks are required.');
|
||||||
@@ -68,13 +118,15 @@ const entrySource = await readFile(resolve(root, entry.path), 'utf8');
|
|||||||
if (!/import\([^)]*environment-/u.test(entrySource)) throw new Error('The live scene is not separated behind a dynamic import.');
|
if (!/import\([^)]*environment-/u.test(entrySource)) throw new Error('The live scene is not separated behind a dynamic import.');
|
||||||
if (/WebGLRenderer|WebGLProgram|ShaderMaterial/u.test(entrySource)) throw new Error('Three.js/live-scene code leaked into the reduced-motion initial chunk.');
|
if (/WebGLRenderer|WebGLProgram|ShaderMaterial/u.test(entrySource)) throw new Error('Three.js/live-scene code leaked into the reduced-motion initial chunk.');
|
||||||
|
|
||||||
const result = evaluateBudgetTotals(totals);
|
const runtime = evaluateRuntimeBudgetEvidence(runtimeEvidence);
|
||||||
|
const violations = [...evaluateBudgetTotals(totals).violations, ...mediaViolations, ...runtime.violations];
|
||||||
const report = {
|
const report = {
|
||||||
schema: 'portfolio-bundle-budget/v1',
|
schema: 'portfolio-bundle-budget/v1',
|
||||||
compression: 'brotli-quality-11-or-raw-already-compressed',
|
compression: 'brotli-quality-11-or-raw-already-compressed',
|
||||||
totals,
|
totals,
|
||||||
|
runtime: { peakRequests: runtimeEvidence.peakRequests, tiers: runtime.tiers },
|
||||||
records: records.sort((left, right) => left.path.localeCompare(right.path)),
|
records: records.sort((left, right) => left.path.localeCompare(right.path)),
|
||||||
violations: result.violations,
|
violations,
|
||||||
};
|
};
|
||||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||||
if (result.violations.length > 0) process.exitCode = 1;
|
if (violations.length > 0) process.exitCode = 1;
|
||||||
|
|||||||
@@ -3,3 +3,13 @@ export function classifyArtifact(path: string, source?: string): string;
|
|||||||
export function evaluateBudgetTotals(totals: Readonly<Record<string, number>>): {
|
export function evaluateBudgetTotals(totals: Readonly<Record<string, number>>): {
|
||||||
violations: Array<{ budget: string; actual: number; threshold: number }>;
|
violations: Array<{ budget: string; actual: number; threshold: number }>;
|
||||||
};
|
};
|
||||||
|
export function evaluateMediaAssetBudget(
|
||||||
|
asset: Readonly<Record<string, unknown>>,
|
||||||
|
probe: Readonly<Record<string, unknown>> | null,
|
||||||
|
transferBytes: number,
|
||||||
|
assetClass?: 'chart' | 'project-still',
|
||||||
|
): { violations: Array<{ budget: string; actual: number; threshold: number; id?: string }> };
|
||||||
|
export function evaluateRuntimeBudgetEvidence(evidence: Readonly<Record<string, unknown>>): {
|
||||||
|
tiers: Array<{ tier: string; textureBytes: number; geometryBytes: number; renderTargetBytes: number; runtimeAllowanceBytes: number; totalBytes: number }>;
|
||||||
|
violations: Array<{ budget: string; actual: number; threshold: number; id?: string }>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const classifyArtifact = (path, source = '') => {
|
|||||||
if (path === 'index.html') return 'initial-html';
|
if (path === 'index.html') return 'initial-html';
|
||||||
if (/^assets\/index-[^/]+\.css$/u.test(path)) return 'critical-css';
|
if (/^assets\/index-[^/]+\.css$/u.test(path)) return 'critical-css';
|
||||||
if (/^assets\/index-[^/]+\.js$/u.test(path) && /import\(/u.test(source)) return 'initial-application-js';
|
if (/^assets\/index-[^/]+\.js$/u.test(path) && /import\(/u.test(source)) return 'initial-application-js';
|
||||||
|
if (/^assets\/(?:environment-high|high-tier)-[^/]+\.(?:js|avif|webp|png|jpe?g)$/u.test(path)) return 'high-tier-extras';
|
||||||
if (/^assets\/environment-[^/]+\.js$/u.test(path)) return 'first-live-scene';
|
if (/^assets\/environment-[^/]+\.js$/u.test(path)) return 'first-live-scene';
|
||||||
if (/\.(?:woff2?|ttf|otf)$/u.test(path)) return 'initial-fonts';
|
if (/\.(?:woff2?|ttf|otf)$/u.test(path)) return 'initial-fonts';
|
||||||
if (/^assets\/hero-[^/]+\.(?:avif|webp|png|jpe?g)$/u.test(path)) return 'hero-poster';
|
if (/^assets\/hero-[^/]+\.(?:avif|webp|png|jpe?g)$/u.test(path)) return 'hero-poster';
|
||||||
@@ -42,6 +43,7 @@ const thresholdByTotal = Object.freeze({
|
|||||||
firstLiveScene: PERFORMANCE_BUDGETS.firstLiveScene,
|
firstLiveScene: PERFORMANCE_BUDGETS.firstLiveScene,
|
||||||
deferredProjectVisuals: PERFORMANCE_BUDGETS.deferredProjectVisuals,
|
deferredProjectVisuals: PERFORMANCE_BUDGETS.deferredProjectVisuals,
|
||||||
deferredNonVideo: PERFORMANCE_BUDGETS.deferredNonVideo,
|
deferredNonVideo: PERFORMANCE_BUDGETS.deferredNonVideo,
|
||||||
|
highTierExtras: PERFORMANCE_BUDGETS.highTierExtras,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const evaluateBudgetTotals = (totals) => ({
|
export const evaluateBudgetTotals = (totals) => ({
|
||||||
@@ -49,3 +51,85 @@ export const evaluateBudgetTotals = (totals) => ({
|
|||||||
.filter(([budget, threshold]) => (totals[budget] ?? 0) > threshold)
|
.filter(([budget, threshold]) => (totals[budget] ?? 0) > threshold)
|
||||||
.map(([budget, threshold]) => ({ budget, actual: totals[budget], threshold })),
|
.map(([budget, threshold]) => ({ budget, actual: totals[budget], threshold })),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const violation = (budget, actual, threshold, id) => ({ budget, actual, threshold, ...(id === undefined ? {} : { id }) });
|
||||||
|
const finitePositiveInteger = (value) => Number.isInteger(value) && value > 0;
|
||||||
|
|
||||||
|
export const evaluateMediaAssetBudget = (asset, probe, transferBytes, assetClass = undefined) => {
|
||||||
|
if (probe === null || typeof probe !== 'object') throw new Error(`Media probe evidence is required for ${asset?.id ?? 'unknown asset'}.`);
|
||||||
|
if (!finitePositiveInteger(asset?.width) || !finitePositiveInteger(asset?.height)
|
||||||
|
|| !finitePositiveInteger(probe.width) || !finitePositiveInteger(probe.height)) {
|
||||||
|
throw new Error(`Verified dimensions are required for ${asset?.id ?? 'unknown asset'}.`);
|
||||||
|
}
|
||||||
|
if (asset.width !== probe.width || asset.height !== probe.height || asset.mediaType !== probe.mediaType) {
|
||||||
|
throw new Error(`Manifest and probe evidence disagree for ${asset.id}.`);
|
||||||
|
}
|
||||||
|
const violations = [];
|
||||||
|
if (asset.mediaType.startsWith('video/')) {
|
||||||
|
if (!finitePositiveInteger(asset.durationMs) || !finitePositiveInteger(probe.durationMs)
|
||||||
|
|| Math.abs(asset.durationMs - probe.durationMs) > 50) {
|
||||||
|
throw new Error(`Verified duration is required for ${asset.id}.`);
|
||||||
|
}
|
||||||
|
if (transferBytes > PERFORMANCE_BUDGETS.projectVideo) violations.push(violation('projectVideo', transferBytes, PERFORMANCE_BUDGETS.projectVideo, asset.id));
|
||||||
|
if (probe.durationMs > PERFORMANCE_BUDGETS.projectVideoDurationSeconds * 1_000) violations.push(violation('projectVideoDurationSeconds', probe.durationMs / 1_000, PERFORMANCE_BUDGETS.projectVideoDurationSeconds, asset.id));
|
||||||
|
if (probe.width > PERFORMANCE_BUDGETS.projectVideoWidth) violations.push(violation('projectVideoWidth', probe.width, PERFORMANCE_BUDGETS.projectVideoWidth, asset.id));
|
||||||
|
if (probe.height > PERFORMANCE_BUDGETS.projectVideoHeight) violations.push(violation('projectVideoHeight', probe.height, PERFORMANCE_BUDGETS.projectVideoHeight, asset.id));
|
||||||
|
} else {
|
||||||
|
if (asset.durationMs !== null || probe.durationMs !== null) throw new Error(`Still media cannot declare duration for ${asset.id}.`);
|
||||||
|
const budget = assetClass === 'chart' ? 'chart' : 'projectStill';
|
||||||
|
const threshold = PERFORMANCE_BUDGETS[budget];
|
||||||
|
if (transferBytes > threshold) violations.push(violation(budget, transferBytes, threshold, asset.id));
|
||||||
|
}
|
||||||
|
return { violations };
|
||||||
|
};
|
||||||
|
|
||||||
|
const allocationBytes = (allocation, fields, label) => {
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!finitePositiveInteger(allocation?.[field])) throw new Error(`${label} ${allocation?.id ?? 'unknown'} has invalid ${field}.`);
|
||||||
|
}
|
||||||
|
return fields.reduce((total, field) => total * allocation[field], 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const evaluateRuntimeBudgetEvidence = (evidence) => {
|
||||||
|
if (evidence?.schema !== 'portfolio-runtime-budget-evidence/v1' || !Array.isArray(evidence.tiers)) {
|
||||||
|
throw new Error('Runtime budget evidence must use portfolio-runtime-budget-evidence/v1.');
|
||||||
|
}
|
||||||
|
const tierNames = evidence.tiers.map((tier) => tier?.tier).sort();
|
||||||
|
if (JSON.stringify(tierNames) !== JSON.stringify(['high', 'low'])) {
|
||||||
|
throw new Error('Runtime evidence must contain exactly one Low and High tier allocation ledger.');
|
||||||
|
}
|
||||||
|
const imageScene = evidence.peakRequests?.imageScene;
|
||||||
|
const video = evidence.peakRequests?.video;
|
||||||
|
if (!Number.isInteger(imageScene) || imageScene < 0 || !Number.isInteger(video) || video < 0) {
|
||||||
|
throw new Error('Runtime request concurrency evidence is incomplete.');
|
||||||
|
}
|
||||||
|
const violations = [];
|
||||||
|
if (imageScene > PERFORMANCE_BUDGETS.imageSceneConcurrency) violations.push(violation('imageSceneConcurrency', imageScene, PERFORMANCE_BUDGETS.imageSceneConcurrency));
|
||||||
|
if (video > PERFORMANCE_BUDGETS.videoConcurrency) violations.push(violation('videoConcurrency', video, PERFORMANCE_BUDGETS.videoConcurrency));
|
||||||
|
const tiers = evidence.tiers.map((tier) => {
|
||||||
|
if (!['low', 'high'].includes(tier?.tier) || !Array.isArray(tier.textures) || !Array.isArray(tier.renderTargets)
|
||||||
|
|| !Number.isInteger(tier.geometryBytes) || tier.geometryBytes < 0) {
|
||||||
|
throw new Error('Runtime tier allocation evidence is incomplete.');
|
||||||
|
}
|
||||||
|
let textureBytes = 0;
|
||||||
|
for (const texture of tier.textures) {
|
||||||
|
const base = allocationBytes(texture, ['width', 'height', 'bytesPerPixel', 'layers'], 'Texture');
|
||||||
|
if (typeof texture.mipmapped !== 'boolean') throw new Error(`Texture ${texture?.id ?? 'unknown'} is missing mipmap evidence.`);
|
||||||
|
textureBytes += Math.ceil(base * (texture.mipmapped ? 4 / 3 : 1));
|
||||||
|
if (tier.tier === 'low' && (texture.width > PERFORMANCE_BUDGETS.mobileTextureDimension || texture.height > PERFORMANCE_BUDGETS.mobileTextureDimension)) {
|
||||||
|
violations.push(violation('mobileTextureDimension', Math.max(texture.width, texture.height), PERFORMANCE_BUDGETS.mobileTextureDimension, texture.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let renderTargetBytes = 0;
|
||||||
|
for (const target of tier.renderTargets) {
|
||||||
|
renderTargetBytes += allocationBytes(target, ['width', 'height', 'bytesPerPixel', 'attachments', 'samples'], 'Render target');
|
||||||
|
}
|
||||||
|
const runtimeAllowanceBytes = 32 * 1024 * 1024;
|
||||||
|
const totalBytes = textureBytes + tier.geometryBytes + renderTargetBytes + runtimeAllowanceBytes;
|
||||||
|
const budget = tier.tier === 'low' ? 'lowTierGpuEstimate' : 'highTierGpuEstimate';
|
||||||
|
const threshold = PERFORMANCE_BUDGETS[budget];
|
||||||
|
if (totalBytes > threshold) violations.push(violation(budget, totalBytes, threshold));
|
||||||
|
return { tier: tier.tier, textureBytes, geometryBytes: tier.geometryBytes, renderTargetBytes, runtimeAllowanceBytes, totalBytes };
|
||||||
|
});
|
||||||
|
return { tiers, violations };
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ export const aggregateFiveDriveTraces = (runs) => {
|
|||||||
if (typeof profile !== 'string' || runs.some((run) => run?.schema !== 'drive-trace.v1' || run.profile !== profile)) {
|
if (typeof profile !== 'string' || runs.some((run) => run?.schema !== 'drive-trace.v1' || run.profile !== profile)) {
|
||||||
throw new Error('All runs must use one drive-trace.v1 profile.');
|
throw new Error('All runs must use one drive-trace.v1 profile.');
|
||||||
}
|
}
|
||||||
|
const identities = runs.map((run) => run.referenceRun);
|
||||||
|
const indexes = identities.map((identity) => identity?.runIndex).sort((left, right) => left - right);
|
||||||
|
if (JSON.stringify(indexes) !== JSON.stringify([1, 2, 3, 4, 5])) {
|
||||||
|
throw new Error('Reference evidence must contain unique run indexes 1 through 5.');
|
||||||
|
}
|
||||||
|
const runIds = new Set(identities.map((identity) => identity?.runId));
|
||||||
|
const digests = new Set(identities.map((identity) => identity?.attestationDigest));
|
||||||
|
if (runIds.size !== 5 || digests.size !== 5 || identities.some((identity) => !/^[a-f0-9]{64}$/u.test(identity?.attestationDigest ?? ''))) {
|
||||||
|
throw new Error('Reference evidence must contain five unique authenticated identities.');
|
||||||
|
}
|
||||||
const lcp = runs.map((run) => run.metrics.lcp);
|
const lcp = runs.map((run) => run.metrics.lcp);
|
||||||
const tbt = runs.map((run) => run.metrics.tbt);
|
const tbt = runs.map((run) => run.metrics.tbt);
|
||||||
const everyPerRunBudgetPass = runs.every((run) => Object.values(run.budgetResults).every(Boolean));
|
const everyPerRunBudgetPass = runs.every((run) => Object.values(run.budgetResults).every(Boolean));
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface RenderQualitySettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const settings: Readonly<Record<LiveQualityTier, Omit<RenderQualitySettings, 'pixelRatio'> & { dprCap: number }>> = {
|
const settings: Readonly<Record<LiveQualityTier, Omit<RenderQualitySettings, 'pixelRatio'> & { dprCap: number }>> = {
|
||||||
high: { dprCap: 2, terrainSegments: 96, particleCount: 384, postEffects: true },
|
high: { dprCap: 1.5, terrainSegments: 96, particleCount: 384, postEffects: true },
|
||||||
medium: { dprCap: 1.5, terrainSegments: 64, particleCount: 192, postEffects: true },
|
medium: { dprCap: 1.5, terrainSegments: 64, particleCount: 192, postEffects: true },
|
||||||
low: { dprCap: 1.25, terrainSegments: 32, particleCount: 64, postEffects: false },
|
low: { dprCap: 1.25, terrainSegments: 32, particleCount: 64, postEffects: false },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { aggregateFiveDriveTraces } from '../../scripts/lib/performance-aggregation.mjs';
|
import { aggregateFiveDriveTraces } from '../../scripts/lib/performance-aggregation.mjs';
|
||||||
|
|
||||||
const run = (lcp: number, tbt: number, gatingReferenceResult = true) => ({
|
const run = (lcp: number, tbt: number, runIndex: number, gatingReferenceResult = true) => ({
|
||||||
schema: 'drive-trace.v1',
|
schema: 'drive-trace.v1',
|
||||||
profile: 'desktop-high',
|
profile: 'desktop-high',
|
||||||
|
referenceRun: { runId: `run-${runIndex}`, runIndex, attestationDigest: `${runIndex}`.repeat(64) },
|
||||||
gatingReferenceResult,
|
gatingReferenceResult,
|
||||||
validReferenceCadence: true,
|
validReferenceCadence: true,
|
||||||
metrics: { lcp, tbt },
|
metrics: { lcp, tbt },
|
||||||
@@ -13,7 +14,7 @@ const run = (lcp: number, tbt: number, gatingReferenceResult = true) => ({
|
|||||||
describe('five-run drive trace aggregation', () => {
|
describe('five-run drive trace aggregation', () => {
|
||||||
it('uses worst and median gates across exactly five reference runs', () => {
|
it('uses worst and median gates across exactly five reference runs', () => {
|
||||||
expect(aggregateFiveDriveTraces([
|
expect(aggregateFiveDriveTraces([
|
||||||
run(2_000, 100), run(2_100, 110), run(2_500, 200), run(2_900, 250), run(3_000, 300),
|
run(2_000, 100, 1), run(2_100, 110, 2), run(2_500, 200, 3), run(2_900, 250, 4), run(3_000, 300, 5),
|
||||||
])).toMatchObject({
|
])).toMatchObject({
|
||||||
runCount: 5,
|
runCount: 5,
|
||||||
passes: true,
|
passes: true,
|
||||||
@@ -23,8 +24,13 @@ describe('five-run drive trace aggregation', () => {
|
|||||||
|
|
||||||
it('rejects non-reference developer evidence and wrong run counts', () => {
|
it('rejects non-reference developer evidence and wrong run counts', () => {
|
||||||
expect(aggregateFiveDriveTraces([
|
expect(aggregateFiveDriveTraces([
|
||||||
run(2_000, 100, false), run(2_000, 100), run(2_000, 100), run(2_000, 100), run(2_000, 100),
|
run(2_000, 100, 1, false), run(2_000, 100, 2), run(2_000, 100, 3), run(2_000, 100, 4), run(2_000, 100, 5),
|
||||||
]).passes).toBe(false);
|
]).passes).toBe(false);
|
||||||
expect(() => aggregateFiveDriveTraces([run(2_000, 100)])).toThrow(/five/u);
|
expect(() => aggregateFiveDriveTraces([run(2_000, 100, 1)])).toThrow(/five/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects duplicate or incomplete authenticated reference identities', () => {
|
||||||
|
const duplicate = [1, 2, 3, 4, 4].map((index) => run(2_000, 100, index));
|
||||||
|
expect(() => aggregateFiveDriveTraces(duplicate)).toThrow(/run indexes 1 through 5/u);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { classifyArtifact, evaluateBudgetTotals, PERFORMANCE_BUDGETS } from '../../scripts/lib/bundle-budgets.mjs';
|
import {
|
||||||
|
classifyArtifact,
|
||||||
|
evaluateBudgetTotals,
|
||||||
|
evaluateMediaAssetBudget,
|
||||||
|
evaluateRuntimeBudgetEvidence,
|
||||||
|
PERFORMANCE_BUDGETS,
|
||||||
|
} from '../../scripts/lib/bundle-budgets.mjs';
|
||||||
|
|
||||||
describe('release budgets', () => {
|
describe('release budgets', () => {
|
||||||
it('pins every exact section 14 threshold', () => {
|
it('pins every exact section 14 threshold', () => {
|
||||||
@@ -31,6 +37,7 @@ describe('release budgets', () => {
|
|||||||
expect(classifyArtifact('assets/index-a.css', '')).toBe('critical-css');
|
expect(classifyArtifact('assets/index-a.css', '')).toBe('critical-css');
|
||||||
expect(classifyArtifact('assets/index-a.js', 'import("./environment-b.js")')).toBe('initial-application-js');
|
expect(classifyArtifact('assets/index-a.js', 'import("./environment-b.js")')).toBe('initial-application-js');
|
||||||
expect(classifyArtifact('assets/environment-b.js', 'three scene')).toBe('first-live-scene');
|
expect(classifyArtifact('assets/environment-b.js', 'three scene')).toBe('first-live-scene');
|
||||||
|
expect(classifyArtifact('assets/high-tier-b.js', 'optional bloom')).toBe('high-tier-extras');
|
||||||
expect(() => classifyArtifact('assets/mystery.bin', '')).toThrow(/unclassified/u);
|
expect(() => classifyArtifact('assets/mystery.bin', '')).toThrow(/unclassified/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,4 +45,80 @@ describe('release budgets', () => {
|
|||||||
expect(evaluateBudgetTotals({ initialHtmlCriticalCss: 100 * 1024 }).violations).toEqual([]);
|
expect(evaluateBudgetTotals({ initialHtmlCriticalCss: 100 * 1024 }).violations).toEqual([]);
|
||||||
expect(evaluateBudgetTotals({ initialHtmlCriticalCss: 100 * 1024 + 1 }).violations).toContainEqual(expect.objectContaining({ budget: 'initialHtmlCriticalCss' }));
|
expect(evaluateBudgetTotals({ initialHtmlCriticalCss: 100 * 1024 + 1 }).violations).toContainEqual(expect.objectContaining({ budget: 'initialHtmlCriticalCss' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects an undersized transfer when its video delivery rendition is 20s at 1920x1080', () => {
|
||||||
|
const result = evaluateMediaAssetBudget({
|
||||||
|
id: 'project-demo',
|
||||||
|
mediaType: 'video/webm',
|
||||||
|
width: 1_920,
|
||||||
|
height: 1_080,
|
||||||
|
durationMs: 20_000,
|
||||||
|
}, {
|
||||||
|
mediaType: 'video/webm',
|
||||||
|
width: 1_920,
|
||||||
|
height: 1_080,
|
||||||
|
durationMs: 20_000,
|
||||||
|
}, 1_024);
|
||||||
|
|
||||||
|
expect(result.violations.map((violation) => violation.budget)).toEqual(expect.arrayContaining([
|
||||||
|
'projectVideoDurationSeconds',
|
||||||
|
'projectVideoWidth',
|
||||||
|
'projectVideoHeight',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails closed when non-empty media dimensions cannot be deterministically probed', () => {
|
||||||
|
expect(() => evaluateMediaAssetBudget({
|
||||||
|
id: 'project-still',
|
||||||
|
mediaType: 'image/webp',
|
||||||
|
width: 1_280,
|
||||||
|
height: 720,
|
||||||
|
}, null, 1_024)).toThrow(/probe evidence/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects excessive high-tier extras and request concurrency', () => {
|
||||||
|
expect(evaluateBudgetTotals({ highTierExtras: PERFORMANCE_BUDGETS.highTierExtras! + 1 }).violations)
|
||||||
|
.toContainEqual(expect.objectContaining({ budget: 'highTierExtras' }));
|
||||||
|
expect(evaluateRuntimeBudgetEvidence({
|
||||||
|
schema: 'portfolio-runtime-budget-evidence/v1',
|
||||||
|
peakRequests: { imageScene: 3, video: 2 },
|
||||||
|
tiers: [
|
||||||
|
{ tier: 'low', textures: [], geometryBytes: 0, renderTargets: [] },
|
||||||
|
{ tier: 'high', textures: [], geometryBytes: 0, renderTargets: [] },
|
||||||
|
],
|
||||||
|
}).violations.map((violation) => violation.budget)).toEqual(expect.arrayContaining([
|
||||||
|
'imageSceneConcurrency',
|
||||||
|
'videoConcurrency',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives decoded texture and GPU bytes from allocation formulas', () => {
|
||||||
|
const result = evaluateRuntimeBudgetEvidence({
|
||||||
|
schema: 'portfolio-runtime-budget-evidence/v1',
|
||||||
|
peakRequests: { imageScene: 0, video: 0 },
|
||||||
|
tiers: [
|
||||||
|
{
|
||||||
|
tier: 'low',
|
||||||
|
textures: [{ id: 'oversized', width: 4_096, height: 4_096, bytesPerPixel: 4, layers: 1, mipmapped: true }],
|
||||||
|
geometryBytes: 40 * 1024 * 1024,
|
||||||
|
renderTargets: [{ id: 'framebuffer', width: 2_048, height: 2_048, bytesPerPixel: 8, attachments: 2, samples: 1 }],
|
||||||
|
},
|
||||||
|
{ tier: 'high', textures: [], geometryBytes: 0, renderTargets: [] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.tiers[0]?.textureBytes).toBe(Math.ceil(4_096 * 4_096 * 4 * 4 / 3));
|
||||||
|
expect(result.violations.map((violation) => violation.budget)).toEqual(expect.arrayContaining([
|
||||||
|
'mobileTextureDimension',
|
||||||
|
'lowTierGpuEstimate',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails closed when either named tier allocation ledger is absent', () => {
|
||||||
|
expect(() => evaluateRuntimeBudgetEvidence({
|
||||||
|
schema: 'portfolio-runtime-budget-evidence/v1',
|
||||||
|
peakRequests: { imageScene: 0, video: 0 },
|
||||||
|
tiers: [{ tier: 'low', textures: [], geometryBytes: 0, renderTargets: [] }],
|
||||||
|
})).toThrow(/exactly one Low and High/u);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { writeFile } from 'node:fs/promises';
|
|||||||
import type { CDPSession, Page } from '@playwright/test';
|
import type { CDPSession, Page } from '@playwright/test';
|
||||||
import { expect, test } from '../helpers/serve-dist';
|
import { expect, test } from '../helpers/serve-dist';
|
||||||
import { authorizedPerformanceUrl } from '../helpers/test-build-auth';
|
import { authorizedPerformanceUrl } from '../helpers/test-build-auth';
|
||||||
|
import { loadAuthenticatedReferenceRun, validateObservedReferenceEnvironment } from './reference-run';
|
||||||
import {
|
import {
|
||||||
calculateCls,
|
calculateCls,
|
||||||
calculateLabTbt,
|
calculateLabTbt,
|
||||||
@@ -79,9 +80,10 @@ const dispatchWheelBurst = async (page: Page, cdp: CDPSession, at: (offset: numb
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test('drive-trace.v1 runs on current Chromium as explicitly non-reference evidence', async ({ page, distUrl }, testInfo) => {
|
test('drive-trace.v1 runs on current Chromium with fail-closed reference authentication', async ({ browser, page, distUrl }, testInfo) => {
|
||||||
test.setTimeout(75_000);
|
test.setTimeout(75_000);
|
||||||
const profile = profileForProject(testInfo.project.name);
|
const profile = profileForProject(testInfo.project.name);
|
||||||
|
const referenceRun = await loadAuthenticatedReferenceRun(profile.name);
|
||||||
await page.setViewportSize(profile.viewport);
|
await page.setViewportSize(profile.viewport);
|
||||||
const cdp = await page.context().newCDPSession(page);
|
const cdp = await page.context().newCDPSession(page);
|
||||||
await cdp.send('Network.enable');
|
await cdp.send('Network.enable');
|
||||||
@@ -94,6 +96,7 @@ test('drive-trace.v1 runs on current Chromium as explicitly non-reference eviden
|
|||||||
});
|
});
|
||||||
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
||||||
await page.goto('about:blank');
|
await page.goto('about:blank');
|
||||||
|
const startedFromAboutBlank = page.url() === 'about:blank';
|
||||||
const loadStarted = nodePerformance.now();
|
const loadStarted = nodePerformance.now();
|
||||||
await page.goto(authorizedPerformanceUrl(distUrl, profile.tier), { waitUntil: 'load', timeout: 10_000 });
|
await page.goto(authorizedPerformanceUrl(distUrl, profile.tier), { waitUntil: 'load', timeout: 10_000 });
|
||||||
const loadDuration = nodePerformance.now() - loadStarted;
|
const loadDuration = nodePerformance.now() - loadStarted;
|
||||||
@@ -101,6 +104,21 @@ test('drive-trace.v1 runs on current Chromium as explicitly non-reference eviden
|
|||||||
await expect.poll(() => page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.snapshot().lcp.length ?? 0), { timeout: 10_000 }).toBeGreaterThan(0);
|
await expect.poll(() => page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.snapshot().lcp.length ?? 0), { timeout: 10_000 }).toBeGreaterThan(0);
|
||||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running', { timeout: 5_000 });
|
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running', { timeout: 5_000 });
|
||||||
await expect(page.locator('.journey-hud__quality')).toHaveText(profile.tier === 'high' ? 'High' : 'Low');
|
await expect(page.locator('.journey-hud__quality')).toHaveText(profile.tier === 'high' ? 'High' : 'Low');
|
||||||
|
if (referenceRun !== null) {
|
||||||
|
const observed = await page.evaluate(() => ({
|
||||||
|
viewport: { width: innerWidth, height: innerHeight },
|
||||||
|
visible: document.visibilityState === 'visible',
|
||||||
|
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' as const : 'no-preference' as const,
|
||||||
|
saveData: Boolean((navigator as Navigator & { connection?: { saveData?: boolean } }).connection?.saveData),
|
||||||
|
pageZoom: visualViewport?.scale ?? 1,
|
||||||
|
}));
|
||||||
|
validateObservedReferenceEnvironment(referenceRun, {
|
||||||
|
...observed,
|
||||||
|
browserName: 'Chromium',
|
||||||
|
browserVersion: browser.version(),
|
||||||
|
startedFromAboutBlank,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const warmupRafs = await page.evaluate(() => new Promise<number[]>((resolve) => {
|
const warmupRafs = await page.evaluate(() => new Promise<number[]>((resolve) => {
|
||||||
const samples: number[] = [];
|
const samples: number[] = [];
|
||||||
@@ -194,8 +212,9 @@ test('drive-trace.v1 runs on current Chromium as explicitly non-reference eviden
|
|||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
schema: 'drive-trace.v1',
|
schema: 'drive-trace.v1',
|
||||||
evidenceKind: 'current-chromium-developer-machine-non-reference',
|
evidenceKind: referenceRun === null ? 'current-chromium-developer-machine-non-reference' : 'authenticated-physical-reference',
|
||||||
gatingReferenceResult: false,
|
gatingReferenceResult: referenceRun !== null,
|
||||||
|
...(referenceRun === null ? {} : { referenceRun: { runId: referenceRun.runId, runIndex: referenceRun.runIndex, attestationDigest: referenceRun.attestationDigest } }),
|
||||||
profile: profile.name,
|
profile: profile.name,
|
||||||
tier: profile.tier,
|
tier: profile.tier,
|
||||||
browser: testInfo.project.name,
|
browser: testInfo.project.name,
|
||||||
@@ -203,7 +222,7 @@ test('drive-trace.v1 runs on current Chromium as explicitly non-reference eviden
|
|||||||
budgetResults,
|
budgetResults,
|
||||||
referenceBudgetsPass: validReferenceCadence && Object.values(budgetResults).every(Boolean),
|
referenceBudgetsPass: validReferenceCadence && Object.values(budgetResults).every(Boolean),
|
||||||
validReferenceCadence,
|
validReferenceCadence,
|
||||||
pendingPhysicalReferenceRuns: 5,
|
pendingPhysicalReferenceRuns: referenceRun === null ? 5 : 0,
|
||||||
};
|
};
|
||||||
await writeFile(testInfo.outputPath('drive-trace.v1.json'), `${JSON.stringify(report, null, 2)}\n`);
|
await writeFile(testInfo.outputPath('drive-trace.v1.json'), `${JSON.stringify(report, null, 2)}\n`);
|
||||||
});
|
});
|
||||||
|
|||||||
93
tests/performance/reference-run.test.ts
Normal file
93
tests/performance/reference-run.test.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { authenticateReferenceRun, validateObservedReferenceEnvironment } from './reference-run';
|
||||||
|
|
||||||
|
const attestation = (runIndex = 1) => ({
|
||||||
|
schema: 'portfolio-reference-run-attestation/v1',
|
||||||
|
profile: 'desktop-high',
|
||||||
|
runId: `desktop-reference-${runIndex}`,
|
||||||
|
capturedAt: '2026-09-02T05:00:00.000Z',
|
||||||
|
runIndex,
|
||||||
|
device: { model: 'Apple M1 MacBook Air', cpu: 'Apple M1', gpu: 'Apple M1 integrated GPU', ramGb: 8 },
|
||||||
|
browser: { name: 'Chromium', version: '140.0.0', driverVersion: 'Playwright 1.62.1' },
|
||||||
|
osVersion: 'macOS 15.6',
|
||||||
|
display: { refreshHz: 60 },
|
||||||
|
viewport: { width: 1_440, height: 900 },
|
||||||
|
network: { downstreamKbps: 10_000, upstreamKbps: 10_000, roundTripLatencyMs: 40 },
|
||||||
|
assertions: {
|
||||||
|
batteryPercent: 80,
|
||||||
|
powerSaving: false,
|
||||||
|
coolStart: true,
|
||||||
|
noOtherForegroundApplications: true,
|
||||||
|
cpuEmulation: false,
|
||||||
|
pageZoom: 1,
|
||||||
|
reducedMotion: 'no-preference',
|
||||||
|
saveData: false,
|
||||||
|
audioState: 'idle',
|
||||||
|
freshBrowserProfile: true,
|
||||||
|
startedFromAboutBlank: true,
|
||||||
|
storageCleared: { serviceWorkers: true, cacheStorage: true, httpCache: true, cookies: true, localStorage: true, sessionStorage: true, indexedDb: true, preferences: true },
|
||||||
|
physicalReferenceDevice: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticated reference runs', () => {
|
||||||
|
it('cannot mark an ordinary developer trace as gating evidence', () => {
|
||||||
|
const bytes = Buffer.from(JSON.stringify(attestation()));
|
||||||
|
expect(() => authenticateReferenceRun({
|
||||||
|
referenceMode: false,
|
||||||
|
expectedProfile: 'desktop-high',
|
||||||
|
attestationBytes: bytes,
|
||||||
|
expectedDigest: createHash('sha256').update(bytes).digest('hex'),
|
||||||
|
})).toThrow(/reference mode/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails closed on digest, device, network, or assertion mismatch', () => {
|
||||||
|
const invalid = attestation();
|
||||||
|
invalid.network.downstreamKbps = 9_999;
|
||||||
|
const bytes = Buffer.from(JSON.stringify(invalid));
|
||||||
|
expect(() => authenticateReferenceRun({
|
||||||
|
referenceMode: true,
|
||||||
|
expectedProfile: 'desktop-high',
|
||||||
|
attestationBytes: bytes,
|
||||||
|
expectedDigest: createHash('sha256').update(bytes).digest('hex'),
|
||||||
|
})).toThrow(/network/u);
|
||||||
|
expect(() => authenticateReferenceRun({
|
||||||
|
referenceMode: true,
|
||||||
|
expectedProfile: 'desktop-high',
|
||||||
|
attestationBytes: bytes,
|
||||||
|
expectedDigest: '0'.repeat(64),
|
||||||
|
})).toThrow(/digest/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('authenticates only exact pinned metadata and validates browser-observed assertions', () => {
|
||||||
|
const bytes = Buffer.from(JSON.stringify(attestation()));
|
||||||
|
const authenticated = authenticateReferenceRun({
|
||||||
|
referenceMode: true,
|
||||||
|
expectedProfile: 'desktop-high',
|
||||||
|
attestationBytes: bytes,
|
||||||
|
expectedDigest: createHash('sha256').update(bytes).digest('hex'),
|
||||||
|
});
|
||||||
|
expect(authenticated).toMatchObject({ runId: 'desktop-reference-1', runIndex: 1, profile: 'desktop-high' });
|
||||||
|
expect(() => validateObservedReferenceEnvironment(authenticated, {
|
||||||
|
viewport: { width: 1_440, height: 900 },
|
||||||
|
visible: true,
|
||||||
|
reducedMotion: 'no-preference',
|
||||||
|
saveData: false,
|
||||||
|
pageZoom: 1,
|
||||||
|
browserName: 'Chromium',
|
||||||
|
browserVersion: '140.0.0',
|
||||||
|
startedFromAboutBlank: true,
|
||||||
|
})).not.toThrow();
|
||||||
|
expect(() => validateObservedReferenceEnvironment(authenticated, {
|
||||||
|
viewport: { width: 1_440, height: 899 },
|
||||||
|
visible: true,
|
||||||
|
reducedMotion: 'no-preference',
|
||||||
|
saveData: false,
|
||||||
|
pageZoom: 1,
|
||||||
|
browserName: 'Chromium',
|
||||||
|
browserVersion: '140.0.0',
|
||||||
|
startedFromAboutBlank: true,
|
||||||
|
})).toThrow(/viewport/u);
|
||||||
|
});
|
||||||
|
});
|
||||||
102
tests/performance/reference-run.ts
Normal file
102
tests/performance/reference-run.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
export type ReferenceProfile = 'desktop-high' | 'mobile-low';
|
||||||
|
|
||||||
|
export interface AuthenticatedReferenceRun {
|
||||||
|
profile: ReferenceProfile;
|
||||||
|
runId: string;
|
||||||
|
runIndex: number;
|
||||||
|
attestationDigest: string;
|
||||||
|
browserVersion: string;
|
||||||
|
viewport: { width: number; height: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthenticationInput {
|
||||||
|
referenceMode: boolean;
|
||||||
|
expectedProfile: ReferenceProfile;
|
||||||
|
attestationBytes: Uint8Array;
|
||||||
|
expectedDigest: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ObservedEnvironment {
|
||||||
|
viewport: { width: number; height: number };
|
||||||
|
visible: boolean;
|
||||||
|
reducedMotion: 'no-preference' | 'reduce';
|
||||||
|
saveData: boolean;
|
||||||
|
pageZoom: number;
|
||||||
|
browserName: string;
|
||||||
|
browserVersion: string;
|
||||||
|
startedFromAboutBlank: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exactKeys = (value: unknown, keys: readonly string[], label: string): Record<string, unknown> => {
|
||||||
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} is invalid.`);
|
||||||
|
const actual = Object.keys(value).sort();
|
||||||
|
const expected = [...keys].sort();
|
||||||
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`${label} fields are invalid.`);
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pinned = {
|
||||||
|
'desktop-high': { viewport: { width: 1_440, height: 900 }, cpu: 'Apple M1', gpu: 'Apple M1 integrated GPU', ramGb: 8, downstreamKbps: 10_000, upstreamKbps: 10_000, roundTripLatencyMs: 40 },
|
||||||
|
'mobile-low': { viewport: { width: 390, height: 844 }, cpu: 'Google Tensor', gpu: 'Mali-G78 MP20', ramGb: 8, downstreamKbps: 1_600, upstreamKbps: 750, roundTripLatencyMs: 150 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const authenticateReferenceRun = (input: AuthenticationInput): AuthenticatedReferenceRun => {
|
||||||
|
if (!input.referenceMode) throw new Error('Authenticated reference mode is required.');
|
||||||
|
if (!/^[a-f0-9]{64}$/u.test(input.expectedDigest)) throw new Error('Reference attestation digest is invalid.');
|
||||||
|
const actualDigest = createHash('sha256').update(input.attestationBytes).digest('hex');
|
||||||
|
if (actualDigest !== input.expectedDigest) throw new Error('Reference attestation digest mismatch.');
|
||||||
|
let decoded: unknown;
|
||||||
|
try { decoded = JSON.parse(Buffer.from(input.attestationBytes).toString('utf8')); } catch { throw new Error('Reference attestation JSON is invalid.'); }
|
||||||
|
const root = exactKeys(decoded, ['schema', 'profile', 'runId', 'runIndex', 'capturedAt', 'device', 'browser', 'osVersion', 'display', 'viewport', 'network', 'assertions'], 'Reference attestation');
|
||||||
|
if (root.schema !== 'portfolio-reference-run-attestation/v1' || root.profile !== input.expectedProfile
|
||||||
|
|| typeof root.runId !== 'string' || !/^[a-z0-9][a-z0-9-]{2,79}$/u.test(root.runId)
|
||||||
|
|| !Number.isInteger(root.runIndex) || Number(root.runIndex) < 1 || Number(root.runIndex) > 5
|
||||||
|
|| typeof root.capturedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(root.capturedAt)
|
||||||
|
|| !Number.isFinite(Date.parse(root.capturedAt))
|
||||||
|
|| typeof root.osVersion !== 'string' || root.osVersion.length < 2) throw new Error('Reference run metadata is invalid.');
|
||||||
|
const profile = pinned[input.expectedProfile];
|
||||||
|
const device = exactKeys(root.device, ['model', 'cpu', 'gpu', 'ramGb'], 'Reference device');
|
||||||
|
if (typeof device.model !== 'string' || device.model.length < 2 || device.cpu !== profile.cpu || device.gpu !== profile.gpu || device.ramGb !== profile.ramGb) throw new Error('Reference device does not match the pinned class.');
|
||||||
|
const browser = exactKeys(root.browser, ['name', 'version', 'driverVersion'], 'Reference browser');
|
||||||
|
if (browser.name !== 'Chromium' || typeof browser.version !== 'string' || !/^\d+(?:\.\d+){1,3}$/u.test(browser.version)
|
||||||
|
|| typeof browser.driverVersion !== 'string' || browser.driverVersion.length < 2) throw new Error('Reference browser metadata is invalid.');
|
||||||
|
const display = exactKeys(root.display, ['refreshHz'], 'Reference display');
|
||||||
|
if (display.refreshHz !== 60) throw new Error('Reference display must be fixed at 60 Hz.');
|
||||||
|
const viewport = exactKeys(root.viewport, ['width', 'height'], 'Reference viewport');
|
||||||
|
if (viewport.width !== profile.viewport.width || viewport.height !== profile.viewport.height) throw new Error('Reference viewport does not match the pinned profile.');
|
||||||
|
const network = exactKeys(root.network, ['downstreamKbps', 'upstreamKbps', 'roundTripLatencyMs'], 'Reference network');
|
||||||
|
if (network.downstreamKbps !== profile.downstreamKbps || network.upstreamKbps !== profile.upstreamKbps || network.roundTripLatencyMs !== profile.roundTripLatencyMs) throw new Error('Reference network does not match the pinned profile.');
|
||||||
|
const assertions = exactKeys(root.assertions, ['batteryPercent', 'powerSaving', 'coolStart', 'noOtherForegroundApplications', 'cpuEmulation', 'pageZoom', 'reducedMotion', 'saveData', 'audioState', 'freshBrowserProfile', 'startedFromAboutBlank', 'storageCleared', 'physicalReferenceDevice'], 'Reference assertions');
|
||||||
|
const storage = exactKeys(assertions.storageCleared, ['serviceWorkers', 'cacheStorage', 'httpCache', 'cookies', 'localStorage', 'sessionStorage', 'indexedDb', 'preferences'], 'Reference storage assertions');
|
||||||
|
if (typeof assertions.batteryPercent !== 'number' || assertions.batteryPercent <= 50 || assertions.batteryPercent > 100
|
||||||
|
|| assertions.powerSaving !== false || assertions.coolStart !== true || assertions.noOtherForegroundApplications !== true
|
||||||
|
|| assertions.cpuEmulation !== false || assertions.pageZoom !== 1 || assertions.reducedMotion !== 'no-preference'
|
||||||
|
|| assertions.saveData !== false || assertions.audioState !== 'idle' || assertions.freshBrowserProfile !== true
|
||||||
|
|| assertions.startedFromAboutBlank !== true || assertions.physicalReferenceDevice !== true
|
||||||
|
|| Object.values(storage).some((value) => value !== true)) throw new Error('Reference environment assertions are not all satisfied.');
|
||||||
|
return {
|
||||||
|
profile: input.expectedProfile,
|
||||||
|
runId: root.runId,
|
||||||
|
runIndex: Number(root.runIndex),
|
||||||
|
attestationDigest: actualDigest,
|
||||||
|
browserVersion: browser.version,
|
||||||
|
viewport: profile.viewport,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadAuthenticatedReferenceRun = async (profile: ReferenceProfile): Promise<AuthenticatedReferenceRun | null> => {
|
||||||
|
if (process.env.PORTFOLIO_REFERENCE_LAB !== '1') return null;
|
||||||
|
const path = process.env.PORTFOLIO_REFERENCE_ATTESTATION_PATH;
|
||||||
|
const digest = process.env.PORTFOLIO_REFERENCE_ATTESTATION_SHA256;
|
||||||
|
if (path === undefined || digest === undefined) throw new Error('Reference mode requires an attestation path and SHA-256 digest.');
|
||||||
|
return authenticateReferenceRun({ referenceMode: true, expectedProfile: profile, attestationBytes: await readFile(path), expectedDigest: digest });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateObservedReferenceEnvironment = (reference: AuthenticatedReferenceRun, observed: ObservedEnvironment): void => {
|
||||||
|
if (observed.viewport.width !== reference.viewport.width || observed.viewport.height !== reference.viewport.height) throw new Error('Observed viewport does not match the attestation.');
|
||||||
|
if (!observed.visible || observed.reducedMotion !== 'no-preference' || observed.saveData || observed.pageZoom !== 1 || !observed.startedFromAboutBlank) throw new Error('Observed reference environment assertions failed.');
|
||||||
|
if (observed.browserName !== 'Chromium' || observed.browserVersion !== reference.browserVersion) throw new Error('Observed browser does not match the attestation.');
|
||||||
|
};
|
||||||
@@ -98,7 +98,7 @@ describe('scene render quality', () => {
|
|||||||
const medium = renderQualityFor('medium', 3, false);
|
const medium = renderQualityFor('medium', 3, false);
|
||||||
const low = renderQualityFor('low', 3, false);
|
const low = renderQualityFor('low', 3, false);
|
||||||
|
|
||||||
expect(high).toEqual({ pixelRatio: 2, terrainSegments: 96, particleCount: 384, postEffects: true });
|
expect(high).toEqual({ pixelRatio: 1.5, terrainSegments: 96, particleCount: 384, postEffects: true });
|
||||||
expect(medium).toEqual({ pixelRatio: 1.5, terrainSegments: 64, particleCount: 192, postEffects: true });
|
expect(medium).toEqual({ pixelRatio: 1.5, terrainSegments: 64, particleCount: 192, postEffects: true });
|
||||||
expect(low).toEqual({ pixelRatio: 1.25, terrainSegments: 32, particleCount: 64, postEffects: false });
|
expect(low).toEqual({ pixelRatio: 1.25, terrainSegments: 32, particleCount: 64, postEffects: false });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user