mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
08b6aeeb17
* 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.
133 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|