perf: enforce journey budgets and reference traces
This commit is contained in:
@@ -20,6 +20,9 @@ import type { PortfolioContent, SectionId } from '@/content/types';
|
||||
import type { SceneHostState } from '@/composables/useSceneController';
|
||||
import type { MotionPolicy, QualityTier } from '@/journey/motion-policy';
|
||||
|
||||
declare const __PORTFOLIO_TEST_BUILD__: boolean;
|
||||
const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__;
|
||||
|
||||
const props = defineProps<{ content: Readonly<PortfolioContent> }>();
|
||||
|
||||
const canonicalSectionIds = [
|
||||
@@ -57,7 +60,13 @@ const adaptive = useAdaptiveQuality({
|
||||
sceneState,
|
||||
pageLifecycle,
|
||||
});
|
||||
const actualQuality = computed(() => adaptive.snapshot.value.actual);
|
||||
const requestedPerformanceTier = TEST_BUILD
|
||||
? new URL(window.location.href).searchParams.get('__portfolio_tier')
|
||||
: null;
|
||||
const pinnedPerformanceTier = requestedPerformanceTier === 'high' || requestedPerformanceTier === 'low'
|
||||
? requestedPerformanceTier
|
||||
: null;
|
||||
const actualQuality = computed(() => pinnedPerformanceTier ?? adaptive.snapshot.value.actual);
|
||||
const activeSectionLabel = computed(() => sectionLabel(journey.value.activeSectionId));
|
||||
const serverDefaultPolicy: MotionPolicy = {
|
||||
allowSceneRequest: false,
|
||||
@@ -70,6 +79,16 @@ const displayedQuality = computed<QualityTier>(() => controlsReady.value ? actua
|
||||
const displayedSceneState = computed(() => controlsReady.value ? sceneState.value : 'running');
|
||||
let removePageListeners = (): void => {};
|
||||
|
||||
const sampleSceneFrame = (start: number, end: number): void => {
|
||||
if (pinnedPerformanceTier === null) adaptive.sampleFrame(start, end);
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.recordSceneFrame(start, end);
|
||||
};
|
||||
|
||||
const updateSceneState = (state: SceneHostState): void => {
|
||||
sceneState.value = state;
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.recordSceneState(state);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => journey.value.activeSectionId,
|
||||
(id) => navigation.notePassiveSection(id),
|
||||
@@ -100,6 +119,28 @@ const enableMotion = (): void => {
|
||||
const resumeMotion = (): void => {
|
||||
motion.setPaused(false);
|
||||
adaptive.setMaximum(motion.policy.value.maximumTier, 'manual');
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.recordActionMutation('resume');
|
||||
};
|
||||
|
||||
const pauseMotion = (): void => {
|
||||
motion.setPaused(true);
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.recordActionMutation('pause');
|
||||
};
|
||||
|
||||
const startDrive = (): void => {
|
||||
audio.startFromGesture();
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.recordActionMutation('start');
|
||||
};
|
||||
|
||||
const toggleAudioMuted = (): void => {
|
||||
const action = audio.muted.value ? 'unmute' : 'mute';
|
||||
audio.toggleMuted();
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.recordActionMutation(action);
|
||||
};
|
||||
|
||||
const updateAudioVolume = (value: number): void => {
|
||||
audio.setVolume(value);
|
||||
if (TEST_BUILD && value === 0.35) window.__PORTFOLIO_PERFORMANCE__?.recordActionMutation('volume');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -126,8 +167,8 @@ onUnmounted(() => removePageListeners());
|
||||
:policy="motion.policy.value"
|
||||
:quality="actualQuality"
|
||||
:scene-description="content.sceneDescription"
|
||||
:sample-frame="adaptive.sampleFrame"
|
||||
@state-change="sceneState = $event"
|
||||
:sample-frame="sampleSceneFrame"
|
||||
@state-change="updateSceneState"
|
||||
/>
|
||||
<JourneyHud
|
||||
:journey="journey"
|
||||
@@ -203,13 +244,13 @@ onUnmounted(() => removePageListeners());
|
||||
:audio-state="audio.state.value"
|
||||
:audio-muted="audio.muted.value"
|
||||
:audio-volume="controlsReady ? motion.volume.value : 0.4"
|
||||
@pause="motion.setPaused(true)"
|
||||
@pause="pauseMotion"
|
||||
@resume="resumeMotion"
|
||||
@enable="enableMotion"
|
||||
@quality-change="setQuality"
|
||||
@audio-start="audio.startFromGesture"
|
||||
@audio-start="startDrive"
|
||||
@audio-resume="audio.resumeFromGesture"
|
||||
@audio-mute-toggle="audio.toggleMuted"
|
||||
@audio-volume-change="audio.setVolume"
|
||||
@audio-mute-toggle="toggleAudioMuted"
|
||||
@audio-volume-change="updateAudioVolume"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -86,9 +86,6 @@ const changeVolume = (event: Event): void => {
|
||||
type="button"
|
||||
@click="emit('audioResume')"
|
||||
>Resume sound</button>
|
||||
<button type="button" @click="emit('audioMuteToggle')">
|
||||
{{ audioMuted ? 'Unmute sound' : 'Mute sound' }}
|
||||
</button>
|
||||
<label>
|
||||
<span>Sound volume</span>
|
||||
<input
|
||||
@@ -101,6 +98,9 @@ const changeVolume = (event: Event): void => {
|
||||
@input="changeVolume"
|
||||
>
|
||||
</label>
|
||||
<button type="button" @click="emit('audioMuteToggle')">
|
||||
{{ audioMuted ? 'Unmute sound' : 'Mute sound' }}
|
||||
</button>
|
||||
</div>
|
||||
<label>
|
||||
<span>Quality</span>
|
||||
|
||||
@@ -7,6 +7,17 @@ const props = defineProps<{
|
||||
journey: JourneySnapshot;
|
||||
onSectionLinkClick: (event: MouseEvent) => void;
|
||||
}>();
|
||||
|
||||
const compactLabels: Readonly<Record<SectionId, string>> = {
|
||||
start: 'Start',
|
||||
profile: 'Profile',
|
||||
experience: 'Career',
|
||||
infrastructure: 'Infra',
|
||||
'selected-builds': 'Builds',
|
||||
'additional-work': 'More work',
|
||||
toolkit: 'Toolkit',
|
||||
contact: 'Contact',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -15,11 +26,13 @@ const props = defineProps<{
|
||||
<li v-for="item in items" :key="item.id">
|
||||
<a
|
||||
:href="`#${item.id}`"
|
||||
:aria-label="item.label"
|
||||
:data-compact-label="compactLabels[item.id]"
|
||||
:aria-current="
|
||||
props.journey.activeSectionId === item.id ? 'page' : undefined
|
||||
"
|
||||
@click="props.onSectionLinkClick"
|
||||
>{{ item.label }}</a
|
||||
><span class="site-navigation__full-label" aria-hidden="true">{{ item.label }}</span></a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -13,6 +13,14 @@ if (requestsTestFault) {
|
||||
await installAuthorizedTestHooks();
|
||||
}
|
||||
|
||||
const requestsPerformanceTrace = TEST_BUILD
|
||||
&& new URL(location.href).searchParams.get('__portfolio_performance') === 'drive-trace.v1'
|
||||
&& new URL(location.href).searchParams.has('__portfolio_test_token');
|
||||
if (requestsPerformanceTrace) {
|
||||
const { installAuthorizedPerformanceInstrumentation } = await import('./testing/performance-instrumentation');
|
||||
await installAuthorizedPerformanceInstrumentation();
|
||||
}
|
||||
|
||||
export const readProductionInputsSnapshot = (source: Document): Readonly<ProductionInputs> => {
|
||||
const snapshot = source.getElementById('portfolio-inputs');
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { SectionId } from "@/content/types";
|
||||
import type { JourneySource } from "./types";
|
||||
|
||||
declare const __PORTFOLIO_TEST_BUILD__: boolean;
|
||||
const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__;
|
||||
|
||||
export interface NavigationState {
|
||||
epoch: number;
|
||||
source: JourneySource;
|
||||
@@ -162,6 +165,10 @@ export function createNavigationController(
|
||||
source,
|
||||
destinationId,
|
||||
};
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.noteNavigationEpoch(currentState.epoch);
|
||||
if (TEST_BUILD && source === 'explicit-navigation' && (destinationId === 'toolkit' || destinationId === 'contact')) {
|
||||
window.__PORTFOLIO_PERFORMANCE__?.recordActionMutation(destinationId);
|
||||
}
|
||||
emit();
|
||||
return currentState.epoch;
|
||||
};
|
||||
@@ -184,6 +191,7 @@ export function createNavigationController(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (TEST_BUILD && window.__PORTFOLIO_PERFORMANCE__?.holdNavigationSettlement(epoch)) return;
|
||||
cancelSettle();
|
||||
frameId = runtime.requestFrame(() => {
|
||||
frameId = undefined;
|
||||
@@ -202,6 +210,7 @@ export function createNavigationController(
|
||||
id: SectionId,
|
||||
target: HTMLElement,
|
||||
): void => {
|
||||
if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.registerNavigationSettlement(epoch);
|
||||
settleTimerId = runtime.setTimer(() => {
|
||||
settleTimerId = undefined;
|
||||
if (
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
padding: var(--space-4) 4vw var(--space-3);
|
||||
padding: calc(var(--space-4) + 44px) 4vw var(--space-3);
|
||||
border-block-end: var(--panel-border);
|
||||
background: #0b111b;
|
||||
}
|
||||
@@ -21,14 +21,20 @@
|
||||
}
|
||||
|
||||
.site-navigation {
|
||||
margin-block-start: var(--space-1);
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
inset: 0 0 auto;
|
||||
min-block-size: 44px;
|
||||
border-block-end: var(--panel-border);
|
||||
background: rgb(7 13 23 / 94%);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.site-navigation ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 0.25rem;
|
||||
max-inline-size: 78rem;
|
||||
max-inline-size: 90rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
@@ -38,7 +44,8 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-block-size: 44px;
|
||||
padding-inline: 0.7rem;
|
||||
justify-content: center;
|
||||
padding-inline: 0.4rem;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--mono);
|
||||
font-size: var(--step--1);
|
||||
@@ -63,7 +70,7 @@
|
||||
min-width: 0;
|
||||
margin: 0 1rem var(--space-4);
|
||||
padding: var(--space-3);
|
||||
scroll-margin-block: var(--space-3);
|
||||
scroll-margin-block-start: calc(44px + var(--space-3));
|
||||
border: var(--panel-border);
|
||||
border-radius: 2px;
|
||||
background: var(--surface);
|
||||
@@ -89,6 +96,26 @@
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.site-header {
|
||||
padding-block-start: calc(var(--space-4) + 88px);
|
||||
}
|
||||
|
||||
.site-navigation ul {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.site-navigation__full-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-navigation a::after {
|
||||
content: attr(data-compact-label);
|
||||
}
|
||||
|
||||
.portfolio-section {
|
||||
scroll-margin-block-start: calc(88px + var(--space-3));
|
||||
}
|
||||
|
||||
.project-link,
|
||||
.contact-link,
|
||||
.contact-social-link {
|
||||
|
||||
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