fix(perf): reduce idle Rust CPU and stabilize Performance Probe overlay (#939)

* fix(perf): skip Performance Probe CPU snapshot poll on Windows

Windows has no Rust CPU/RSS sampler, but the probe still invoked
performance_cpu_snapshot every 2s when the modal or overlay pins were
active. Skip the IPC on unsupported platforms and only poll JS-side
metrics; do not start overlay polling for CPU/memory pins alone.

* fix(analysis): park backfill coordinator until Advanced is configured

#881 started run_coordinator_forever at app init with a 2s sleep even when
disabled, waking tokio on every platform for no work. Park on Notify instead;
wake on configure (enable/disable) and library sync-idle. Long sleeps use
select with wake so sync-idle can interrupt COMPLETED_RECHECK waits.

Investigation branch — not for merge until periodic CPU root cause is confirmed.

* fix(perf): stop probe overlay flicker on live poll updates

Publish CPU samples only after a valid jiffies baseline, skip no-op
snapshot emits, and sync sparkline history atomically in the store.
Overlay uses wall-clock sparkline time and auto-scales low CPU values.

* fix(perf): cut idle Rust CPU from probe scan, cover prefetch, and storage poll

Move performance_cpu_snapshot /proc work to spawn_blocking so tokio
workers are not charged with probe sampling. Stop lazy cover strategy
from running route prefetch disk stats every 1.5s, and slow hot-cache
size refresh on Settings → Storage to 15s.

* fix(perf): stabilize probe sparkline clock between live poll ticks

Track sampleAt separately from updatedAt so CPU rate history and overlay
sparklines only advance on real % changes, not FPS re-renders or RSS-only
poll ticks. Hold CPU sparkline Y scale with a peak ref to avoid scale jumps.

* fix(cover): restore lazy route prefetch without idle disk stats poll

Re-enable lazy cover registry warm-up so cached WebP paths reach
diskSrcCache before cells mount. Skip cover_cache_stats on every 1.5s
tick — drain batches via ensure only, poll full disk usage every 30s
when the registry is idle.

* docs: CHANGELOG and credits for PR #939 idle CPU perf fix

* fix(cover): peek before route prefetch ensure to match main responsiveness

Route prefetch moved batch drain ahead of cover_cache_stats for idle CPU,
which removed the accidental throttle and flooded ensure invoke slots.
Use warmCoverDiskSrcBatch first (cached hits skip ensure), ensure misses
only, and yield while high-priority viewport work is queued.
This commit is contained in:
cucadmuh
2026-06-01 15:50:17 +03:00
committed by GitHub
parent 4ac373a65b
commit 08b6aeeb17
14 changed files with 353 additions and 138 deletions
+16 -24
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
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';
@@ -18,12 +18,10 @@ import {
type LiveOverlayItem,
} from '../utils/perf/formatLiveOverlayItems';
import {
getPerfLiveHistoryClock,
syncPerfLiveHistoryFromPoll,
usePerfLiveHistorySamples,
getPerfLiveHistorySamples,
} from '../utils/perf/perfLiveHistory';
import { acquirePerfLivePoll, usePerfLiveSnapshot } from '../utils/perf/perfLiveStore';
import { hasAnyLiveMetricPollNeed, usePerfLiveOverlayPins } from '../utils/perf/perfOverlayPins';
import { usePerfLiveSnapshot } from '../utils/perf/perfLiveStore';
import { usePerfLiveOverlayPins } from '../utils/perf/perfOverlayPins';
import {
perfOverlayCornerClass,
usePerfOverlayAppearance,
@@ -41,11 +39,12 @@ const QUEUE_STATS_MS = 750;
function LiveOverlayPinnedMetric({
item,
now,
history,
}: {
item: LiveOverlayItem;
now: number;
history: ReturnType<typeof getPerfLiveHistorySamples>;
}) {
const history = usePerfLiveHistorySamples(item.id);
const sparklineKind = item.kind === 'memory' ? 'memory' : 'cpu';
return (
@@ -70,7 +69,6 @@ export default function FpsOverlay() {
const [queueStats, setQueueStats] = useState<AnalysisPipelineQueueStatsDto | null>(null);
const [coverQueueLines, setCoverQueueLines] = useState<string[]>([]);
const last = useAnalysisPerfLast();
const lastHistoryAt = useRef(0);
const liveOverlayItems = useMemo(
() => buildLiveOverlayItems(livePins, live),
@@ -89,24 +87,13 @@ export default function FpsOverlay() {
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]);
const sparklineNow = useMemo(
() => (live.sampleAt > 0 ? live.sampleAt : Date.now()),
[live.sampleAt],
);
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) {
setTpm(0);
@@ -219,7 +206,12 @@ export default function FpsOverlay() {
<div className="fps-overlay__block">
<div className="fps-overlay__block-title">Live</div>
{liveOverlayItems.map(item => (
<LiveOverlayPinnedMetric key={item.id} item={item} now={sparklineNow} />
<LiveOverlayPinnedMetric
key={item.id}
item={item}
now={sparklineNow}
history={item.sparkline ? getPerfLiveHistorySamples(item.id) : []}
/>
))}
</div>
)}