import { useEffect, useMemo, useState, type CSSProperties } from 'react'; import { createPortal } from 'react-dom'; import { analysisGetPipelineQueueStats, type AnalysisPipelineQueueStatsDto } from '../api/analysis'; 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 { getPerfLiveHistorySamples, } from '../utils/perf/perfLiveHistory'; import { usePerfLiveSnapshot } from '../utils/perf/perfLiveStore'; import { usePerfLiveOverlayPins } from '../utils/perf/perfOverlayPins'; import { perfOverlayCornerClass, usePerfOverlayAppearance, } from '../utils/perf/perfOverlayAppearance'; import { resolveOverlayVisibility, usePerfOverlayMode, } from '../utils/perf/perfOverlayMode'; import { useAnalysisPerfListener } from '../hooks/useAnalysisPerfListener'; import { useCoverPerfListener } from '../hooks/useCoverPerfListener'; import { getCoverCachedPerMinute } from '../utils/perf/coverPerfStore'; const SAMPLE_MS = 500; const TPM_REFRESH_MS = 500; const QUEUE_STATS_MS = 750; function LiveOverlayPinnedMetric({ item, now, history, }: { item: LiveOverlayItem; now: number; history: ReturnType; }) { const sparklineKind = item.kind === 'memory' ? 'memory' : 'cpu'; return (
{item.line}
{item.sparkline && ( )}
); } /** FPS + pipeline + pinned live metrics overlay (Performance Probe). */ export default function FpsOverlay() { 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 [cpm, setCpm] = useState(0); const [queueStats, setQueueStats] = useState(null); const [coverQueueLines, setCoverQueueLines] = useState([]); const last = useAnalysisPerfLast(); 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; const sparklineNow = useMemo( () => (live.sampleAt > 0 ? live.sampleAt : Date.now()), [live.sampleAt], ); useAnalysisPerfListener(showAnalysisPerfOverlay || livePins.has('analysis:tpm') || livePins.has('analysis:last')); useCoverPerfListener(showCoverPerfOverlay || livePins.has('cover:cpm')); useEffect(() => { if (!showAnalysisPerfOverlay) { setTpm(0); return; } const refresh = () => setTpm(getAnalysisTracksPerMinute()); refresh(); const id = window.setInterval(refresh, TPM_REFRESH_MS); return () => window.clearInterval(id); }, [showAnalysisPerfOverlay, last?.at]); useEffect(() => { if (!showAnalysisPerfOverlay) { setQueueStats(null); return; } let cancelled = false; const refresh = () => { void analysisGetPipelineQueueStats() .then(stats => { if (!cancelled) setQueueStats(stats); }) .catch(() => { if (!cancelled) setQueueStats(null); }); }; refresh(); const id = window.setInterval(refresh, QUEUE_STATS_MS); return () => { cancelled = true; window.clearInterval(id); }; }, [showAnalysisPerfOverlay]); useEffect(() => { if (!showCoverPerfOverlay) { setCpm(0); return; } const refresh = () => setCpm(getCoverCachedPerMinute()); refresh(); const id = window.setInterval(refresh, TPM_REFRESH_MS); return () => window.clearInterval(id); }, [showCoverPerfOverlay]); 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); 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 (overlayMode === 'off') return null; if (!showFpsOverlay && !showAnalysisPerfOverlay && !showCoverPerfOverlay && !showLive) return null; const analysisQueueLines = queueStats ? formatAnalysisPipelineQueueOverlay(queueStats) : []; return createPortal( , document.body, ); }