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:
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)
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user