Files
Psychotoxical-psysonic/src/components/perf/PerfOverlaySparkline.tsx
T
cucadmuh 08b6aeeb17 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.
2026-06-01 15:50:17 +03:00

133 lines
4.2 KiB
TypeScript

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(5, maxVal * 1.25) };
}
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 (raw.max > peakRef.current) peakRef.current = raw.max;
if (kind === 'cpu') {
return { min: 0, max: Math.max(5, Math.max(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>
);
}