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.
|
* Uses WebGL2 when available (with ambient motion), falls back to CSS blur.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import {
|
import {
|
||||||
BUILD_IMMERSIVE_ENABLED,
|
BUILD_IMMERSIVE_ENABLED,
|
||||||
isWebGL2Available,
|
isWebGL2Available,
|
||||||
@@ -45,8 +45,47 @@
|
|||||||
let isTransitioning = $state(false);
|
let isTransitioning = $state(false);
|
||||||
let previousArtwork = $state('');
|
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
|
// Check capabilities and load WebGL component if available
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// Listen for performance watchdog degradation events
|
||||||
|
window.addEventListener('immersive:background-degraded', handleBackgroundDegraded);
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
backgroundMode = config.backgroundMode ?? 'full';
|
backgroundMode = config.backgroundMode ?? 'full';
|
||||||
|
|
||||||
@@ -90,6 +129,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('immersive:background-degraded', handleBackgroundDegraded);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle WebGL fallback (if WebGL init fails)
|
* Handle WebGL fallback (if WebGL init fails)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
shouldImmersiveBeAvailable,
|
shouldImmersiveBeAvailable,
|
||||||
getWebGL2Info,
|
getWebGL2Info,
|
||||||
getConfig,
|
getConfig,
|
||||||
|
setConfig,
|
||||||
setRuntimeEnabled as setConfigRuntimeEnabled,
|
setRuntimeEnabled as setConfigRuntimeEnabled,
|
||||||
} from './config';
|
} from './config';
|
||||||
import type {
|
import type {
|
||||||
@@ -23,7 +24,9 @@ import type {
|
|||||||
ImmersiveBackend,
|
ImmersiveBackend,
|
||||||
UnavailableReason,
|
UnavailableReason,
|
||||||
ImmersiveMetrics,
|
ImmersiveMetrics,
|
||||||
|
BackgroundMode,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { showToast } from '$lib/stores/toastStore';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// State Store
|
// State Store
|
||||||
@@ -59,6 +62,14 @@ let lastFpsUpdate = 0;
|
|||||||
let currentFps = 0;
|
let currentFps = 0;
|
||||||
let lastFrameTime = 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.
|
* Update metrics from render loop.
|
||||||
* Called by ImmersiveAmbientCanvas on each frame.
|
* 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';
|
const renderer = gpuInfo?.renderer ?? 'Unknown GPU';
|
||||||
console.log(`[Immersive] Initializing with WebGL2 - ${renderer}`);
|
console.log(`[Immersive] Initializing with WebGL2 - ${renderer}`);
|
||||||
|
|
||||||
|
// Reset performance watchdog
|
||||||
|
perfWatchdogStart = performance.now();
|
||||||
|
lowFpsConsecutive = 0;
|
||||||
|
perfDegraded = false;
|
||||||
|
|
||||||
// Set initial state
|
// Set initial state
|
||||||
stateStore.set({
|
stateStore.set({
|
||||||
active: true,
|
active: true,
|
||||||
@@ -216,10 +290,12 @@ export async function init(): Promise<boolean> {
|
|||||||
export function destroy(): void {
|
export function destroy(): void {
|
||||||
console.log('[Immersive] Destroyed');
|
console.log('[Immersive] Destroyed');
|
||||||
|
|
||||||
// Reset metrics
|
// Reset metrics and watchdog
|
||||||
frameCount = 0;
|
frameCount = 0;
|
||||||
lastFpsUpdate = 0;
|
lastFpsUpdate = 0;
|
||||||
currentFps = 0;
|
currentFps = 0;
|
||||||
|
perfWatchdogStart = 0;
|
||||||
|
lowFpsConsecutive = 0;
|
||||||
|
|
||||||
stateStore.set({
|
stateStore.set({
|
||||||
active: false,
|
active: false,
|
||||||
|
|||||||
Reference in new issue
Block a user