mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(browse,cover,perf): lazy catalogs, cover pipeline, and Performance Probe (#890)
* fix(cover): per-server cache stats and cover pipeline perf probe Stop count_cached_cover_ids from borrowing sibling bucket counts so Settings progress no longer attributes one server's disk cache to another. Add cover pipeline queue stats (ui ensure queue, ui vs lib HTTP/WebP semaphores) to Performance Probe overlay, with clearer ui/lib labels. * fix(browse): stabilize in-page infinite scroll and cap cover memory caches Extract useInpageScrollSentinel for album grids and song lists so sentinel reconnects do not spam loadMore during scroll. Harden useAlbumBrowseData with sync loading refs, tighter root margin, and hasMore termination when dedupe adds nothing. Pause middle-priority cover work during SQL pagination and bound diskSrc/resolve/ensure tail maps on long cold-cache sessions. * refactor(browse): unify in-page infinite scroll hooks and sentinel UI Extract shared transport (viewport ref, async pagination guards, client slice) and InpageScrollSentinel so Albums, New Releases, Artists, and song lists use one pagination pattern instead of duplicated IntersectionObserver wiring. * fix(browse): prioritize album SQL pagination over cover ensures Pause the entire webview ensure pump during grid page fetches, resume after SQL settles, add cover-queue backpressure before load-more, and re-probe the sentinel when pagination finishes so cold-cache scroll does not stall. * fix(browse): unblock covers, SQL spawn_blocking, and pagination retry Pair grid-pagination hold begin/end on stale fetches, resume the ensure pump after SQL, retry load-more when the cover backlog drains while the sentinel stays visible, and run album browse SQL on spawn_blocking so Tokio stays responsive during library_advanced_search. * feat(browse): All Albums client-slice scroll on local index (Artists-style) Load the filtered catalog once from SQLite when the library index is ready, then grow the visible grid with useClientSliceInfiniteScroll instead of offset SQL pagination per scroll. Network-only servers keep page mode. * fix(browse): lazy local catalog chunks instead of full 50k SQL fetch All Albums slice mode now loads 200 albums first, shows the grid immediately, then appends catalog chunks in the background as the user scrolls. Avoids the blocking library_advanced_search that hung the app on large libraries. * fix(browse): keep album covers loading during active grid scroll Pass high ensure priority and the in-page scroll root to AlbumCard on All Albums, stop pausing cover traffic for background catalog chunks, and never trim high-priority ensure jobs from the queue during scroll bursts. * fix(cover): viewport priority tiers and unstick ensure invoke pump All Albums uses IO-driven high/middle instead of blanket high; release only on unmount so scroll-ahead jobs are not dropped on reprioritize. Ensure queue shares one Rust flight per cover id, attaches duplicate waiters without consuming invoke slots, and times out wedged calls. Warm the first viewport slice on large grids; acquire CPU permits before spawn_blocking in cover_cache to avoid blocking-thread deadlocks. * fix(cover): wire in-page scroll root on New Releases and Lossless grids AlbumCard IO uses the same viewport id as VirtualCardGrid so cover ensure priority tracks visible in-page rows like All Albums. * fix(browse): lazy local artist catalog in 200-row chunks Replace runLocalBrowseAllArtists bulk fetch with paginated local-index chunks so large libraries do not hang on open; preserve text search, starred, letter filter, and client-slice scroll behavior. * feat(perf): add RSS and thread CPU groups to Performance Probe Extend performance_cpu_snapshot with process RSS (psysonic + WebKit children) and in-process thread CPU breakdown. Classify tokio-rt-worker and tokio-* workers separately from glib, audio/pipewire, reqwest, and other misc threads (Linux /proc only). * feat(perf): redesign Performance Probe with tabs, pins, and overlay layout Split the probe into Monitor (live metric cards, per-metric overlay pins, corner and opacity controls) and Toggles (diagnostic tree). Share live polling via perfLiveStore; label analysis/cover pipeline blocks in the HUD. * feat(perf): overlay sparklines, macOS CPU/memory, and sync fixes Add 1-minute pinned-metric sparklines with right-aligned growth and a shared poll clock. Enable macOS performance snapshots via sysinfo. Fix overlay infinite loop from unstable history snapshots, bar/sparkline tick jitter, and probe bar rescale flicker. * docs: CHANGELOG and credits for PR #890 * perf(probe): scoped CPU poll, adjustable interval, lazy thread groups Read only psysonic + WebKit children instead of the full process table; macOS uses sysctl host CPU and refreshes cached child PIDs. Add 0.5–10s poll slider (default 2s). Collect /proc thread groups only when the Monitor section is open or a thread metric is pinned. * feat(perf): three-way overlay mode switch (off / FPS / pinned) Add Monitor control for overlay visibility: hidden, FPS-only, or pinned metrics from Monitor. Live CPU poll runs only in pinned mode with live pins.
This commit is contained in:
+152
-12
@@ -1,29 +1,111 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { analysisGetPipelineQueueStats, type AnalysisPipelineQueueStatsDto } from '../api/analysis';
|
||||
import { usePerfProbeFlag } from '../utils/perf/perfFlags';
|
||||
import { coverGetPipelineQueueStats, type CoverPipelineQueueStatsDto } from '../api/coverCache';
|
||||
import { coverEnsureQueueStats } from '../cover/ensureQueue';
|
||||
import { coverPeekQueueStats } from '../cover/peekQueue';
|
||||
import PerfOverlaySparkline from './perf/PerfOverlaySparkline';
|
||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||
import {
|
||||
formatPerfMs,
|
||||
getAnalysisTracksPerMinute,
|
||||
useAnalysisPerfLast,
|
||||
} from '../utils/perf/analysisPerfStore';
|
||||
import { formatAnalysisPipelineQueueOverlay } from '../utils/perf/formatAnalysisQueueStats';
|
||||
import { formatCoverPipelineQueueOverlay } from '../utils/perf/formatCoverPipelineQueueOverlay';
|
||||
import {
|
||||
buildLiveOverlayItems,
|
||||
type LiveOverlayItem,
|
||||
} from '../utils/perf/formatLiveOverlayItems';
|
||||
import {
|
||||
getPerfLiveHistoryClock,
|
||||
syncPerfLiveHistoryFromPoll,
|
||||
usePerfLiveHistorySamples,
|
||||
} from '../utils/perf/perfLiveHistory';
|
||||
import { acquirePerfLivePoll, usePerfLiveSnapshot } from '../utils/perf/perfLiveStore';
|
||||
import { hasAnyLiveMetricPollNeed, usePerfLiveOverlayPins } from '../utils/perf/perfOverlayPins';
|
||||
import {
|
||||
perfOverlayCornerClass,
|
||||
usePerfOverlayAppearance,
|
||||
} from '../utils/perf/perfOverlayAppearance';
|
||||
import {
|
||||
resolveOverlayVisibility,
|
||||
usePerfOverlayMode,
|
||||
} from '../utils/perf/perfOverlayMode';
|
||||
import { useAnalysisPerfListener } from '../hooks/useAnalysisPerfListener';
|
||||
|
||||
const SAMPLE_MS = 500;
|
||||
const TPM_REFRESH_MS = 500;
|
||||
const QUEUE_STATS_MS = 750;
|
||||
|
||||
/** FPS + analysis throughput overlay (Performance Probe). */
|
||||
function LiveOverlayPinnedMetric({
|
||||
item,
|
||||
now,
|
||||
}: {
|
||||
item: LiveOverlayItem;
|
||||
now: number;
|
||||
}) {
|
||||
const history = usePerfLiveHistorySamples(item.id);
|
||||
const sparklineKind = item.kind === 'memory' ? 'memory' : 'cpu';
|
||||
|
||||
return (
|
||||
<div className="fps-overlay__live-metric">
|
||||
<div className="fps-overlay__row fps-overlay__row--live">{item.line}</div>
|
||||
{item.sparkline && (
|
||||
<PerfOverlaySparkline samples={history} kind={sparklineKind} now={now} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** FPS + pipeline + pinned live metrics overlay (Performance Probe). */
|
||||
export default function FpsOverlay() {
|
||||
const showFpsOverlay = usePerfProbeFlag('showFpsOverlay');
|
||||
const showAnalysisPerfOverlay = usePerfProbeFlag('showAnalysisPerfOverlay');
|
||||
const overlayMode = usePerfOverlayMode();
|
||||
const perfFlags = usePerfProbeFlags();
|
||||
const livePins = usePerfLiveOverlayPins();
|
||||
const live = usePerfLiveSnapshot();
|
||||
const overlayAppearance = usePerfOverlayAppearance();
|
||||
const [fps, setFps] = useState(0);
|
||||
const [tpm, setTpm] = useState(0);
|
||||
const [queueStats, setQueueStats] = useState<AnalysisPipelineQueueStatsDto | null>(null);
|
||||
const [coverQueueLines, setCoverQueueLines] = useState<string[]>([]);
|
||||
const last = useAnalysisPerfLast();
|
||||
const lastHistoryAt = useRef(0);
|
||||
|
||||
useAnalysisPerfListener(showAnalysisPerfOverlay);
|
||||
const liveOverlayItems = useMemo(
|
||||
() => buildLiveOverlayItems(livePins, live),
|
||||
[livePins, live],
|
||||
);
|
||||
|
||||
const visibility = useMemo(
|
||||
() => resolveOverlayVisibility(overlayMode, perfFlags, liveOverlayItems.length),
|
||||
[overlayMode, perfFlags, liveOverlayItems.length],
|
||||
);
|
||||
|
||||
const {
|
||||
showFps: showFpsOverlay,
|
||||
showAnalysis: showAnalysisPerfOverlay,
|
||||
showCover: showCoverPerfOverlay,
|
||||
showLive,
|
||||
} = visibility;
|
||||
|
||||
lastHistoryAt.current = overlayMode === 'pinned'
|
||||
? syncPerfLiveHistoryFromPoll(livePins, live, lastHistoryAt.current)
|
||||
: lastHistoryAt.current;
|
||||
|
||||
const sparklineNow = useMemo(() => {
|
||||
const clock = getPerfLiveHistoryClock(
|
||||
liveOverlayItems.filter(item => item.sparkline).map(item => item.id),
|
||||
);
|
||||
return clock > 0 ? clock : Date.now();
|
||||
}, [liveOverlayItems, live.updatedAt]);
|
||||
|
||||
useAnalysisPerfListener(showAnalysisPerfOverlay || livePins.has('analysis:tpm') || livePins.has('analysis:last'));
|
||||
|
||||
useEffect(() => {
|
||||
if (overlayMode !== 'pinned' || !hasAnyLiveMetricPollNeed()) return;
|
||||
return acquirePerfLivePoll('overlay-pins');
|
||||
}, [overlayMode, livePins.size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAnalysisPerfOverlay) {
|
||||
@@ -59,6 +141,36 @@ export default function FpsOverlay() {
|
||||
};
|
||||
}, [showAnalysisPerfOverlay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showCoverPerfOverlay) {
|
||||
setCoverQueueLines([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const refresh = () => {
|
||||
void coverGetPipelineQueueStats()
|
||||
.then((rust: CoverPipelineQueueStatsDto) => {
|
||||
if (cancelled) return;
|
||||
setCoverQueueLines(
|
||||
formatCoverPipelineQueueOverlay({
|
||||
rust,
|
||||
ensure: coverEnsureQueueStats(),
|
||||
peek: coverPeekQueueStats(),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setCoverQueueLines([]);
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
const id = window.setInterval(refresh, QUEUE_STATS_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(id);
|
||||
};
|
||||
}, [showCoverPerfOverlay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showFpsOverlay) {
|
||||
setFps(0);
|
||||
@@ -85,19 +197,35 @@ export default function FpsOverlay() {
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [showFpsOverlay]);
|
||||
|
||||
if (!showFpsOverlay && !showAnalysisPerfOverlay) return null;
|
||||
if (overlayMode === 'off') return null;
|
||||
if (!showFpsOverlay && !showAnalysisPerfOverlay && !showCoverPerfOverlay && !showLive) return null;
|
||||
|
||||
const analysisQueueLines = queueStats ? formatAnalysisPipelineQueueOverlay(queueStats) : [];
|
||||
|
||||
return createPortal(
|
||||
<div className="fps-overlay" aria-hidden="true">
|
||||
<div
|
||||
className={`fps-overlay ${perfOverlayCornerClass(overlayAppearance.corner)}`}
|
||||
style={{ '--fps-overlay-opacity': overlayAppearance.opacity } as CSSProperties}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{showFpsOverlay && (
|
||||
<div className="fps-overlay__row">
|
||||
<div className="fps-overlay__row fps-overlay__row--fps">
|
||||
{fps}
|
||||
{' '}
|
||||
<span className="fps-overlay__unit">FPS</span>
|
||||
</div>
|
||||
)}
|
||||
{showLive && (
|
||||
<div className="fps-overlay__block">
|
||||
<div className="fps-overlay__block-title">Live</div>
|
||||
{liveOverlayItems.map(item => (
|
||||
<LiveOverlayPinnedMetric key={item.id} item={item} now={sparklineNow} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showAnalysisPerfOverlay && (
|
||||
<>
|
||||
<div className="fps-overlay__block">
|
||||
<div className="fps-overlay__block-title">Analysis pipeline</div>
|
||||
<div className="fps-overlay__row">
|
||||
{tpm.toFixed(1)}
|
||||
{' '}
|
||||
@@ -122,12 +250,24 @@ export default function FpsOverlay() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{queueStats && formatAnalysisPipelineQueueOverlay(queueStats).map(line => (
|
||||
{analysisQueueLines.map(line => (
|
||||
<div key={line} className="fps-overlay__row fps-overlay__row--steps">
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{showCoverPerfOverlay && (
|
||||
<div className="fps-overlay__block">
|
||||
<div className="fps-overlay__block-title">Cover pipeline</div>
|
||||
{coverQueueLines.length > 0 ? coverQueueLines.map(line => (
|
||||
<div key={line} className="fps-overlay__row fps-overlay__row--steps">
|
||||
{line}
|
||||
</div>
|
||||
)) : (
|
||||
<div className="fps-overlay__row fps-overlay__row--steps">collecting…</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { CSSProperties, RefCallback } from 'react';
|
||||
|
||||
const DEFAULT_STYLE: CSSProperties = {
|
||||
height: '20px',
|
||||
margin: '2rem 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
type InpageScrollSentinelProps = {
|
||||
bindSentinel: RefCallback<HTMLDivElement | null>;
|
||||
loading?: boolean;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
/** Bottom-of-grid load-more sentinel + optional spinner (in-page scroll areas). */
|
||||
export default function InpageScrollSentinel({
|
||||
bindSentinel,
|
||||
loading = false,
|
||||
style,
|
||||
}: InpageScrollSentinelProps) {
|
||||
return (
|
||||
<div ref={bindSentinel} style={{ ...DEFAULT_STYLE, ...style }}>
|
||||
{loading && <div className="spinner" style={{ width: 20, height: 20 }} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import SongRow, { SongListHeader } from './SongRow';
|
||||
import { useInpageScrollSentinel } from '../hooks/useInpageScrollSentinel';
|
||||
import InpageScrollSentinel from './InpageScrollSentinel';
|
||||
|
||||
interface Props {
|
||||
songs: SubsonicSong[];
|
||||
@@ -22,19 +24,14 @@ interface Props {
|
||||
* is never painted over — issue #841).
|
||||
*/
|
||||
export default function PagedSongList({ songs, hasMore, loadingMore, onLoadMore, showBpm }: Props) {
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
const onLoadMoreRef = useRef(onLoadMore);
|
||||
onLoadMoreRef.current = onLoadMore;
|
||||
|
||||
// Re-observe whenever `onLoadMore` changes identity (callers rebuild it after
|
||||
// each page), so a sentinel still in view keeps loading the next page.
|
||||
useEffect(() => {
|
||||
const el = sentinelRef.current;
|
||||
if (!el) return;
|
||||
const obs = new IntersectionObserver(entries => {
|
||||
if (entries[0]?.isIntersecting) onLoadMore();
|
||||
}, { rootMargin: '600px' });
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, [onLoadMore]);
|
||||
const bindSentinel = useInpageScrollSentinel({
|
||||
active: hasMore,
|
||||
onIntersect: () => onLoadMoreRef.current(),
|
||||
rootMargin: '600px',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,9 +40,11 @@ export default function PagedSongList({ songs, hasMore, loadingMore, onLoadMore,
|
||||
<SongRow key={song.id} song={song} showBpm={showBpm} />
|
||||
))}
|
||||
{hasMore && (
|
||||
<div ref={sentinelRef} style={{ display: 'flex', justifyContent: 'center', padding: '1rem' }}>
|
||||
{loadingMore && <div className="spinner" style={{ width: 20, height: 20 }} />}
|
||||
</div>
|
||||
<InpageScrollSentinel
|
||||
bindSentinel={bindSentinel}
|
||||
loading={loadingMore}
|
||||
style={{ padding: '1rem', height: 'auto', margin: 0 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function Sidebar({
|
||||
isLoggedIn,
|
||||
pathname: location.pathname,
|
||||
});
|
||||
const { perfProbeOpen, setPerfProbeOpen, perfCpu, perfDiagRates, analysisPerf } = useSidebarPerfProbe();
|
||||
const { perfProbeOpen, setPerfProbeOpen } = useSidebarPerfProbe();
|
||||
const perfFlags = usePerfProbeFlags();
|
||||
|
||||
|
||||
@@ -258,9 +258,6 @@ export default function Sidebar({
|
||||
open={perfProbeOpen}
|
||||
onClose={() => setPerfProbeOpen(false)}
|
||||
perfFlags={perfFlags}
|
||||
perfCpu={perfCpu}
|
||||
perfDiagRates={perfDiagRates}
|
||||
analysisPerf={analysisPerf}
|
||||
hotCacheEnabled={hotCacheEnabled}
|
||||
setHotCacheEnabled={setHotCacheEnabled}
|
||||
normalizationEngine={normalizationEngine}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import type { PerfLiveSample } from '../../utils/perf/perfLiveHistory';
|
||||
import { PERF_LIVE_HISTORY_MS } from '../../utils/perf/perfLiveHistory';
|
||||
|
||||
type SparklineKind = 'cpu' | 'memory';
|
||||
|
||||
interface Props {
|
||||
samples: readonly PerfLiveSample[];
|
||||
kind: SparklineKind;
|
||||
now: number;
|
||||
windowMs?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
function sampleX(at: number, now: number, windowMs: number, width: number): number {
|
||||
const age = now - at;
|
||||
const ratio = 1 - age / windowMs;
|
||||
return Math.max(0, Math.min(width, ratio * width));
|
||||
}
|
||||
|
||||
function sparklinePaths(
|
||||
samples: readonly PerfLiveSample[],
|
||||
now: number,
|
||||
windowMs: number,
|
||||
width: number,
|
||||
height: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): { line: string; area: string } {
|
||||
if (samples.length === 0) return { line: '', area: '' };
|
||||
|
||||
const range = Math.max(max - min, 1e-6);
|
||||
const toY = (value: number) => height - ((value - min) / range) * (height - 2) - 1;
|
||||
const sorted = [...samples].sort((a, b) => a.at - b.at);
|
||||
|
||||
if (sorted.length === 1) {
|
||||
const x = sampleX(sorted[0].at, now, windowMs, width);
|
||||
const y = toY(sorted[0].value);
|
||||
const x0 = Math.max(0, x - 6);
|
||||
const line = `M${x0.toFixed(2)},${y.toFixed(2)} L${x.toFixed(2)},${y.toFixed(2)}`;
|
||||
const area = `${line} L${x.toFixed(2)},${height} L${x0.toFixed(2)},${height} Z`;
|
||||
return { line, area };
|
||||
}
|
||||
|
||||
const points = sorted.map(sample => ({
|
||||
x: sampleX(sample.at, now, windowMs, width),
|
||||
y: toY(sample.value),
|
||||
}));
|
||||
const line = points
|
||||
.map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)},${point.y.toFixed(2)}`)
|
||||
.join(' ');
|
||||
const first = points[0];
|
||||
const last = points[points.length - 1];
|
||||
const area = `${line} L${last.x.toFixed(2)},${height} L${first.x.toFixed(2)},${height} Z`;
|
||||
return { line, area };
|
||||
}
|
||||
|
||||
function scale(samples: readonly PerfLiveSample[], kind: SparklineKind): { min: number; max: number } {
|
||||
if (samples.length === 0) return { min: 0, max: 1 };
|
||||
const values = samples.map(sample => sample.value);
|
||||
const minVal = Math.min(...values);
|
||||
const maxVal = Math.max(...values);
|
||||
if (kind === 'cpu') {
|
||||
return { min: 0, max: Math.max(100, maxVal * 1.1) };
|
||||
}
|
||||
const pad = Math.max((maxVal - minVal) * 0.08, maxVal * 0.02, 1);
|
||||
return { min: Math.max(0, minVal - pad), max: maxVal + pad };
|
||||
}
|
||||
|
||||
function stableScale(
|
||||
samples: readonly PerfLiveSample[],
|
||||
kind: SparklineKind,
|
||||
peakRef: { current: number },
|
||||
): { min: number; max: number } {
|
||||
const raw = scale(samples, kind);
|
||||
if (kind === 'cpu') return raw;
|
||||
if (raw.max > peakRef.current) peakRef.current = raw.max;
|
||||
return { min: 0, max: Math.max(peakRef.current, raw.max) };
|
||||
}
|
||||
|
||||
/** Compact 1-minute sparkline for perf overlay CPU / memory pins. */
|
||||
export default function PerfOverlaySparkline({
|
||||
samples,
|
||||
kind,
|
||||
now,
|
||||
windowMs = PERF_LIVE_HISTORY_MS,
|
||||
width = 132,
|
||||
height = 22,
|
||||
}: Props) {
|
||||
const peakRef = useRef(1);
|
||||
|
||||
const { path, areaPath } = useMemo(() => {
|
||||
const bounds = stableScale(samples, kind, peakRef);
|
||||
const paths = sparklinePaths(samples, now, windowMs, width, height, bounds.min, bounds.max);
|
||||
return { path: paths.line, areaPath: paths.area };
|
||||
}, [samples, kind, now, windowMs, width, height]);
|
||||
|
||||
const latest = samples.length > 0 ? samples[samples.length - 1]?.value : null;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={`perf-overlay-sparkline perf-overlay-sparkline--${kind}`}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect
|
||||
className="perf-overlay-sparkline__track"
|
||||
x={0}
|
||||
y={0}
|
||||
width={width}
|
||||
height={height}
|
||||
rx={3}
|
||||
/>
|
||||
{areaPath && <path className="perf-overlay-sparkline__fill" d={areaPath} />}
|
||||
{path && (
|
||||
<path className="perf-overlay-sparkline__line" d={path} vectorEffect="non-scaling-stroke" />
|
||||
)}
|
||||
<title>
|
||||
{latest == null
|
||||
? `${kind === 'cpu' ? 'CPU' : 'Memory'} (1 min)`
|
||||
: kind === 'cpu'
|
||||
? `CPU ${latest.toFixed(1)}% (1 min)`
|
||||
: `Memory ${latest.toFixed(1)} MB (1 min)`}
|
||||
</title>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +1,19 @@
|
||||
import { useState } from 'react';
|
||||
import { Activity, SlidersHorizontal, X } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X } from 'lucide-react';
|
||||
import SidebarPerfProbePhase2 from './SidebarPerfProbePhase2';
|
||||
import { resetPerfProbeFlags, setPerfProbeFlag, type PerfProbeFlags } from '../../utils/perf/perfFlags';
|
||||
import SidebarPerfProbeMonitorTab from './perfProbe/SidebarPerfProbeMonitorTab';
|
||||
import SidebarPerfProbeTogglesTab from './perfProbe/SidebarPerfProbeTogglesTab';
|
||||
import { resetPerfProbeFlags, type PerfProbeFlags } from '../../utils/perf/perfFlags';
|
||||
import { clearPerfLiveOverlayPins } from '../../utils/perf/perfOverlayPins';
|
||||
import { resetPerfOverlayAppearance } from '../../utils/perf/perfOverlayAppearance';
|
||||
import { resetPerfOverlayMode } from '../../utils/perf/perfOverlayMode';
|
||||
|
||||
interface PerfCpu {
|
||||
app: number;
|
||||
webkit: number;
|
||||
supported: boolean;
|
||||
}
|
||||
|
||||
interface PerfDiagRates {
|
||||
progress: number;
|
||||
waveform: number;
|
||||
home: number;
|
||||
}
|
||||
|
||||
interface AnalysisPerfDiag {
|
||||
tracksPerMinute: number;
|
||||
lastTotalMs: number | null;
|
||||
lastFetchMs: number | null;
|
||||
lastSeedMs: number | null;
|
||||
lastBpmMs: number | null;
|
||||
}
|
||||
type TabId = 'monitor' | 'toggles';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
perfFlags: PerfProbeFlags;
|
||||
perfCpu: PerfCpu | null;
|
||||
perfDiagRates: PerfDiagRates | null;
|
||||
analysisPerf: AnalysisPerfDiag | null;
|
||||
hotCacheEnabled: boolean;
|
||||
setHotCacheEnabled: (v: boolean) => void;
|
||||
normalizationEngine: string;
|
||||
@@ -39,250 +23,99 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function SidebarPerfProbeModal({
|
||||
open, onClose, perfFlags, perfCpu, perfDiagRates, analysisPerf,
|
||||
hotCacheEnabled, setHotCacheEnabled,
|
||||
normalizationEngine, setNormalizationEngine,
|
||||
loggingMode, setLoggingMode,
|
||||
open,
|
||||
onClose,
|
||||
perfFlags,
|
||||
hotCacheEnabled,
|
||||
setHotCacheEnabled,
|
||||
normalizationEngine,
|
||||
setNormalizationEngine,
|
||||
loggingMode,
|
||||
setLoggingMode,
|
||||
}: Props) {
|
||||
const [tab, setTab] = useState<TabId>('monitor');
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const resetAll = () => {
|
||||
resetPerfProbeFlags();
|
||||
clearPerfLiveOverlayPins();
|
||||
resetPerfOverlayAppearance();
|
||||
resetPerfOverlayMode();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="modal-overlay modal-overlay--perf-probe" onClick={() => onClose()} role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="modal-content sidebar-perf-modal"
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{ maxWidth: 560 }}
|
||||
<div
|
||||
className="modal-overlay modal-overlay--perf-probe"
|
||||
onClick={() => onClose()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="perf-probe-title"
|
||||
>
|
||||
<div
|
||||
className="modal-content sidebar-perf-modal"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<button type="button" className="modal-close" onClick={() => onClose()} aria-label="Close">
|
||||
<X size={18} />
|
||||
</button>
|
||||
|
||||
<header className="sidebar-perf-modal__header">
|
||||
<h3 id="perf-probe-title" className="modal-title">Performance Probe</h3>
|
||||
<p className="sidebar-perf-modal__hint">
|
||||
Live metrics with optional on-screen overlays, plus diagnostic disable toggles.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="sidebar-perf-modal__tabs" role="tablist" aria-label="Performance probe sections">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === 'monitor'}
|
||||
className={`sidebar-perf-modal__tab${tab === 'monitor' ? ' sidebar-perf-modal__tab--active' : ''}`}
|
||||
onClick={() => setTab('monitor')}
|
||||
>
|
||||
<button className="modal-close" onClick={() => onClose()}><X size={18} /></button>
|
||||
<h3 className="modal-title">Performance Probe</h3>
|
||||
<p className="sidebar-perf-modal__hint">
|
||||
Temporary runtime switches to estimate UI effect cost.
|
||||
</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>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.showAnalysisPerfOverlay}
|
||||
onChange={e => setPerfProbeFlag('showAnalysisPerfOverlay', e.target.checked)}
|
||||
/>
|
||||
<span>Show analysis throughput overlay (tpm + last track timings)</span>
|
||||
</label>
|
||||
<div className="sidebar-perf-modal__cpu">
|
||||
<div className="sidebar-perf-modal__cpu-title">Live CPU (approx)</div>
|
||||
{perfCpu == null ? (
|
||||
<div className="sidebar-perf-modal__cpu-row">Collecting samples…</div>
|
||||
) : perfCpu.supported ? (
|
||||
<>
|
||||
<div className="sidebar-perf-modal__cpu-row">psysonic: {perfCpu.app.toFixed(1)}%</div>
|
||||
<div className="sidebar-perf-modal__cpu-row">WebKitWebProcess: {perfCpu.webkit.toFixed(1)}%</div>
|
||||
{perfDiagRates && (
|
||||
<>
|
||||
<div className="sidebar-perf-modal__cpu-row">audio:progress rate: {perfDiagRates.progress.toFixed(1)}/s</div>
|
||||
<div className="sidebar-perf-modal__cpu-row">waveform draws rate: {perfDiagRates.waveform.toFixed(1)}/s</div>
|
||||
<div className="sidebar-perf-modal__cpu-row">Home commits rate: {perfDiagRates.home.toFixed(1)}/s</div>
|
||||
</>
|
||||
)}
|
||||
{analysisPerf && (
|
||||
<>
|
||||
<div className="sidebar-perf-modal__cpu-row">analysis tpm (1m avg): {analysisPerf.tracksPerMinute.toFixed(1)}</div>
|
||||
{analysisPerf.lastTotalMs != null && (
|
||||
<div className="sidebar-perf-modal__cpu-row">
|
||||
last track: {(analysisPerf.lastTotalMs / 1000).toFixed(1)}s
|
||||
{' '}
|
||||
(fetch {(analysisPerf.lastFetchMs ?? 0) / 1000}s · seed {(analysisPerf.lastSeedMs ?? 0) / 1000}s · bpm {(analysisPerf.lastBpmMs ?? 0) / 1000}s)
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="sidebar-perf-modal__cpu-row">Unavailable on this platform/build.</div>
|
||||
)}
|
||||
</div>
|
||||
<details className="sidebar-perf-modal__phase">
|
||||
<summary className="sidebar-perf-modal__phase-title">Phase 1 — Global / Shell / Network</summary>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableWaveformCanvas}
|
||||
onChange={e => setPerfProbeFlag('disableWaveformCanvas', e.target.checked)}
|
||||
/>
|
||||
<span>Disable only PlayerBar waveform (`WaveformSeek`)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disablePlayerProgressUi}
|
||||
onChange={e => setPerfProbeFlag('disablePlayerProgressUi', e.target.checked)}
|
||||
/>
|
||||
<span>Disable player live progress UI updates (time + seek/progress bindings)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMarqueeScroll}
|
||||
onChange={e => setPerfProbeFlag('disableMarqueeScroll', e.target.checked)}
|
||||
/>
|
||||
<span>Disable marquee text scrolling</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableBackdropBlur}
|
||||
onChange={e => setPerfProbeFlag('disableBackdropBlur', e.target.checked)}
|
||||
/>
|
||||
<span>Disable backdrop blur effects</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableCssAnimations}
|
||||
onChange={e => setPerfProbeFlag('disableCssAnimations', e.target.checked)}
|
||||
/>
|
||||
<span>Disable CSS animations and transitions</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableOverlayScrollbars}
|
||||
onChange={e => setPerfProbeFlag('disableOverlayScrollbars', e.target.checked)}
|
||||
/>
|
||||
<span>Disable overlay scrollbar engine (JS + rail)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableTooltipPortal}
|
||||
onChange={e => setPerfProbeFlag('disableTooltipPortal', e.target.checked)}
|
||||
/>
|
||||
<span>Disable global tooltip portal/listeners</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableQueuePanelMount}
|
||||
onChange={e => setPerfProbeFlag('disableQueuePanelMount', e.target.checked)}
|
||||
/>
|
||||
<span>Disable QueuePanel mount (desktop right column)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableBackgroundPolling}
|
||||
onChange={e => setPerfProbeFlag('disableBackgroundPolling', e.target.checked)}
|
||||
/>
|
||||
<span>Disable background polling (connection + radio metadata)</span>
|
||||
</label>
|
||||
<div className="sidebar-perf-modal__subhead">Engine/network toggles</div>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!hotCacheEnabled}
|
||||
onChange={e => setHotCacheEnabled(!e.target.checked)}
|
||||
/>
|
||||
<span>Disable hot-cache prefetch downloads</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={normalizationEngine === 'off'}
|
||||
onChange={e => setNormalizationEngine(e.target.checked ? 'off' : 'loudness')}
|
||||
/>
|
||||
<span>Disable normalization engine (set to Off)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={loggingMode === 'off'}
|
||||
onChange={e => setLoggingMode(e.target.checked ? 'off' : 'normal')}
|
||||
/>
|
||||
<span>Set runtime logging mode to Off</span>
|
||||
</label>
|
||||
</details>
|
||||
<SidebarPerfProbePhase2 perfFlags={perfFlags} />
|
||||
<details className="sidebar-perf-modal__phase">
|
||||
<summary className="sidebar-perf-modal__phase-title">Phase 3 — Active diagnostics (quick access)</summary>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disablePlayerProgressUi}
|
||||
onChange={e => setPerfProbeFlag('disablePlayerProgressUi', e.target.checked)}
|
||||
/>
|
||||
<span>Disable player live progress UI updates (time + seek/progress bindings)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableWaveformCanvas}
|
||||
onChange={e => setPerfProbeFlag('disableWaveformCanvas', e.target.checked)}
|
||||
/>
|
||||
<span>Disable only PlayerBar waveform (`WaveformSeek`)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeRailArtwork}
|
||||
onChange={e => setPerfProbeFlag('disableHomeRailArtwork', e.target.checked)}
|
||||
/>
|
||||
<span>Disable artwork inside Home rows/rails only</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRailArtwork}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRailArtwork', e.target.checked)}
|
||||
/>
|
||||
<span>Disable artwork inside Home rows/rails</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRails}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRails', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Home rows/rails (`AlbumRow` + `SongRail`)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageHeroBackdrop}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageHeroBackdrop', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Hero backdrop/crossfade only</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeArtworkFx}
|
||||
onChange={e => setPerfProbeFlag('disableHomeArtworkFx', e.target.checked)}
|
||||
/>
|
||||
<span>Keep artwork, disable Home card visual effects (hover/overlay/shadows)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeArtworkClip}
|
||||
onChange={e => setPerfProbeFlag('disableHomeArtworkClip', e.target.checked)}
|
||||
/>
|
||||
<span>Diagnostic: flatten Home artwork clipping (no rounded corners/masks)</span>
|
||||
</label>
|
||||
</details>
|
||||
<div className="sidebar-perf-modal__actions">
|
||||
<button type="button" className="btn btn-ghost" onClick={() => resetPerfProbeFlags()}>
|
||||
Reset
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={() => onClose()}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<Activity size={15} />
|
||||
Monitor
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === 'toggles'}
|
||||
className={`sidebar-perf-modal__tab${tab === 'toggles' ? ' sidebar-perf-modal__tab--active' : ''}`}
|
||||
onClick={() => setTab('toggles')}
|
||||
>
|
||||
<SlidersHorizontal size={15} />
|
||||
Toggles
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-perf-modal__body">
|
||||
{tab === 'monitor' ? (
|
||||
<SidebarPerfProbeMonitorTab />
|
||||
) : (
|
||||
<SidebarPerfProbeTogglesTab
|
||||
perfFlags={perfFlags}
|
||||
hotCacheEnabled={hotCacheEnabled}
|
||||
setHotCacheEnabled={setHotCacheEnabled}
|
||||
normalizationEngine={normalizationEngine}
|
||||
setNormalizationEngine={setNormalizationEngine}
|
||||
loggingMode={loggingMode}
|
||||
setLoggingMode={setLoggingMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sidebar-perf-modal__actions">
|
||||
<button type="button" className="btn btn-ghost" onClick={resetAll}>
|
||||
Reset all
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={() => onClose()}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { setPerfProbeFlag, type PerfProbeFlags } from '../../utils/perf/perfFlags';
|
||||
|
||||
export default function SidebarPerfProbePhase2({ perfFlags }: { perfFlags: PerfProbeFlags }) {
|
||||
return (
|
||||
<details className="sidebar-perf-modal__phase">
|
||||
<summary className="sidebar-perf-modal__phase-title">Phase 2 — Mainstage (Center Content)</summary>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainRouteContentMount}
|
||||
onChange={e => setPerfProbeFlag('disableMainRouteContentMount', e.target.checked)}
|
||||
/>
|
||||
<span>Disable central route content mount</span>
|
||||
</label>
|
||||
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
|
||||
<summary className="sidebar-perf-modal__phase-title">Shared mainstage layers (multiple pages)</summary>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageStickyHeader}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageStickyHeader', e.target.checked)}
|
||||
/>
|
||||
<span>Disable sticky headers (Tracks + Albums)</span>
|
||||
</label>
|
||||
</details>
|
||||
|
||||
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
|
||||
<summary className="sidebar-perf-modal__phase-title">Home (`/`)</summary>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageHero}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageHero', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Home hero block</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageHeroBackdrop}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageHeroBackdrop', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Hero backdrop/crossfade only</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRails}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRails', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Home rows/rails (`AlbumRow` + `SongRail`)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeAlbumRows}
|
||||
onChange={e => setPerfProbeFlag('disableHomeAlbumRows', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Home `AlbumRow` sections only</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeSongRails}
|
||||
onChange={e => setPerfProbeFlag('disableHomeSongRails', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Home `SongRail` sections only</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRailArtwork}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRailArtwork', e.target.checked)}
|
||||
/>
|
||||
<span>Disable artwork inside Home rows/rails</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeRailArtwork}
|
||||
onChange={e => setPerfProbeFlag('disableHomeRailArtwork', e.target.checked)}
|
||||
/>
|
||||
<span>Disable artwork inside Home rows/rails only</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeArtworkFx}
|
||||
onChange={e => setPerfProbeFlag('disableHomeArtworkFx', e.target.checked)}
|
||||
/>
|
||||
<span>Keep artwork, disable Home card visual effects (hover/overlay/shadows)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableHomeArtworkClip}
|
||||
onChange={e => setPerfProbeFlag('disableHomeArtworkClip', e.target.checked)}
|
||||
/>
|
||||
<span>Diagnostic: flatten Home artwork clipping (no rounded corners/masks)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRailInteractivity}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRailInteractivity', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Home rail scroll/nav handlers</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageGridCards}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageGridCards', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Home discover artists chip-grid</span>
|
||||
</label>
|
||||
</details>
|
||||
|
||||
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
|
||||
<summary className="sidebar-perf-modal__phase-title">Tracks (`/tracks`)</summary>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageHero}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageHero', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Tracks hero block</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRails}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRails', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Tracks rails (Highly Rated + Random)</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRailArtwork}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRailArtwork', e.target.checked)}
|
||||
/>
|
||||
<span>Disable artwork inside Tracks rails</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageRailInteractivity}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageRailInteractivity', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Tracks rail scroll/nav handlers</span>
|
||||
</label>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageVirtualLists}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageVirtualLists', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Tracks virtual browse list (`VirtualSongList`)</span>
|
||||
</label>
|
||||
</details>
|
||||
|
||||
<details className="sidebar-perf-modal__phase sidebar-perf-modal__phase--nested">
|
||||
<summary className="sidebar-perf-modal__phase-title">Albums (`/albums`)</summary>
|
||||
<label className="sidebar-perf-modal__item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags.disableMainstageGridCards}
|
||||
onChange={e => setPerfProbeFlag('disableMainstageGridCards', e.target.checked)}
|
||||
/>
|
||||
<span>Disable Albums card grid (`AlbumCard` list)</span>
|
||||
</label>
|
||||
</details>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
PERF_LIVE_POLL_MS_MAX,
|
||||
PERF_LIVE_POLL_MS_MIN,
|
||||
PERF_LIVE_POLL_MS_STEP,
|
||||
setPerfLivePollIntervalMs,
|
||||
usePerfLivePollIntervalMs,
|
||||
} from '../../../utils/perf/perfLivePollSettings';
|
||||
|
||||
export default function PerfLivePollControls() {
|
||||
const pollMs = usePerfLivePollIntervalMs();
|
||||
const pollSec = (pollMs / 1000).toFixed(1);
|
||||
|
||||
return (
|
||||
<section className="perf-live-poll" aria-label="Live poll interval">
|
||||
<div className="perf-live-poll__title">Live sampling</div>
|
||||
<label className="perf-live-poll__row">
|
||||
<span className="perf-live-poll__label">
|
||||
Poll interval
|
||||
{' '}
|
||||
<span className="perf-live-poll__value">{pollSec}s</span>
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={PERF_LIVE_POLL_MS_MIN}
|
||||
max={PERF_LIVE_POLL_MS_MAX}
|
||||
step={PERF_LIVE_POLL_MS_STEP}
|
||||
value={pollMs}
|
||||
onChange={e => setPerfLivePollIntervalMs(Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
PERF_OVERLAY_CORNER_OPTIONS,
|
||||
setPerfOverlayCorner,
|
||||
setPerfOverlayOpacity,
|
||||
usePerfOverlayAppearance,
|
||||
} from '../../../utils/perf/perfOverlayAppearance';
|
||||
|
||||
export default function PerfOverlayAppearanceControls() {
|
||||
const { corner, opacity } = usePerfOverlayAppearance();
|
||||
|
||||
return (
|
||||
<section className="perf-overlay-appearance" aria-label="Overlay layout">
|
||||
<div className="perf-overlay-appearance__title">Layout</div>
|
||||
<div className="perf-overlay-appearance__row">
|
||||
<span className="perf-overlay-appearance__label">Corner</span>
|
||||
<div className="perf-overlay-appearance__corners" role="group" aria-label="Overlay corner">
|
||||
{PERF_OVERLAY_CORNER_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
className={`perf-overlay-appearance__corner${corner === opt.id ? ' perf-overlay-appearance__corner--active' : ''}`}
|
||||
aria-pressed={corner === opt.id}
|
||||
onClick={() => setPerfOverlayCorner(opt.id)}
|
||||
title={opt.label}
|
||||
>
|
||||
<span className={`perf-overlay-appearance__corner-mark perf-overlay-appearance__corner-mark--${opt.id}`} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<label className="perf-overlay-appearance__row perf-overlay-appearance__opacity">
|
||||
<span className="perf-overlay-appearance__label">
|
||||
Opacity
|
||||
{' '}
|
||||
<span className="perf-overlay-appearance__value">{Math.round(opacity * 100)}%</span>
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={25}
|
||||
max={100}
|
||||
step={5}
|
||||
value={Math.round(opacity * 100)}
|
||||
onChange={e => setPerfOverlayOpacity(Number(e.target.value) / 100)}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
PERF_OVERLAY_MODE_OPTIONS,
|
||||
setPerfOverlayMode,
|
||||
usePerfOverlayMode,
|
||||
} from '../../../utils/perf/perfOverlayMode';
|
||||
|
||||
export default function PerfOverlayModeControls() {
|
||||
const mode = usePerfOverlayMode();
|
||||
|
||||
return (
|
||||
<section className="perf-overlay-mode" aria-label="Overlay mode">
|
||||
<div className="perf-overlay-mode__title">On-screen overlay</div>
|
||||
<div className="perf-overlay-mode__segments" role="group" aria-label="Overlay mode">
|
||||
{PERF_OVERLAY_MODE_OPTIONS.map(option => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
className={`perf-overlay-mode__segment${mode === option.id ? ' perf-overlay-mode__segment--active' : ''}`}
|
||||
aria-pressed={mode === option.id}
|
||||
onClick={() => setPerfOverlayMode(option.id)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="perf-overlay-mode__hint">
|
||||
{mode === 'off' && 'Overlay hidden.'}
|
||||
{mode === 'fps' && 'Shows only the FPS counter.'}
|
||||
{mode === 'pinned' && 'Shows metrics pinned in Monitor (pipeline + live).'}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { ChevronRight, Pin, PinOff } from 'lucide-react';
|
||||
|
||||
type PinKind = 'live' | 'pipeline';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
detail?: string;
|
||||
barPct?: number;
|
||||
barTone?: 'cpu' | 'memory' | 'rate' | 'neutral';
|
||||
pinned?: boolean;
|
||||
pinKind?: PinKind;
|
||||
onTogglePin?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function PerfProbeMetricCard({
|
||||
label,
|
||||
value,
|
||||
unit,
|
||||
detail,
|
||||
barPct,
|
||||
barTone = 'neutral',
|
||||
pinned = false,
|
||||
pinKind,
|
||||
onTogglePin,
|
||||
disabled = false,
|
||||
}: Props) {
|
||||
const showBar = barPct != null && Number.isFinite(barPct) && barPct > 0;
|
||||
|
||||
return (
|
||||
<div className={`perf-metric-card${disabled ? ' perf-metric-card--disabled' : ''}`}>
|
||||
<div className="perf-metric-card__head">
|
||||
<div className="perf-metric-card__label">{label}</div>
|
||||
{onTogglePin && (
|
||||
<button
|
||||
type="button"
|
||||
className={`perf-metric-card__pin${pinned ? ' perf-metric-card__pin--active' : ''}`}
|
||||
onClick={onTogglePin}
|
||||
aria-pressed={pinned}
|
||||
title={pinned ? 'Remove from overlay' : 'Show in overlay'}
|
||||
>
|
||||
{pinned ? <PinOff size={14} /> : <Pin size={14} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="perf-metric-card__value-row">
|
||||
<span className="perf-metric-card__value">{value}</span>
|
||||
{unit && <span className="perf-metric-card__unit">{unit}</span>}
|
||||
</div>
|
||||
{showBar && (
|
||||
<div className="perf-metric-card__bar-track" aria-hidden="true">
|
||||
<div
|
||||
className={`perf-metric-card__bar-fill perf-metric-card__bar-fill--${barTone}`}
|
||||
style={{ width: `${Math.min(100, barPct)}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{detail && <div className="perf-metric-card__detail">{detail}</div>}
|
||||
{pinKind === 'pipeline' && pinned && (
|
||||
<div className="perf-metric-card__badge">Pipeline overlay</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
hint?: string;
|
||||
children: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function PerfProbeMetricSection({
|
||||
title,
|
||||
hint,
|
||||
children,
|
||||
defaultOpen = true,
|
||||
onOpenChange,
|
||||
}: SectionProps) {
|
||||
return (
|
||||
<details
|
||||
className="perf-metric-section"
|
||||
open={defaultOpen}
|
||||
onToggle={e => onOpenChange?.((e.currentTarget as HTMLDetailsElement).open)}
|
||||
>
|
||||
<summary className="perf-metric-section__title">
|
||||
<ChevronRight size={14} className="perf-metric-section__chevron" />
|
||||
<span>{title}</span>
|
||||
{hint && <span className="perf-metric-section__hint">{hint}</span>}
|
||||
</summary>
|
||||
<div className="perf-metric-section__grid">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { usePerfLiveSnapshot } from '../../../utils/perf/perfLiveStore';
|
||||
import {
|
||||
syncPerfLiveThreadGroupsNeed,
|
||||
} from '../../../utils/perf/perfLivePollSettings';
|
||||
import {
|
||||
togglePerfLiveOverlayPin,
|
||||
togglePipelineOverlayPin,
|
||||
usePerfLiveOverlayPins,
|
||||
usePipelineOverlayPinned,
|
||||
type PerfLiveOverlayPinId,
|
||||
} from '../../../utils/perf/perfOverlayPins';
|
||||
import PerfProbeMetricCard, { PerfProbeMetricSection } from './PerfProbeMetricCard';
|
||||
import PerfOverlayAppearanceControls from './PerfOverlayAppearanceControls';
|
||||
import PerfOverlayModeControls from './PerfOverlayModeControls';
|
||||
import PerfLivePollControls from './PerfLivePollControls';
|
||||
|
||||
function memoryBarPct(rssKb: number, maxKb: number): number {
|
||||
if (maxKb <= 0) return 0;
|
||||
return (rssKb / maxKb) * 100;
|
||||
}
|
||||
|
||||
export default function SidebarPerfProbeMonitorTab() {
|
||||
const live = usePerfLiveSnapshot();
|
||||
const livePins = usePerfLiveOverlayPins();
|
||||
const fpsPinned = usePipelineOverlayPinned('pipeline:fps');
|
||||
const analysisPinned = usePipelineOverlayPinned('pipeline:analysis');
|
||||
const coverPinned = usePipelineOverlayPinned('pipeline:cover');
|
||||
const cpu = live.cpu;
|
||||
const collecting = live.collecting && cpu == null;
|
||||
const peakMemoryKbRef = useRef(1);
|
||||
const peakThreadCpuRef = useRef(1);
|
||||
const [threadSectionOpen, setThreadSectionOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
syncPerfLiveThreadGroupsNeed(threadSectionOpen, livePins);
|
||||
}, [threadSectionOpen, livePins]);
|
||||
|
||||
const maxMemoryKb = useMemo(() => {
|
||||
const current = Math.max(1, ...(cpu?.memory.map(m => m.rss_kb) ?? [1]));
|
||||
if (current > peakMemoryKbRef.current) peakMemoryKbRef.current = current;
|
||||
return peakMemoryKbRef.current;
|
||||
}, [cpu?.memory]);
|
||||
|
||||
const maxThreadCpu = useMemo(() => {
|
||||
const current = Math.max(1, ...(cpu?.threadCpu.map(t => t.pct) ?? [1]));
|
||||
if (current > peakThreadCpuRef.current) peakThreadCpuRef.current = current;
|
||||
return peakThreadCpuRef.current;
|
||||
}, [cpu?.threadCpu]);
|
||||
|
||||
if (collecting) {
|
||||
return (
|
||||
<div className="perf-monitor-empty">
|
||||
<div className="spinner" style={{ width: 22, height: 22 }} />
|
||||
<span>Collecting live samples…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (cpu && !cpu.supported) {
|
||||
return (
|
||||
<div className="perf-monitor-empty">
|
||||
Live CPU/memory monitoring is unavailable on this platform or build.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const toggleLive = (id: PerfLiveOverlayPinId) => () => togglePerfLiveOverlayPin(id);
|
||||
const livePinned = (id: PerfLiveOverlayPinId) => livePins.has(id);
|
||||
|
||||
return (
|
||||
<div className="perf-monitor">
|
||||
<PerfOverlayModeControls />
|
||||
<PerfOverlayAppearanceControls />
|
||||
<PerfLivePollControls />
|
||||
<PerfProbeMetricSection title="Pipeline overlays" hint="Rust / UI queues">
|
||||
<PerfProbeMetricCard
|
||||
label="FPS"
|
||||
value="—"
|
||||
detail="requestAnimationFrame rate"
|
||||
pinned={fpsPinned}
|
||||
pinKind="pipeline"
|
||||
onTogglePin={() => togglePipelineOverlayPin('pipeline:fps')}
|
||||
/>
|
||||
<PerfProbeMetricCard
|
||||
label="Analysis"
|
||||
value="—"
|
||||
detail="Throughput + last track timings"
|
||||
pinned={analysisPinned}
|
||||
pinKind="pipeline"
|
||||
onTogglePin={() => togglePipelineOverlayPin('pipeline:analysis')}
|
||||
/>
|
||||
<PerfProbeMetricCard
|
||||
label="Cover pipeline"
|
||||
value="—"
|
||||
detail="Ensure / HTTP / encode queues"
|
||||
pinned={coverPinned}
|
||||
pinKind="pipeline"
|
||||
onTogglePin={() => togglePipelineOverlayPin('pipeline:cover')}
|
||||
/>
|
||||
</PerfProbeMetricSection>
|
||||
|
||||
{cpu && (
|
||||
<>
|
||||
<PerfProbeMetricSection title="CPU — processes">
|
||||
<PerfProbeMetricCard
|
||||
label="psysonic"
|
||||
value={cpu.app.toFixed(1)}
|
||||
unit="%"
|
||||
barPct={cpu.app}
|
||||
barTone="cpu"
|
||||
pinned={livePinned('cpu:app')}
|
||||
onTogglePin={toggleLive('cpu:app')}
|
||||
/>
|
||||
<PerfProbeMetricCard
|
||||
label="WebKit web"
|
||||
value={cpu.webkit.toFixed(1)}
|
||||
unit="%"
|
||||
barPct={cpu.webkit}
|
||||
barTone="cpu"
|
||||
pinned={livePinned('cpu:webkit')}
|
||||
onTogglePin={toggleLive('cpu:webkit')}
|
||||
/>
|
||||
</PerfProbeMetricSection>
|
||||
|
||||
{(cpu.threadCpu.length > 0 || threadSectionOpen) && (
|
||||
<PerfProbeMetricSection
|
||||
title="CPU — psysonic threads"
|
||||
defaultOpen={false}
|
||||
onOpenChange={setThreadSectionOpen}
|
||||
>
|
||||
{cpu.threadCpu.length > 0 ? cpu.threadCpu.map(row => {
|
||||
const pinId = `cpu:thread:${row.label}` as PerfLiveOverlayPinId;
|
||||
return (
|
||||
<PerfProbeMetricCard
|
||||
key={row.label}
|
||||
label={row.label}
|
||||
value={row.pct.toFixed(1)}
|
||||
unit="%"
|
||||
detail={row.threadCount > 1 ? `${row.threadCount} threads` : undefined}
|
||||
barPct={(row.pct / maxThreadCpu) * 100}
|
||||
barTone="cpu"
|
||||
pinned={livePinned(pinId)}
|
||||
onTogglePin={toggleLive(pinId)}
|
||||
/>
|
||||
);
|
||||
}) : (
|
||||
<div className="perf-monitor-empty perf-monitor-empty--inline">Collecting thread samples…</div>
|
||||
)}
|
||||
</PerfProbeMetricSection>
|
||||
)}
|
||||
|
||||
{cpu.memory.length > 0 && (
|
||||
<PerfProbeMetricSection title="Memory — RSS">
|
||||
{cpu.memory.map(row => {
|
||||
const pinId = `mem:${row.label}` as PerfLiveOverlayPinId;
|
||||
return (
|
||||
<PerfProbeMetricCard
|
||||
key={row.label}
|
||||
label={row.label}
|
||||
value={(row.rss_kb / 1024).toFixed(1)}
|
||||
unit="MB"
|
||||
barPct={memoryBarPct(row.rss_kb, maxMemoryKb)}
|
||||
barTone="memory"
|
||||
pinned={livePinned(pinId)}
|
||||
onTogglePin={toggleLive(pinId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PerfProbeMetricSection>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{live.diagRates && (
|
||||
<PerfProbeMetricSection title="UI event rates" defaultOpen={false}>
|
||||
<PerfProbeMetricCard
|
||||
label="audio:progress"
|
||||
value={live.diagRates.progress.toFixed(1)}
|
||||
unit="/s"
|
||||
barPct={Math.min(100, live.diagRates.progress * 2)}
|
||||
barTone="rate"
|
||||
pinned={livePinned('rate:progress')}
|
||||
onTogglePin={toggleLive('rate:progress')}
|
||||
/>
|
||||
<PerfProbeMetricCard
|
||||
label="waveform draws"
|
||||
value={live.diagRates.waveform.toFixed(1)}
|
||||
unit="/s"
|
||||
barPct={Math.min(100, live.diagRates.waveform * 2)}
|
||||
barTone="rate"
|
||||
pinned={livePinned('rate:waveform')}
|
||||
onTogglePin={toggleLive('rate:waveform')}
|
||||
/>
|
||||
<PerfProbeMetricCard
|
||||
label="Home commits"
|
||||
value={live.diagRates.home.toFixed(1)}
|
||||
unit="/s"
|
||||
barPct={Math.min(100, live.diagRates.home * 5)}
|
||||
barTone="rate"
|
||||
pinned={livePinned('rate:home')}
|
||||
onTogglePin={toggleLive('rate:home')}
|
||||
/>
|
||||
</PerfProbeMetricSection>
|
||||
)}
|
||||
|
||||
{live.analysis && (
|
||||
<PerfProbeMetricSection title="Analysis" defaultOpen={false}>
|
||||
<PerfProbeMetricCard
|
||||
label="Throughput"
|
||||
value={live.analysis.tracksPerMinute.toFixed(1)}
|
||||
unit="tpm"
|
||||
pinned={livePinned('analysis:tpm')}
|
||||
onTogglePin={toggleLive('analysis:tpm')}
|
||||
/>
|
||||
{live.analysis.lastTotalMs != null && (
|
||||
<PerfProbeMetricCard
|
||||
label="Last track"
|
||||
value={(live.analysis.lastTotalMs / 1000).toFixed(1)}
|
||||
unit="s"
|
||||
detail={`fetch ${((live.analysis.lastFetchMs ?? 0) / 1000).toFixed(1)}s · seed ${((live.analysis.lastSeedMs ?? 0) / 1000).toFixed(1)}s · bpm ${((live.analysis.lastBpmMs ?? 0) / 1000).toFixed(1)}s`}
|
||||
pinned={livePinned('analysis:last')}
|
||||
onTogglePin={toggleLive('analysis:last')}
|
||||
/>
|
||||
)}
|
||||
</PerfProbeMetricSection>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { setPerfProbeFlag, type PerfProbeFlags } from '../../../utils/perf/perfFlags';
|
||||
import type { PerfToggleEngineLeaf, PerfToggleLeaf, PerfToggleNode } from '../../../utils/perf/perfProbeToggleTree';
|
||||
import {
|
||||
isPerfToggleEngineLeaf,
|
||||
isPerfToggleGroup,
|
||||
PERF_PROBE_TOGGLE_TREE,
|
||||
} from '../../../utils/perf/perfProbeToggleTree';
|
||||
|
||||
interface EngineProps {
|
||||
hotCacheEnabled: boolean;
|
||||
setHotCacheEnabled: (v: boolean) => void;
|
||||
normalizationEngine: string;
|
||||
setNormalizationEngine: (v: 'off' | 'loudness') => void;
|
||||
loggingMode: string;
|
||||
setLoggingMode: (v: 'off' | 'normal') => void;
|
||||
}
|
||||
|
||||
interface Props extends EngineProps {
|
||||
perfFlags: PerfProbeFlags;
|
||||
}
|
||||
|
||||
function ToggleLeaf({
|
||||
node,
|
||||
perfFlags,
|
||||
engineProps,
|
||||
}: {
|
||||
node: PerfToggleLeaf | PerfToggleEngineLeaf;
|
||||
perfFlags: PerfProbeFlags;
|
||||
engineProps: EngineProps;
|
||||
}) {
|
||||
if (isPerfToggleEngineLeaf(node)) {
|
||||
const checked = node.engine === 'hotCache'
|
||||
? !engineProps.hotCacheEnabled
|
||||
: node.engine === 'normalization'
|
||||
? engineProps.normalizationEngine === 'off'
|
||||
: engineProps.loggingMode === 'off';
|
||||
return (
|
||||
<label className="perf-tree-leaf">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={e => {
|
||||
if (node.engine === 'hotCache') engineProps.setHotCacheEnabled(!e.target.checked);
|
||||
else if (node.engine === 'normalization') {
|
||||
engineProps.setNormalizationEngine(e.target.checked ? 'off' : 'loudness');
|
||||
} else engineProps.setLoggingMode(e.target.checked ? 'off' : 'normal');
|
||||
}}
|
||||
/>
|
||||
<span className="perf-tree-leaf__text">
|
||||
<span className="perf-tree-leaf__label">{node.label}</span>
|
||||
{node.description && (
|
||||
<span className="perf-tree-leaf__desc">{node.description}</span>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if ('flag' in node) {
|
||||
return (
|
||||
<label className="perf-tree-leaf">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perfFlags[node.flag]}
|
||||
onChange={e => setPerfProbeFlag(node.flag, e.target.checked)}
|
||||
/>
|
||||
<span className="perf-tree-leaf__text">
|
||||
<span className="perf-tree-leaf__label">{node.label}</span>
|
||||
{node.description && (
|
||||
<span className="perf-tree-leaf__desc">{node.description}</span>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ToggleTreeNode({
|
||||
node,
|
||||
depth,
|
||||
perfFlags,
|
||||
engineProps,
|
||||
}: {
|
||||
node: PerfToggleNode;
|
||||
depth: number;
|
||||
perfFlags: PerfProbeFlags;
|
||||
engineProps: EngineProps;
|
||||
}) {
|
||||
const [open, setOpen] = useState(depth < 1);
|
||||
|
||||
if (isPerfToggleGroup(node)) {
|
||||
return (
|
||||
<div className="perf-tree-group" style={{ '--perf-tree-depth': depth } as CSSProperties}>
|
||||
<button
|
||||
type="button"
|
||||
className="perf-tree-group__head"
|
||||
onClick={() => setOpen(v => !v)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<ChevronRight size={14} className={`perf-tree-group__chevron${open ? ' perf-tree-group__chevron--open' : ''}`} />
|
||||
<span>{node.label}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="perf-tree-group__body">
|
||||
{node.children.map(child => (
|
||||
<ToggleTreeNode
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
perfFlags={perfFlags}
|
||||
engineProps={engineProps}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="perf-tree-leaf-wrap" style={{ '--perf-tree-depth': depth } as CSSProperties}>
|
||||
<ToggleLeaf node={node} perfFlags={perfFlags} engineProps={engineProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SidebarPerfProbeTogglesTab({
|
||||
perfFlags,
|
||||
hotCacheEnabled,
|
||||
setHotCacheEnabled,
|
||||
normalizationEngine,
|
||||
setNormalizationEngine,
|
||||
loggingMode,
|
||||
setLoggingMode,
|
||||
}: Props) {
|
||||
const engineProps: EngineProps = {
|
||||
hotCacheEnabled,
|
||||
setHotCacheEnabled,
|
||||
normalizationEngine,
|
||||
setNormalizationEngine,
|
||||
loggingMode,
|
||||
setLoggingMode,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="perf-toggle-tree">
|
||||
<p className="sidebar-perf-modal__hint perf-toggle-tree__hint">
|
||||
Disable subsystems one at a time to estimate their UI cost. Changes apply immediately.
|
||||
</p>
|
||||
{PERF_PROBE_TOGGLE_TREE.map(group => (
|
||||
<ToggleTreeNode
|
||||
key={group.id}
|
||||
node={group}
|
||||
depth={0}
|
||||
perfFlags={perfFlags}
|
||||
engineProps={engineProps}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user