A lazy chunk (journey/nav-wave.ts) that draws the bar's wave: a calm ribbon at cruise, a spectrum at speed, with a hue cycle near the top of the dial. It follows the chapter's accent and the dawn, repaints at once on resize, and draws one still frame under reduced motion. Budgeted as a deferred chunk.
268 lines
15 KiB
JavaScript
268 lines
15 KiB
JavaScript
export const PERFORMANCE_BUDGETS = Object.freeze({
|
|
initialHtmlCriticalCss: 100 * 1024,
|
|
initialApplicationJs: 120 * 1024,
|
|
heroPoster: 220 * 1024,
|
|
initialFonts: 120 * 1024,
|
|
firstLiveScene: 650 * 1024,
|
|
projectVideo: 2.5 * 1024 * 1024,
|
|
projectVideoDurationSeconds: 10,
|
|
projectVideoWidth: 1280,
|
|
projectVideoHeight: 720,
|
|
projectStill: 400 * 1024,
|
|
chart: 100 * 1024,
|
|
deferredProjectVisuals: 2.5 * 1024 * 1024,
|
|
highTierExtras: 750 * 1024,
|
|
deferredNonVideo: 3.5 * 1024 * 1024,
|
|
deferredAudio: 16 * 1024 * 1024,
|
|
imageSceneConcurrency: 2,
|
|
videoConcurrency: 1,
|
|
mobileTextureDimension: 2048,
|
|
lowTierGpuEstimate: 96 * 1024 * 1024,
|
|
highTierGpuEstimate: 192 * 1024 * 1024,
|
|
});
|
|
|
|
export const normalizeDetectedMediaType = (mediaType) => (
|
|
typeof mediaType === 'string' ? mediaType.split(';', 1)[0].trim() : null
|
|
);
|
|
|
|
export const resolveBudgetGateMode = ({ transferOnly, runtimeTracePath }) => {
|
|
if (transferOnly && runtimeTracePath !== null) throw new Error('Transfer-only mode cannot be combined with runtime evidence.');
|
|
if (transferOnly) return { kind: 'transfer-only' };
|
|
if (runtimeTracePath === null) throw new Error('A runtime trace is required for the full performance budget gate. Use --transfer-only only for a non-gating static build check.');
|
|
return { kind: 'full', runtimeTracePath };
|
|
};
|
|
|
|
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';
|
|
// DASHCAM: the photo-mode frame and postcard, loaded on first open.
|
|
if (/^assets\/DashcamFrame-[^/]+\.(?:js|css)$/u.test(path)) return 'deferred-non-video';
|
|
// RELAY STATION: the mast chirp, loaded when the first mast comes online with sound on.
|
|
if (/^assets\/relay-tone-[^/]+\.js$/u.test(path)) return 'deferred-non-video';
|
|
// UNDERPASS: the pass swell, loaded when the first deck shades the road with sound on.
|
|
if (/^assets\/underpass-pass-[^/]+\.js$/u.test(path)) return 'deferred-non-video';
|
|
// NIGHT TRAFFIC: the pass swell, loaded when a vehicle first passes alongside with sound on.
|
|
if (/^assets\/traffic-pass-[^/]+\.js$/u.test(path)) return 'deferred-non-video';
|
|
// PARKING LIGHTS: the favicon renderer, loaded after mount.
|
|
if (/^assets\/tab-icon-[^/]+\.js$/u.test(path)) return 'deferred-non-video';
|
|
// SPEEDOMETER: the gantry dial's model and DOM driver, loaded after mount.
|
|
if (/^assets\/speedometer-dial-[^/]+\.js$/u.test(path)) return 'deferred-non-video';
|
|
// NAV BAR: the lower-bezel wave canvas, loaded after the bar mounts.
|
|
if (/^assets\/nav-wave-[^/]+\.js$/u.test(path)) return 'deferred-non-video';
|
|
if (/^assets\/nightdrive-(?:grand-tourer|districts)-[^/]+\.glb$/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';
|
|
if (/^assets\/project-[^/]+\.(?:webm|mp4)$/u.test(path)) return 'project-video';
|
|
if (/^assets\/project-[^/]*chart[^/]*\.(?:avif|webp|png|jpe?g)$/u.test(path)) return 'chart';
|
|
if (/^assets\/project-[^/]+\.(?:avif|webp|png|jpe?g|svg)$/u.test(path)) return 'project-still';
|
|
if (/^assets\/nightdrive-(?:intro|main|afterglow|drive-lift)\.(?:opus|mp3)$/u.test(path)) return 'soundtrack-audio';
|
|
if (/^(?:robots\.txt|sitemap\.xml|_headers\.example|_redirects\.example)$/u.test(path)) return 'release-metadata';
|
|
throw new Error(`Artifact is unclassified: ${path}`);
|
|
};
|
|
|
|
const thresholdByTotal = Object.freeze({
|
|
initialHtmlCriticalCss: PERFORMANCE_BUDGETS.initialHtmlCriticalCss,
|
|
initialApplicationJs: PERFORMANCE_BUDGETS.initialApplicationJs,
|
|
heroPoster: PERFORMANCE_BUDGETS.heroPoster,
|
|
initialFonts: PERFORMANCE_BUDGETS.initialFonts,
|
|
firstLiveScene: PERFORMANCE_BUDGETS.firstLiveScene,
|
|
deferredProjectVisuals: PERFORMANCE_BUDGETS.deferredProjectVisuals,
|
|
deferredNonVideo: PERFORMANCE_BUDGETS.deferredNonVideo,
|
|
deferredAudio: PERFORMANCE_BUDGETS.deferredAudio,
|
|
highTierExtras: PERFORMANCE_BUDGETS.highTierExtras,
|
|
});
|
|
|
|
export const evaluateBudgetTotals = (totals) => ({
|
|
violations: Object.entries(thresholdByTotal)
|
|
.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 = /** @type {'chart' | 'project-still' | 'soundtrack-audio' | undefined} */ (undefined),
|
|
) => {
|
|
if (probe === null || typeof probe !== 'object') throw new Error(`Media probe evidence is required for ${asset?.id ?? 'unknown asset'}.`);
|
|
if (assetClass === 'soundtrack-audio' && !asset?.mediaType?.startsWith('audio/')) {
|
|
throw new Error(`Soundtrack classification requires audio media for ${asset?.id ?? 'unknown asset'}.`);
|
|
}
|
|
if (asset?.mediaType !== probe.mediaType) {
|
|
throw new Error(`Manifest and probe evidence disagree for ${asset?.id ?? 'unknown asset'}.`);
|
|
}
|
|
if (asset.mediaType.startsWith('audio/')) {
|
|
if (asset.width !== null || asset.height !== null || probe.width !== null || probe.height !== null) {
|
|
throw new Error(`Audio dimensions must be null for ${asset.id}.`);
|
|
}
|
|
if (!finitePositiveInteger(asset.durationMs) || !finitePositiveInteger(probe.durationMs)
|
|
|| Math.abs(asset.durationMs - probe.durationMs) > 100) {
|
|
throw new Error(`Verified duration is required for ${asset.id}.`);
|
|
}
|
|
return { violations: [] };
|
|
}
|
|
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) {
|
|
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 };
|
|
};
|
|
|
|
export const evaluateRuntimeTraceBudgetEvidence = (evidence) => {
|
|
if (evidence?.schema !== 'portfolio-runtime-trace-evidence/v1' || evidence.allocation === null || typeof evidence.allocation !== 'object') {
|
|
throw new Error('Runtime trace evidence must use portfolio-runtime-trace-evidence/v1.');
|
|
}
|
|
const evaluated = evaluateRuntimeBudgetEvidence({
|
|
schema: 'portfolio-runtime-budget-evidence/v1',
|
|
peakRequests: evidence.peakRequests,
|
|
tiers: [
|
|
evidence.allocation,
|
|
evidence.allocation.tier === 'low'
|
|
? { tier: 'high', textures: [], geometryBytes: 0, renderTargets: [] }
|
|
: { tier: 'low', textures: [], geometryBytes: 0, renderTargets: [] },
|
|
],
|
|
});
|
|
const tier = evaluated.tiers.find((item) => item.tier === evidence.allocation.tier);
|
|
if (tier === undefined) throw new Error('Runtime trace tier could not be evaluated.');
|
|
return {
|
|
tier,
|
|
violations: evaluated.violations,
|
|
};
|
|
};
|
|
|
|
export const validateRuntimeBudgetTraceReport = (report) => {
|
|
if (report?.schema !== 'runtime-budget-trace.v1'
|
|
|| report.evidenceKind !== 'authenticated-test-build-production-path'
|
|
|| report.gatingReferenceResult !== false) {
|
|
throw new Error('Runtime budget verification requires an authenticated production-path capture.');
|
|
}
|
|
const expectedProfile = report.tier === 'high' ? 'desktop-high' : report.tier === 'low' ? 'mobile-low' : null;
|
|
if (expectedProfile === null || report.profile !== expectedProfile) {
|
|
throw new Error('Runtime budget capture profile and tier disagree.');
|
|
}
|
|
const viewport = report.observed?.viewport;
|
|
const devicePixelRatio = report.observed?.devicePixelRatio;
|
|
const rendererPixelRatio = report.observed?.rendererPixelRatio;
|
|
const expectedRendererPixelRatio = report.tier === 'high' ? 1.5 : 1.25;
|
|
if (!finitePositiveInteger(viewport?.width) || !finitePositiveInteger(viewport?.height)
|
|
|| !(Number.isFinite(devicePixelRatio) && devicePixelRatio >= expectedRendererPixelRatio)
|
|
|| rendererPixelRatio !== expectedRendererPixelRatio) {
|
|
throw new Error('Runtime budget capture did not exercise the named tier DPR cap.');
|
|
}
|
|
const evidence = report.runtimeBudgetEvidence;
|
|
if (evidence?.allocation?.tier !== report.tier || !Array.isArray(evidence.allocation.renderTargets)) {
|
|
throw new Error('Runtime budget capture allocation and tier disagree.');
|
|
}
|
|
const framebuffers = evidence.allocation.renderTargets.filter((target) => target?.id === 'default-framebuffer');
|
|
const expectedWidth = Math.round(viewport.width * rendererPixelRatio);
|
|
const expectedHeight = Math.round(viewport.height * rendererPixelRatio);
|
|
if (framebuffers.length !== 1 || framebuffers[0].width !== expectedWidth || framebuffers[0].height !== expectedHeight) {
|
|
throw new Error('Runtime budget capture framebuffer dimensions do not match the observed viewport and DPR cap.');
|
|
}
|
|
return evaluateRuntimeTraceBudgetEvidence(evidence);
|
|
};
|
|
|
|
const requestClass = (entry) => {
|
|
if (entry.initiatorType === 'video' || /\.(?:mp4|webm)(?:[?#]|$)/iu.test(entry.name)) return 'video';
|
|
if (entry.initiatorType === 'img' || /\/assets\/(?:environment-|high-tier-)/u.test(entry.name)) return 'imageScene';
|
|
return null;
|
|
};
|
|
|
|
export const calculatePeakRequestConcurrency = (entries) => {
|
|
if (!Array.isArray(entries)) throw new Error('Resource timing entries are required.');
|
|
const peaks = { imageScene: 0, video: 0 };
|
|
for (const kind of ['imageScene', 'video']) {
|
|
const events = [];
|
|
for (const entry of entries) {
|
|
if (requestClass(entry) !== kind) continue;
|
|
if (!Number.isFinite(entry.startTime) || !Number.isFinite(entry.responseEnd) || entry.responseEnd < entry.startTime) {
|
|
throw new Error('Tracked resource timing is incomplete.');
|
|
}
|
|
events.push({ at: entry.startTime, delta: 1 }, { at: entry.responseEnd, delta: -1 });
|
|
}
|
|
events.sort((left, right) => left.at - right.at || left.delta - right.delta);
|
|
let active = 0;
|
|
for (const event of events) {
|
|
active += event.delta;
|
|
peaks[kind] = Math.max(peaks[kind], active);
|
|
}
|
|
}
|
|
return peaks;
|
|
};
|