perf: enforce journey budgets and reference traces

This commit is contained in:
2026-09-02 07:48:02 +03:00
parent 8089d061ec
commit f4cf9fc77e
25 changed files with 1432 additions and 23 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>