mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
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:
@@ -5,18 +5,42 @@ import { useCoverStrategyStore } from '../store/coverStrategyStore';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { coverPrefetchDrainBatch } from './prefetchRegistry';
|
||||
import { coverTrafficBackgroundPaused } from './coverTraffic';
|
||||
import { coverEnsureQueued } from './ensureQueue';
|
||||
import { coverEnsureQueued, coverEnsureQueueStats } from './ensureQueue';
|
||||
import { getDiskSrcForGrid } from './diskSrcLookup';
|
||||
import { coverStorageKeyFromRef } from './storageKeys';
|
||||
import { warmCoverDiskSrcBatch, type CoverWarmItem } from './warmDiskPeek';
|
||||
import { resolveCoverDisplayTier } from './tiers';
|
||||
import type { CoverArtTier } from './types';
|
||||
import type { CoverArtRef, CoverArtTier } from './types';
|
||||
|
||||
const STEADY_POLL_MS = 1500;
|
||||
/** Full cover-root disk walk — idle only, not every prefetch tick. */
|
||||
const STATS_IDLE_POLL_MS = 30_000;
|
||||
const BATCH_LIMIT = 12;
|
||||
/** Match dense card thumbs (~160 CSS px) — prefetch 128 wasted a full re-ensure for 512. */
|
||||
const DENSE_PREFETCH_TIER = resolveCoverDisplayTier(160, { surface: 'dense' }) as CoverArtTier;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function batchWarmItems(refs: CoverArtRef[]): CoverWarmItem[] {
|
||||
return refs.map(ref => ({
|
||||
ref,
|
||||
tier: DENSE_PREFETCH_TIER,
|
||||
storageKey: coverStorageKeyFromRef(ref, DENSE_PREFETCH_TIER),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Back off while viewport cells are waiting on high-priority ensures. */
|
||||
function prefetchShouldYieldToViewport(): boolean {
|
||||
const { queuedHigh, inflight, maxInflight } = coverEnsureQueueStats();
|
||||
return queuedHigh > 0 || inflight >= maxInflight - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Background cover warm-up — low rate; Rust HTTP only (never competes with webview grid fetches).
|
||||
* Registry drains: batched disk peek first (cached WebP → diskSrcCache), ensure only for misses.
|
||||
* Stats (`cover_cache_stats` disk walk) run rarely when the registry is idle.
|
||||
*/
|
||||
export function useCoverArtPrefetch(enabled = true): void {
|
||||
const activeServerId = useAuthStore(s => s.activeServerId);
|
||||
@@ -25,31 +49,48 @@ export function useCoverArtPrefetch(enabled = true): void {
|
||||
useEffect(() => {
|
||||
if (!enabled || !activeServerId || !coverStrategyAllowsRoutePrefetch(strategy)) return;
|
||||
let cancelled = false;
|
||||
let lastStatsAt = 0;
|
||||
let autoDownloadEnabled = true;
|
||||
|
||||
void (async () => {
|
||||
while (!cancelled) {
|
||||
if (coverTrafficBackgroundPaused()) {
|
||||
await new Promise(r => setTimeout(r, STEADY_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
const stats = await coverCacheStats().catch(() => null);
|
||||
if (stats && !stats.autoDownloadEnabled) {
|
||||
await new Promise(r => setTimeout(r, STEADY_POLL_MS * 2));
|
||||
await sleep(STEADY_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
const batch = coverPrefetchDrainBatch(BATCH_LIMIT);
|
||||
if (batch.length > 0) {
|
||||
await Promise.all(
|
||||
batch.map(ref => {
|
||||
const key = coverStorageKeyFromRef(ref, DENSE_PREFETCH_TIER);
|
||||
return coverEnsureQueued(key, ref, DENSE_PREFETCH_TIER, 'low');
|
||||
}),
|
||||
);
|
||||
if (prefetchShouldYieldToViewport()) {
|
||||
await sleep(STEADY_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
await warmCoverDiskSrcBatch(batchWarmItems(batch));
|
||||
|
||||
if (autoDownloadEnabled) {
|
||||
const misses = batch.filter(ref => !getDiskSrcForGrid(ref, DENSE_PREFETCH_TIER));
|
||||
if (misses.length > 0) {
|
||||
await Promise.all(
|
||||
misses.map(ref => {
|
||||
const key = coverStorageKeyFromRef(ref, DENSE_PREFETCH_TIER);
|
||||
return coverEnsureQueued(key, ref, DENSE_PREFETCH_TIER, 'low');
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
await sleep(STEADY_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, STEADY_POLL_MS));
|
||||
const now = Date.now();
|
||||
if (now - lastStatsAt >= STATS_IDLE_POLL_MS) {
|
||||
const stats = await coverCacheStats().catch(() => null);
|
||||
lastStatsAt = now;
|
||||
autoDownloadEnabled = stats?.autoDownloadEnabled ?? true;
|
||||
}
|
||||
|
||||
await sleep(STEADY_POLL_MS);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user