diff --git a/src/App.tsx b/src/App.tsx index 6e080cad..37abd14b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -106,6 +106,7 @@ import { usePreviewStore } from './store/previewStore'; import { DEFAULT_IN_APP_BINDINGS, canRunShortcutActionInMiniWindow, executeCliPlayerCommand, executeRuntimeAction, isGlobalShortcutActionId, isShortcutAction } from './config/shortcutActions'; import { matchInAppShortcutAction } from './shortcuts/runtime'; import ZipDownloadOverlay from './components/ZipDownloadOverlay'; +import FpsOverlay from './components/FpsOverlay'; import PasteClipboardHandler from './components/PasteClipboardHandler'; import { usePerfProbeFlags } from './utils/perfFlags'; @@ -1328,6 +1329,7 @@ export default function App() { {!perfFlags.disableTooltipPortal && } + ); } @@ -1418,6 +1420,7 @@ export default function App() { {exportPickerOpen && setExportPickerOpen(false)} />} + ); diff --git a/src/components/FpsOverlay.tsx b/src/components/FpsOverlay.tsx new file mode 100644 index 00000000..7bb36d61 --- /dev/null +++ b/src/components/FpsOverlay.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { usePerfProbeFlag } from '../utils/perfFlags'; + +const SAMPLE_MS = 500; + +/** FPS from rAF callbacks over sliding ~500ms windows; only runs when Performance Probe enables the overlay. */ +export default function FpsOverlay() { + const showFpsOverlay = usePerfProbeFlag('showFpsOverlay'); + const [fps, setFps] = useState(0); + + useEffect(() => { + if (!showFpsOverlay) { + setFps(0); + return; + } + + let frames = 0; + let lastReport = performance.now(); + let rafId = 0; + + const loop = () => { + frames++; + const now = performance.now(); + if (now - lastReport >= SAMPLE_MS) { + const elapsedSec = (now - lastReport) / 1000; + setFps(Math.round(frames / elapsedSec)); + frames = 0; + lastReport = now; + } + rafId = requestAnimationFrame(loop); + }; + + rafId = requestAnimationFrame(loop); + return () => cancelAnimationFrame(rafId); + }, [showFpsOverlay]); + + if (!showFpsOverlay) return null; + + return createPortal( + + {fps} + {' '} + FPS + , + document.body, + ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 61a96fc7..b3bdb11e 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1112,6 +1112,14 @@ export default function Sidebar({ Temporary runtime switches to estimate UI effect cost. + + setPerfProbeFlag('showFpsOverlay', e.target.checked)} + /> + Show FPS overlay (requestAnimationFrame rate) + Live CPU (approx) {perfCpu == null ? ( @@ -1242,7 +1250,7 @@ export default function Sidebar({ /> Disable central route content mount - + Shared mainstage layers (multiple pages) - + Home (`/`) - + Tracks (`/tracks`) - + Albums (`/albums`) - + Phase 3 — Active diagnostics (quick access) DEFAULT_FLAGS); } + +/** Subscribe to a single probe flag so unrelated toggles do not re-render the consumer. */ +export function usePerfProbeFlag(key: K): PerfProbeFlags[K] { + return useSyncExternalStore( + subscribePerfProbeFlags, + () => getPerfProbeFlags()[key], + () => DEFAULT_FLAGS[key], + ); +}
Temporary runtime switches to estimate UI effect cost.