fix: close performance evidence and budget gates

This commit is contained in:
2026-09-02 08:27:21 +03:00
parent 1734003172
commit 5525557c3a
13 changed files with 541 additions and 39 deletions

View File

@@ -1,8 +1,17 @@
#!/usr/bin/env node
import { createHash } from 'node:crypto';
import { brotliCompressSync, constants as zlibConstants } from 'node:zlib';
import { readFile, readdir, stat } from 'node:fs/promises';
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');
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)
? bytes.byteLength
: brotliCompressSync(bytes, {
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 },
}).byteLength);
: brotliCompressSync(bytes, { 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 mediaViolations = [];
for (const absolute of await listFiles(root)) {
const path = relative(root, absolute).split(sep).join('/');
const bytes = await readFile(absolute);
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) });
}
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 totals = {
@@ -42,25 +107,10 @@ const totals = {
initialFonts: sum('initial-fonts'),
firstLiveScene: sum('first-live-scene'),
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 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.');
@@ -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 (/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 = {
schema: 'portfolio-bundle-budget/v1',
compression: 'brotli-quality-11-or-raw-already-compressed',
totals,
runtime: { peakRequests: runtimeEvidence.peakRequests, tiers: runtime.tiers },
records: records.sort((left, right) => left.path.localeCompare(right.path)),
violations: result.violations,
violations,
};
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
if (result.violations.length > 0) process.exitCode = 1;
if (violations.length > 0) process.exitCode = 1;

View File

@@ -3,3 +3,13 @@ export function classifyArtifact(path: string, source?: string): string;
export function evaluateBudgetTotals(totals: Readonly<Record<string, 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 }>;
};

View File

@@ -24,6 +24,7 @@ export const classifyArtifact = (path, source = '') => {
if (path === 'index.html') return 'initial-html';
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\/(?: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 (/\.(?:woff2?|ttf|otf)$/u.test(path)) return 'initial-fonts';
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,
deferredProjectVisuals: PERFORMANCE_BUDGETS.deferredProjectVisuals,
deferredNonVideo: PERFORMANCE_BUDGETS.deferredNonVideo,
highTierExtras: PERFORMANCE_BUDGETS.highTierExtras,
});
export const evaluateBudgetTotals = (totals) => ({
@@ -49,3 +51,85 @@ export const evaluateBudgetTotals = (totals) => ({
.filter(([budget, threshold]) => (totals[budget] ?? 0) > 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 };
};

View File

@@ -10,6 +10,16 @@ export const aggregateFiveDriveTraces = (runs) => {
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.');
}
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 tbt = runs.map((run) => run.metrics.tbt);
const everyPerRunBudgetPass = runs.every((run) => Object.values(run.budgetResults).every(Boolean));