Feat/performance probe fps overlay (#472)

* feat(perf-probe): add FPS overlay toggle and tidy probe modal

Add optional rAF-based FPS readout controlled by a persisted probe flag.
Remove the separate keyboard shortcut. Collapse all phase sections by default.

* perf(fps-overlay): subscribe only to showFpsOverlay flag

Add usePerfProbeFlag so the overlay does not re-render when other probe
toggles change. Track the animation frame id with a loop-local variable.
This commit is contained in:
cucadmuh
2026-05-06 02:13:59 +03:00
committed by GitHub
parent c3d37546cf
commit de3c0d9da1
5 changed files with 100 additions and 5 deletions
+3
View File
@@ -106,6 +106,7 @@ import { usePreviewStore } from './store/previewStore';
import { DEFAULT_IN_APP_BINDINGS, canRunShortcutActionInMiniWindow, executeCliPlayerCommand, executeRuntimeAction, isGlobalShortcutActionId, isShortcutAction } from './config/shortcutActions'; import { DEFAULT_IN_APP_BINDINGS, canRunShortcutActionInMiniWindow, executeCliPlayerCommand, executeRuntimeAction, isGlobalShortcutActionId, isShortcutAction } from './config/shortcutActions';
import { matchInAppShortcutAction } from './shortcuts/runtime'; import { matchInAppShortcutAction } from './shortcuts/runtime';
import ZipDownloadOverlay from './components/ZipDownloadOverlay'; import ZipDownloadOverlay from './components/ZipDownloadOverlay';
import FpsOverlay from './components/FpsOverlay';
import PasteClipboardHandler from './components/PasteClipboardHandler'; import PasteClipboardHandler from './components/PasteClipboardHandler';
import { usePerfProbeFlags } from './utils/perfFlags'; import { usePerfProbeFlags } from './utils/perfFlags';
@@ -1328,6 +1329,7 @@ export default function App() {
<MiniPlayer /> <MiniPlayer />
<GlobalConfirmModal /> <GlobalConfirmModal />
{!perfFlags.disableTooltipPortal && <TooltipPortal />} {!perfFlags.disableTooltipPortal && <TooltipPortal />}
<FpsOverlay />
</DragDropProvider> </DragDropProvider>
); );
} }
@@ -1418,6 +1420,7 @@ export default function App() {
</Suspense> </Suspense>
{exportPickerOpen && <ExportPickerModal onConfirm={handleExport} onClose={() => setExportPickerOpen(false)} />} {exportPickerOpen && <ExportPickerModal onConfirm={handleExport} onClose={() => setExportPickerOpen(false)} />}
<ZipDownloadOverlay /> <ZipDownloadOverlay />
<FpsOverlay />
</BrowserRouter> </BrowserRouter>
</WindowVisibilityProvider> </WindowVisibilityProvider>
); );
+48
View File
@@ -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(
<div className="fps-overlay" aria-hidden="true">
{fps}
{' '}
<span className="fps-overlay__unit">FPS</span>
</div>,
document.body,
);
}
+13 -5
View File
@@ -1112,6 +1112,14 @@ export default function Sidebar({
<p className="sidebar-perf-modal__hint"> <p className="sidebar-perf-modal__hint">
Temporary runtime switches to estimate UI effect cost. Temporary runtime switches to estimate UI effect cost.
</p> </p>
<label className="sidebar-perf-modal__item">
<input
type="checkbox"
checked={perfFlags.showFpsOverlay}
onChange={e => setPerfProbeFlag('showFpsOverlay', e.target.checked)}
/>
<span>Show FPS overlay (requestAnimationFrame rate)</span>
</label>
<div className="sidebar-perf-modal__cpu"> <div className="sidebar-perf-modal__cpu">
<div className="sidebar-perf-modal__cpu-title">Live CPU (approx)</div> <div className="sidebar-perf-modal__cpu-title">Live CPU (approx)</div>
{perfCpu == null ? ( {perfCpu == null ? (
@@ -1242,7 +1250,7 @@ export default function Sidebar({
/> />
<span>Disable central route content mount</span> <span>Disable central route content mount</span>
</label> </label>
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested" open> <details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
<summary className="sidebar-perf-modal__phase-title">Shared mainstage layers (multiple pages)</summary> <summary className="sidebar-perf-modal__phase-title">Shared mainstage layers (multiple pages)</summary>
<label className="sidebar-perf-modal__item"> <label className="sidebar-perf-modal__item">
<input <input
@@ -1254,7 +1262,7 @@ export default function Sidebar({
</label> </label>
</details> </details>
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested" open> <details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
<summary className="sidebar-perf-modal__phase-title">Home (`/`)</summary> <summary className="sidebar-perf-modal__phase-title">Home (`/`)</summary>
<label className="sidebar-perf-modal__item"> <label className="sidebar-perf-modal__item">
<input <input
@@ -1346,7 +1354,7 @@ export default function Sidebar({
</label> </label>
</details> </details>
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested" open> <details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
<summary className="sidebar-perf-modal__phase-title">Tracks (`/tracks`)</summary> <summary className="sidebar-perf-modal__phase-title">Tracks (`/tracks`)</summary>
<label className="sidebar-perf-modal__item"> <label className="sidebar-perf-modal__item">
<input <input
@@ -1390,7 +1398,7 @@ export default function Sidebar({
</label> </label>
</details> </details>
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested" open> <details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
<summary className="sidebar-perf-modal__phase-title">Albums (`/albums`)</summary> <summary className="sidebar-perf-modal__phase-title">Albums (`/albums`)</summary>
<label className="sidebar-perf-modal__item"> <label className="sidebar-perf-modal__item">
<input <input
@@ -1402,7 +1410,7 @@ export default function Sidebar({
</label> </label>
</details> </details>
</details> </details>
<details className="sidebar-perf-modal__phase" open> <details className="sidebar-perf-modal__phase">
<summary className="sidebar-perf-modal__phase-title">Phase 3 Active diagnostics (quick access)</summary> <summary className="sidebar-perf-modal__phase-title">Phase 3 Active diagnostics (quick access)</summary>
<label className="sidebar-perf-modal__item"> <label className="sidebar-perf-modal__item">
<input <input
+24
View File
@@ -13744,3 +13744,27 @@ html[data-perf-disable-home-artwork-clip="true"] .home-lite-artwork .song-card-c
background: color-mix(in srgb, var(--accent) 26%, transparent); background: color-mix(in srgb, var(--accent) 26%, transparent);
border-color: color-mix(in srgb, var(--accent) 50%, transparent); border-color: color-mix(in srgb, var(--accent) 50%, transparent);
} }
/* FPS overlay (Performance Probe → Show FPS overlay) */
.fps-overlay {
position: fixed;
top: 10px;
right: 10px;
z-index: 100003;
pointer-events: none;
padding: 6px 10px;
border-radius: 6px;
font-family: var(--font-mono, ui-monospace, 'Courier New', monospace);
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--text-primary, #fff);
background: color-mix(in srgb, var(--ctp-crust, #111) 82%, transparent);
border: 1px solid color-mix(in srgb, var(--border-subtle, #444) 70%, transparent);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
}
.fps-overlay__unit {
font-size: 10px;
color: var(--text-muted, #888);
letter-spacing: 0.02em;
}
+12
View File
@@ -1,6 +1,8 @@
import { useSyncExternalStore } from 'react'; import { useSyncExternalStore } from 'react';
export type PerfProbeFlags = { export type PerfProbeFlags = {
/** On-screen rAF-based FPS counter (Performance Probe). */
showFpsOverlay: boolean;
disableWaveformCanvas: boolean; disableWaveformCanvas: boolean;
disablePlayerProgressUi: boolean; disablePlayerProgressUi: boolean;
disableMarqueeScroll: boolean; disableMarqueeScroll: boolean;
@@ -29,6 +31,7 @@ export type PerfProbeFlags = {
const STORAGE_KEY = 'psysonic_perf_probe_flags_v1'; const STORAGE_KEY = 'psysonic_perf_probe_flags_v1';
const DEFAULT_FLAGS: PerfProbeFlags = { const DEFAULT_FLAGS: PerfProbeFlags = {
showFpsOverlay: false,
disableWaveformCanvas: false, disableWaveformCanvas: false,
disablePlayerProgressUi: false, disablePlayerProgressUi: false,
disableMarqueeScroll: false, disableMarqueeScroll: false,
@@ -147,3 +150,12 @@ export function resetPerfProbeFlags(): void {
export function usePerfProbeFlags(): PerfProbeFlags { export function usePerfProbeFlags(): PerfProbeFlags {
return useSyncExternalStore(subscribePerfProbeFlags, getPerfProbeFlags, () => DEFAULT_FLAGS); return useSyncExternalStore(subscribePerfProbeFlags, getPerfProbeFlags, () => DEFAULT_FLAGS);
} }
/** Subscribe to a single probe flag so unrelated toggles do not re-render the consumer. */
export function usePerfProbeFlag<K extends keyof PerfProbeFlags>(key: K): PerfProbeFlags[K] {
return useSyncExternalStore(
subscribePerfProbeFlags,
() => getPerfProbeFlags()[key],
() => DEFAULT_FLAGS[key],
);
}