perf: enforce journey budgets and reference traces
This commit is contained in:
394
src/testing/performance-instrumentation.ts
Normal file
394
src/testing/performance-instrumentation.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
declare const __PORTFOLIO_TEST_BUILD__: boolean;
|
||||
declare const __PORTFOLIO_TEST_TOKEN_DIGEST__: string;
|
||||
|
||||
const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__;
|
||||
const TEST_TOKEN_DIGEST = typeof __PORTFOLIO_TEST_TOKEN_DIGEST__ === 'undefined'
|
||||
? ''
|
||||
: __PORTFOLIO_TEST_TOKEN_DIGEST__;
|
||||
|
||||
export interface LayoutShiftSample {
|
||||
startTime: number;
|
||||
value: number;
|
||||
hadRecentInput: boolean;
|
||||
}
|
||||
|
||||
export interface LongTaskSample {
|
||||
startTime: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface EventTimingSample {
|
||||
target: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
interactionId: number;
|
||||
}
|
||||
|
||||
export interface ScriptedActionSample {
|
||||
id: string;
|
||||
target: string;
|
||||
dispatchTime: number;
|
||||
acknowledgementCount: number;
|
||||
mutationTime: number | null;
|
||||
doubleRafTime: number | null;
|
||||
eventTimingSupported: boolean;
|
||||
eventEntries: readonly EventTimingSample[];
|
||||
}
|
||||
|
||||
export interface SceneInterval { start: number; end: number }
|
||||
export interface SceneFrameSample { start: number; end: number }
|
||||
|
||||
export const nearestRankP95 = (samples: readonly number[]): number => {
|
||||
if (samples.length === 0) throw new Error('At least one sample is required.');
|
||||
const sorted = [...samples].sort((left, right) => left - right);
|
||||
return sorted[Math.ceil(0.95 * sorted.length) - 1]!;
|
||||
};
|
||||
|
||||
export const latestLcpBeforeCutoff = (entries: readonly number[], cutoff: number): number => {
|
||||
const eligible = entries.filter((entry) => entry < cutoff);
|
||||
if (eligible.length === 0) throw new Error('An LCP entry is required before the cutoff.');
|
||||
return Math.max(...eligible);
|
||||
};
|
||||
|
||||
export const calculateCls = (entries: readonly LayoutShiftSample[]): number => {
|
||||
const eligible = entries
|
||||
.filter((entry) => !entry.hadRecentInput)
|
||||
.sort((left, right) => left.startTime - right.startTime);
|
||||
let maximum = 0;
|
||||
let windowStart = -Infinity;
|
||||
let previous = -Infinity;
|
||||
let sum = 0;
|
||||
for (const entry of eligible) {
|
||||
if (entry.startTime - previous > 1_000 || entry.startTime - windowStart > 5_000) {
|
||||
windowStart = entry.startTime;
|
||||
sum = 0;
|
||||
}
|
||||
sum += entry.value;
|
||||
previous = entry.startTime;
|
||||
maximum = Math.max(maximum, sum);
|
||||
}
|
||||
return maximum;
|
||||
};
|
||||
|
||||
export const calculateLabTbt = (
|
||||
tasks: readonly LongTaskSample[],
|
||||
fcp: number | null,
|
||||
runningAt: number,
|
||||
): number => {
|
||||
if (fcp === null) throw new Error('FCP is required for lab total blocking time.');
|
||||
return tasks.reduce((total, task) => {
|
||||
const start = Math.max(task.startTime + 50, fcp);
|
||||
const end = Math.min(task.startTime + task.duration, runningAt);
|
||||
return total + Math.max(0, end - start);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const postRunningLongTasks = (
|
||||
tasks: readonly LongTaskSample[],
|
||||
runningAt: number,
|
||||
traceEnd: number,
|
||||
): LongTaskSample[] => tasks.filter((task) => (
|
||||
task.startTime < traceEnd && task.startTime + task.duration > runningAt
|
||||
));
|
||||
|
||||
const exactActionIds = new Set(['start', 'volume', 'mute', 'unmute', 'pause', 'resume', 'toolkit', 'contact']);
|
||||
|
||||
export const evaluateEventToNextPaint = (
|
||||
actions: readonly ScriptedActionSample[],
|
||||
): { values: number[]; p95: number } => {
|
||||
if (actions.length !== 8 || new Set(actions.map((action) => action.id)).size !== 8 || actions.some((action) => !exactActionIds.has(action.id))) {
|
||||
throw new Error('The exact eight scripted actions are required.');
|
||||
}
|
||||
const values = actions.map((action) => {
|
||||
if (action.acknowledgementCount !== 1) throw new Error('Each action requires exactly one acknowledgement.');
|
||||
if (action.mutationTime === null || action.mutationTime > action.dispatchTime + 500) throw new Error('The exact state mutation missed its deadline.');
|
||||
if (action.doubleRafTime === null || action.doubleRafTime > action.dispatchTime + 500) throw new Error('Double RAF confirmation missed its deadline.');
|
||||
if (!action.eventTimingSupported) throw new Error('Event Timing observation is unsupported.');
|
||||
const matching = action.eventEntries.filter((entry) => (
|
||||
entry.target === action.target
|
||||
&& entry.startTime >= action.dispatchTime
|
||||
&& entry.startTime <= action.dispatchTime + 500
|
||||
));
|
||||
if (matching.length === 0) return 16;
|
||||
const groups = new Map<number, number>();
|
||||
for (const entry of matching) {
|
||||
if (entry.interactionId === 0) continue;
|
||||
groups.set(entry.interactionId, Math.max(groups.get(entry.interactionId) ?? 0, entry.duration));
|
||||
}
|
||||
if (groups.size !== 1) throw new Error('Event Timing groups are ambiguous.');
|
||||
return [...groups.values()][0]!;
|
||||
});
|
||||
return { values, p95: nearestRankP95(values) };
|
||||
};
|
||||
|
||||
export const evaluateSceneContinuity = (
|
||||
intervals: readonly SceneInterval[],
|
||||
callbacks: readonly SceneFrameSample[],
|
||||
expectedFramePeriod: number,
|
||||
): { passes: boolean; expectedCallbacks: number; actualCallbacks: number } => {
|
||||
if (!(expectedFramePeriod > 0)) throw new Error('Expected frame period must be positive.');
|
||||
const eligible = intervals.filter((interval) => interval.end - interval.start >= expectedFramePeriod);
|
||||
let expectedCallbacks = 0;
|
||||
let actualCallbacks = 0;
|
||||
let boundariesPass = eligible.length > 0;
|
||||
for (const interval of eligible) {
|
||||
expectedCallbacks += Math.floor((interval.end - interval.start) / expectedFramePeriod);
|
||||
const inside = callbacks.filter((sample) => sample.start >= interval.start && sample.end <= interval.end);
|
||||
actualCallbacks += inside.length;
|
||||
if (inside.length === 0) {
|
||||
boundariesPass = false;
|
||||
continue;
|
||||
}
|
||||
if (inside[0]!.start - interval.start > 3 * expectedFramePeriod) boundariesPass = false;
|
||||
for (let index = 1; index < inside.length; index += 1) {
|
||||
if (inside[index]!.start - inside[index - 1]!.start > 3 * expectedFramePeriod) boundariesPass = false;
|
||||
}
|
||||
if (interval.end - inside.at(-1)!.end > 3 * expectedFramePeriod) boundariesPass = false;
|
||||
}
|
||||
const coverage = expectedCallbacks > 0 ? actualCallbacks / expectedCallbacks : 0;
|
||||
return { passes: boundariesPass && coverage >= 0.9, expectedCallbacks, actualCallbacks };
|
||||
};
|
||||
|
||||
export const calculateLostPresentationShare = (
|
||||
rafTimestamps: readonly number[],
|
||||
expectedFramePeriod: number,
|
||||
minimumOpportunities: number,
|
||||
): { opportunities: number; lost: number; share: number; hasSufficientCoverage: boolean } => {
|
||||
let opportunities = 0;
|
||||
let lost = 0;
|
||||
for (let index = 1; index < rafTimestamps.length; index += 1) {
|
||||
const interval = rafTimestamps[index]! - rafTimestamps[index - 1]!;
|
||||
const nextOpportunities = Math.max(1, Math.round(interval / expectedFramePeriod));
|
||||
opportunities += nextOpportunities;
|
||||
lost += nextOpportunities - 1;
|
||||
}
|
||||
return {
|
||||
opportunities,
|
||||
lost,
|
||||
share: opportunities === 0 ? 1 : lost / opportunities,
|
||||
hasSufficientCoverage: opportunities >= minimumOpportunities,
|
||||
};
|
||||
};
|
||||
|
||||
export const unexpectedLateShifts = (
|
||||
entries: readonly LayoutShiftSample[],
|
||||
firstSceneFrame: number,
|
||||
traceEnd: number,
|
||||
): LayoutShiftSample[] => entries.filter((entry) => (
|
||||
!entry.hadRecentInput && entry.startTime >= firstSceneFrame && entry.startTime < traceEnd
|
||||
));
|
||||
|
||||
interface PerformanceSnapshot {
|
||||
version: 'drive-trace.v1';
|
||||
lcp: readonly number[];
|
||||
layoutShifts: readonly LayoutShiftSample[];
|
||||
longTasks: readonly LongTaskSample[];
|
||||
pageRafs: readonly number[];
|
||||
sceneFrames: readonly SceneFrameSample[];
|
||||
sceneIntervals: readonly SceneInterval[];
|
||||
firstSceneFrame: number | null;
|
||||
traceStart: number | null;
|
||||
traceEnd: number | null;
|
||||
cutoff: number;
|
||||
navigationBarrier: { armed: boolean; pendingEpoch: number | null; cancelledEpoch: number | null };
|
||||
actions: readonly ScriptedActionSample[];
|
||||
}
|
||||
|
||||
interface PerformanceHooks {
|
||||
readonly version: 'drive-trace.v1';
|
||||
startTrace(): void;
|
||||
endTrace(): void;
|
||||
recordSceneFrame(start: number, end: number): void;
|
||||
recordSceneState(state: string): void;
|
||||
armNavigationSettlementBarrier(): void;
|
||||
registerNavigationSettlement(epoch: number): void;
|
||||
holdNavigationSettlement(epoch: number): boolean;
|
||||
noteNavigationEpoch(epoch: number): void;
|
||||
beginAction(id: string, selector: string): void;
|
||||
recordActionMutation(id: string): void;
|
||||
snapshot(): PerformanceSnapshot;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__PORTFOLIO_PERFORMANCE__?: PerformanceHooks;
|
||||
}
|
||||
}
|
||||
|
||||
const digest = async (value: string): Promise<string> => {
|
||||
const output = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
||||
return [...new Uint8Array(output)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
export const installAuthorizedPerformanceInstrumentation = async (): Promise<void> => {
|
||||
if (!TEST_BUILD) return;
|
||||
const url = new URL(location.href);
|
||||
if (url.searchParams.get('__portfolio_performance') !== 'drive-trace.v1') return;
|
||||
const token = url.searchParams.get('__portfolio_test_token');
|
||||
if (token === null || await digest(token) !== TEST_TOKEN_DIGEST) return;
|
||||
|
||||
const lcp: number[] = [];
|
||||
const layoutShifts: LayoutShiftSample[] = [];
|
||||
const longTasks: LongTaskSample[] = [];
|
||||
const pageRafs: number[] = [];
|
||||
const sceneFrames: SceneFrameSample[] = [];
|
||||
const sceneIntervals: SceneInterval[] = [];
|
||||
let firstSceneFrame: number | null = null;
|
||||
let traceStart: number | null = null;
|
||||
let traceEnd: number | null = null;
|
||||
let runningIntervalStart: number | null = null;
|
||||
let cutoff = Infinity;
|
||||
let sentinel = 0;
|
||||
let barrierArmed = false;
|
||||
let barrierPendingEpoch: number | null = null;
|
||||
let barrierCancelledEpoch: number | null = null;
|
||||
const actionRecords = new Map<string, ScriptedActionSample & { element: Element | null }>();
|
||||
|
||||
const observe = (type: string, callback: (entry: PerformanceEntry) => void): void => {
|
||||
if (typeof PerformanceObserver === 'undefined') return;
|
||||
try {
|
||||
const observer = new PerformanceObserver((list) => list.getEntries().forEach(callback));
|
||||
observer.observe({ type, buffered: true });
|
||||
} catch { /* unsupported entry type */ }
|
||||
};
|
||||
observe('largest-contentful-paint', (entry) => lcp.push(entry.startTime));
|
||||
observe('layout-shift', (entry) => {
|
||||
const shift = entry as PerformanceEntry & { value?: number; hadRecentInput?: boolean };
|
||||
layoutShifts.push({ startTime: shift.startTime, value: shift.value ?? 0, hadRecentInput: shift.hadRecentInput ?? false });
|
||||
});
|
||||
observe('longtask', (entry) => longTasks.push({ startTime: entry.startTime, duration: entry.duration }));
|
||||
let eventTimingSupported = false;
|
||||
if (typeof PerformanceObserver !== 'undefined') {
|
||||
try {
|
||||
const eventObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
const timed = entry as PerformanceEntry & { interactionId?: number; target?: Node | null };
|
||||
for (const record of actionRecords.values()) {
|
||||
if (timed.target === record.element) {
|
||||
(record.eventEntries as EventTimingSample[]).push({
|
||||
target: record.id,
|
||||
startTime: timed.startTime,
|
||||
duration: timed.duration,
|
||||
interactionId: timed.interactionId ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const eventOptions = { type: 'event', buffered: true, durationThreshold: 16 } as PerformanceObserverInit & { durationThreshold: number };
|
||||
eventObserver.observe(eventOptions);
|
||||
eventTimingSupported = true;
|
||||
} catch { /* Event Timing unsupported */ }
|
||||
}
|
||||
const cutOff = (event: Event): void => {
|
||||
if (event.isTrusted) cutoff = Math.min(cutoff, performance.now());
|
||||
};
|
||||
addEventListener('pointerdown', cutOff, { capture: true, once: true });
|
||||
addEventListener('keydown', cutOff, { capture: true, once: true });
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) cutoff = Math.min(cutoff, performance.now());
|
||||
});
|
||||
const sentinelFrame = (now: number): void => {
|
||||
if (traceStart !== null && traceEnd === null) pageRafs.push(now);
|
||||
sentinel = requestAnimationFrame(sentinelFrame);
|
||||
};
|
||||
sentinel = requestAnimationFrame(sentinelFrame);
|
||||
|
||||
const hooks: PerformanceHooks = {
|
||||
version: 'drive-trace.v1',
|
||||
startTrace() {
|
||||
traceStart = performance.now();
|
||||
traceEnd = null;
|
||||
pageRafs.length = 0;
|
||||
sceneFrames.length = 0;
|
||||
sceneIntervals.length = 0;
|
||||
runningIntervalStart = document.querySelector('.journey-world')?.getAttribute('data-scene-state') === 'running' ? traceStart : null;
|
||||
},
|
||||
endTrace() {
|
||||
traceEnd = performance.now();
|
||||
if (runningIntervalStart !== null) {
|
||||
sceneIntervals.push({ start: runningIntervalStart, end: traceEnd });
|
||||
runningIntervalStart = null;
|
||||
}
|
||||
cancelAnimationFrame(sentinel);
|
||||
},
|
||||
recordSceneFrame(start, end) {
|
||||
if (firstSceneFrame === null) firstSceneFrame = end;
|
||||
if (traceStart !== null && traceEnd === null) sceneFrames.push({ start, end });
|
||||
},
|
||||
recordSceneState(state) {
|
||||
const now = performance.now();
|
||||
if (traceStart === null || traceEnd !== null) return;
|
||||
if (state === 'running' && runningIntervalStart === null) runningIntervalStart = now;
|
||||
if (state !== 'running' && runningIntervalStart !== null) {
|
||||
sceneIntervals.push({ start: runningIntervalStart, end: now });
|
||||
runningIntervalStart = null;
|
||||
}
|
||||
},
|
||||
armNavigationSettlementBarrier() {
|
||||
barrierArmed = true;
|
||||
barrierPendingEpoch = null;
|
||||
barrierCancelledEpoch = null;
|
||||
},
|
||||
registerNavigationSettlement(epoch) {
|
||||
if (barrierArmed) barrierPendingEpoch = epoch;
|
||||
},
|
||||
holdNavigationSettlement(epoch) {
|
||||
return barrierArmed && barrierPendingEpoch === epoch;
|
||||
},
|
||||
noteNavigationEpoch(epoch) {
|
||||
if (barrierArmed && barrierPendingEpoch !== null && barrierPendingEpoch !== epoch) {
|
||||
barrierCancelledEpoch = barrierPendingEpoch;
|
||||
barrierPendingEpoch = null;
|
||||
barrierArmed = false;
|
||||
}
|
||||
},
|
||||
beginAction(id, selector) {
|
||||
if (!exactActionIds.has(id) || actionRecords.has(id)) throw new Error('The performance action is invalid or duplicated.');
|
||||
const element = document.querySelector(selector);
|
||||
if (!(element instanceof Element)) throw new Error('The performance action target is unavailable.');
|
||||
actionRecords.set(id, {
|
||||
id,
|
||||
target: id,
|
||||
dispatchTime: performance.now(),
|
||||
acknowledgementCount: 0,
|
||||
mutationTime: null,
|
||||
doubleRafTime: null,
|
||||
eventTimingSupported,
|
||||
eventEntries: [],
|
||||
element,
|
||||
});
|
||||
},
|
||||
recordActionMutation(id) {
|
||||
const record = actionRecords.get(id);
|
||||
if (record === undefined) return;
|
||||
record.acknowledgementCount += 1;
|
||||
record.mutationTime = performance.now();
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => { record.doubleRafTime = performance.now(); }));
|
||||
},
|
||||
snapshot: () => Object.freeze({
|
||||
version: 'drive-trace.v1' as const,
|
||||
lcp: Object.freeze([...lcp]),
|
||||
layoutShifts: Object.freeze([...layoutShifts]),
|
||||
longTasks: Object.freeze([...longTasks]),
|
||||
pageRafs: Object.freeze([...pageRafs]),
|
||||
sceneFrames: Object.freeze([...sceneFrames]),
|
||||
sceneIntervals: Object.freeze([...sceneIntervals]),
|
||||
firstSceneFrame,
|
||||
traceStart,
|
||||
traceEnd,
|
||||
cutoff,
|
||||
navigationBarrier: Object.freeze({ armed: barrierArmed, pendingEpoch: barrierPendingEpoch, cancelledEpoch: barrierCancelledEpoch }),
|
||||
actions: Object.freeze([...actionRecords.values()].map(({ element: _element, ...record }) => Object.freeze({
|
||||
...record,
|
||||
eventEntries: Object.freeze([...record.eventEntries]),
|
||||
}))),
|
||||
}),
|
||||
};
|
||||
Object.defineProperty(window, '__PORTFOLIO_PERFORMANCE__', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: Object.freeze(hooks),
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user