mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
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:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user