feat(immersive): auto-degrade background on sustained low FPS

Add performance watchdog to ImmersiveRenderer that monitors FPS after
a 3-second warmup period. If FPS stays below 15 for 5 consecutive
seconds, automatically degrades background mode (Full -> Lite -> Off),
persists the setting, and shows a toast notification.

ImmersiveBackground listens for the degradation event and switches
renderer in-place without requiring a full remount.
This commit is contained in:
vicrodh committed 2026-03-08 21:49:35 -06:00
1 parent 776987856a
commit 017a081f7f
2 files changed
+121 -2

No files matched your search

@@ -6,7 +6,7 @@
* Uses WebGL2 when available (with ambient motion), falls back to CSS blur.
*/
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import {
BUILD_IMMERSIVE_ENABLED,
isWebGL2Available,
@@ -45,8 +45,47 @@
let isTransitioning = $state(false);
let previousArtwork = $state('');
/**
* Switch to a degraded background mode (called by performance watchdog).
*/
async function switchToMode(mode: BackgroundMode): Promise<void> {
console.log(`[ImmersiveBackground] Switching to mode: ${mode}`);
// Reset all mode flags
useWebGL = false;
useFallback = false;
useSolidColor = false;
useLiteMode = false;
WebGLCanvas = null;
backgroundMode = mode;
if (mode === 'off') {
useSolidColor = true;
if (artwork) extractDominantColor(artwork);
} else if (mode === 'lite') {
useLiteMode = true;
if (artwork) {
try {
liteImageUrl = await generateAtmosphere(artwork);
} catch (err) {
console.warn('[ImmersiveBackground] Lite mode texture failed:', err);
}
}
}
}
function handleBackgroundDegraded(event: Event): void {
const detail = (event as CustomEvent).detail;
if (detail?.to) {
switchToMode(detail.to as BackgroundMode);
}
}
// Check capabilities and load WebGL component if available
onMount(async () => {
// Listen for performance watchdog degradation events
window.addEventListener('immersive:background-degraded', handleBackgroundDegraded);
const config = getConfig();
backgroundMode = config.backgroundMode ?? 'full';
@@ -90,6 +129,10 @@
}
});
onDestroy(() => {
window.removeEventListener('immersive:background-degraded', handleBackgroundDegraded);
});
/**
* Handle WebGL fallback (if WebGL init fails)
*/
+77 -1
View File
@@ -16,6 +16,7 @@ import {
shouldImmersiveBeAvailable,
getWebGL2Info,
getConfig,
setConfig,
setRuntimeEnabled as setConfigRuntimeEnabled,
} from './config';
import type {
@@ -23,7 +24,9 @@ import type {
ImmersiveBackend,
UnavailableReason,
ImmersiveMetrics,
BackgroundMode,
} from './types';
import { showToast } from '$lib/stores/toastStore';
// ============================================================================
// State Store
@@ -59,6 +62,14 @@ let lastFpsUpdate = 0;
let currentFps = 0;
let lastFrameTime = 0;
// Performance watchdog: auto-degrade background mode on sustained low FPS
const PERF_FPS_THRESHOLD = 15; // Below this = performance issue (real failures drop to single digits)
const PERF_WARMUP_MS = 3000; // Ignore first 3s (texture loading, init)
const PERF_SUSTAIN_SECONDS = 5; // Must be sustained for 5s before acting
let perfWatchdogStart = 0; // When immersive was initialized
let lowFpsConsecutive = 0; // Consecutive 1-second ticks below threshold
let perfDegraded = false; // Already degraded this session (don't loop)
/**
* Update metrics from render loop.
* Called by ImmersiveAmbientCanvas on each frame.
@@ -85,6 +96,64 @@ export function updateMetrics(frameTimeMs: number): void {
},
};
});
// Performance watchdog (only after warmup, only once per session)
if (!perfDegraded && perfWatchdogStart > 0 && (now - perfWatchdogStart) > PERF_WARMUP_MS) {
if (currentFps > 0 && currentFps < PERF_FPS_THRESHOLD) {
lowFpsConsecutive++;
if (lowFpsConsecutive >= PERF_SUSTAIN_SECONDS) {
degradeBackgroundMode();
}
} else {
// Reset counter if FPS recovers
lowFpsConsecutive = 0;
}
}
}
}
/**
* Auto-degrade background mode due to detected performance issues.
* Full → Lite → Off. Persists the change and notifies the user.
*/
function degradeBackgroundMode(): void {
const config = getConfig();
const currentMode: BackgroundMode = config.backgroundMode ?? 'full';
let newMode: BackgroundMode;
if (currentMode === 'full') {
newMode = 'lite';
} else if (currentMode === 'lite') {
newMode = 'off';
} else {
// Already off, nothing to degrade to
perfDegraded = true;
return;
}
console.warn(
`[Immersive] Performance watchdog: ${currentFps} FPS for ${PERF_SUSTAIN_SECONDS}s, ` +
`degrading background ${currentMode} → ${newMode}`
);
// Persist the change
setConfig({ backgroundMode: newMode });
perfDegraded = true;
lowFpsConsecutive = 0;
// Notify user
const modeLabel = newMode === 'lite' ? 'Lite' : 'Off';
showToast(
`Low FPS detected — background switched to ${modeLabel} for better performance`,
'info',
5000
);
// Emit event so ImmersiveBackground can react without full remount
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('immersive:background-degraded', {
detail: { from: currentMode, to: newMode }
}));
}
}
@@ -193,6 +262,11 @@ export async function init(): Promise<boolean> {
const renderer = gpuInfo?.renderer ?? 'Unknown GPU';
console.log(`[Immersive] Initializing with WebGL2 - ${renderer}`);
// Reset performance watchdog
perfWatchdogStart = performance.now();
lowFpsConsecutive = 0;
perfDegraded = false;
// Set initial state
stateStore.set({
active: true,
@@ -216,10 +290,12 @@ export async function init(): Promise<boolean> {
export function destroy(): void {
console.log('[Immersive] Destroyed');
// Reset metrics
// Reset metrics and watchdog
frameCount = 0;
lastFpsUpdate = 0;
currentFps = 0;
perfWatchdogStart = 0;
lowFpsConsecutive = 0;
stateStore.set({
active: false,