mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
77ecc8ddfe
Stop replacing the whole Monitor tab when CPU/RSS sampling is unsupported; show pipeline, UI rate, and analysis sections with an inline platform note. Also compute UI diagRates when the Rust snapshot returns supported: false.
293 lines
8.1 KiB
TypeScript
293 lines
8.1 KiB
TypeScript
import { useSyncExternalStore } from 'react';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { clearPerfLiveHistory } from './perfLiveHistory';
|
|
import { getAnalysisTracksPerMinute } from './analysisPerfStore';
|
|
import {
|
|
buildPerfCpuSnapshotRequest,
|
|
getPerfLivePollIntervalMs,
|
|
registerPerfLivePollScheduleBump,
|
|
} from './perfLivePollSettings';
|
|
|
|
export type PerfProcessMemory = {
|
|
label: string;
|
|
rss_kb: number;
|
|
};
|
|
|
|
export type PerfThreadCpu = {
|
|
label: string;
|
|
threadCount: number;
|
|
pct: number;
|
|
};
|
|
|
|
export type PerfLiveCpu = {
|
|
app: number;
|
|
webkit: number;
|
|
supported: boolean;
|
|
memory: PerfProcessMemory[];
|
|
threadCpu: PerfThreadCpu[];
|
|
};
|
|
|
|
export type PerfDiagRates = {
|
|
progress: number;
|
|
waveform: number;
|
|
home: number;
|
|
};
|
|
|
|
export type PerfAnalysisDiag = {
|
|
tracksPerMinute: number;
|
|
lastTotalMs: number | null;
|
|
lastFetchMs: number | null;
|
|
lastSeedMs: number | null;
|
|
lastBpmMs: number | null;
|
|
};
|
|
|
|
export type PerfLiveSnapshot = {
|
|
cpu: PerfLiveCpu | null;
|
|
diagRates: PerfDiagRates | null;
|
|
analysis: PerfAnalysisDiag | null;
|
|
collecting: boolean;
|
|
/** Wall time of the last CPU poll; shared clock for overlay sparklines. */
|
|
updatedAt: number;
|
|
};
|
|
|
|
type ProcSnapshot = {
|
|
supported: boolean;
|
|
total_jiffies: number;
|
|
app_jiffies: number;
|
|
webkit_jiffies: number;
|
|
logical_cpus: number;
|
|
memory: PerfProcessMemory[];
|
|
thread_cpu_groups: Array<{ label: string; thread_count: number; jiffies: number }>;
|
|
};
|
|
|
|
const EMPTY: PerfLiveSnapshot = {
|
|
cpu: null,
|
|
diagRates: null,
|
|
analysis: null,
|
|
collecting: false,
|
|
updatedAt: 0,
|
|
};
|
|
|
|
let snapshot: PerfLiveSnapshot = { ...EMPTY };
|
|
let pollRefCount = 0;
|
|
const listeners = new Set<() => void>();
|
|
let pollTimer: number | null = null;
|
|
let prevProc: ProcSnapshot | null = null;
|
|
let prevCounters: { progress: number; waveform: number; home: number } | null = null;
|
|
let prevCountersAt = 0;
|
|
let pollGeneration = 0;
|
|
|
|
function emit(): void {
|
|
listeners.forEach(fn => fn());
|
|
}
|
|
|
|
function setSnapshot(next: PerfLiveSnapshot): void {
|
|
snapshot = next;
|
|
emit();
|
|
}
|
|
|
|
function readUiCounters(): { progress: number; waveform: number; home: number } {
|
|
const root = globalThis as unknown as { __psyPerfCounters?: Record<string, number> };
|
|
const counters = root.__psyPerfCounters ?? {};
|
|
return {
|
|
progress: counters.audioProgressEvents ?? 0,
|
|
waveform: counters.waveformDraws ?? 0,
|
|
home: counters.homeCommits ?? 0,
|
|
};
|
|
}
|
|
|
|
function buildAnalysisDiag(): PerfAnalysisDiag {
|
|
return {
|
|
tracksPerMinute: getAnalysisTracksPerMinute(),
|
|
lastTotalMs: snapshot.analysis?.lastTotalMs ?? null,
|
|
lastFetchMs: snapshot.analysis?.lastFetchMs ?? null,
|
|
lastSeedMs: snapshot.analysis?.lastSeedMs ?? null,
|
|
lastBpmMs: snapshot.analysis?.lastBpmMs ?? null,
|
|
};
|
|
}
|
|
|
|
function nextDiagRates(
|
|
nextCounters: { progress: number; waveform: number; home: number },
|
|
now: number,
|
|
): PerfDiagRates | null {
|
|
if (!prevCounters || prevCountersAt <= 0) return snapshot.diagRates;
|
|
const dt = Math.max(0.25, (now - prevCountersAt) / 1000);
|
|
return {
|
|
progress: (nextCounters.progress - prevCounters.progress) / dt,
|
|
waveform: (nextCounters.waveform - prevCounters.waveform) / dt,
|
|
home: (nextCounters.home - prevCounters.home) / dt,
|
|
};
|
|
}
|
|
|
|
async function pollOnce(): Promise<void> {
|
|
const generation = pollGeneration;
|
|
const now = Date.now();
|
|
try {
|
|
const snap = await invoke<ProcSnapshot>('performance_cpu_snapshot', buildPerfCpuSnapshotRequest());
|
|
if (generation !== pollGeneration) return;
|
|
|
|
const nextCounters = readUiCounters();
|
|
const diagRates = nextDiagRates(nextCounters, now);
|
|
prevCounters = nextCounters;
|
|
prevCountersAt = now;
|
|
|
|
if (!snap.supported) {
|
|
setSnapshot({
|
|
cpu: { app: 0, webkit: 0, supported: false, memory: [], threadCpu: [] },
|
|
diagRates,
|
|
analysis: buildAnalysisDiag(),
|
|
collecting: false,
|
|
updatedAt: now,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const memory = snap.memory;
|
|
let cpu: PerfLiveCpu = {
|
|
app: snapshot.cpu?.app ?? 0,
|
|
webkit: snapshot.cpu?.webkit ?? 0,
|
|
supported: true,
|
|
memory,
|
|
threadCpu: snap.thread_cpu_groups.map(g => ({
|
|
label: g.label,
|
|
threadCount: g.thread_count,
|
|
pct: snapshot.cpu?.threadCpu.find(t => t.label === g.label)?.pct ?? 0,
|
|
})),
|
|
};
|
|
|
|
if (prevProc) {
|
|
const totalDelta = snap.total_jiffies - prevProc.total_jiffies;
|
|
const appDelta = snap.app_jiffies - prevProc.app_jiffies;
|
|
const webkitDelta = snap.webkit_jiffies - prevProc.webkit_jiffies;
|
|
if (totalDelta > 0) {
|
|
const cpuScale = Math.max(1, snap.logical_cpus || 1) * 100;
|
|
const prevThreadByLabel = new Map(
|
|
prevProc.thread_cpu_groups.map(g => [g.label, g.jiffies]),
|
|
);
|
|
cpu = {
|
|
app: clampPct((appDelta / totalDelta) * cpuScale),
|
|
webkit: clampPct((webkitDelta / totalDelta) * cpuScale),
|
|
supported: true,
|
|
memory,
|
|
threadCpu: snap.thread_cpu_groups.map(g => {
|
|
const prevJiffies = prevThreadByLabel.get(g.label) ?? g.jiffies;
|
|
const delta = g.jiffies - prevJiffies;
|
|
return {
|
|
label: g.label,
|
|
threadCount: g.thread_count,
|
|
pct: clampPct((delta / totalDelta) * cpuScale),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
prevProc = snap;
|
|
|
|
setSnapshot({
|
|
cpu,
|
|
diagRates,
|
|
analysis: buildAnalysisDiag(),
|
|
collecting: false,
|
|
updatedAt: now,
|
|
});
|
|
} catch {
|
|
if (generation !== pollGeneration) return;
|
|
setSnapshot({
|
|
...snapshot,
|
|
cpu: { app: 0, webkit: 0, supported: false, memory: [], threadCpu: [] },
|
|
collecting: false,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
}
|
|
|
|
function clampPct(value: number): number {
|
|
if (!Number.isFinite(value)) return 0;
|
|
return Math.max(0, Math.min(1000, value));
|
|
}
|
|
|
|
function schedulePoll(): void {
|
|
if (pollTimer != null) return;
|
|
setSnapshot({ ...snapshot, collecting: snapshot.cpu == null });
|
|
const intervalMs = getPerfLivePollIntervalMs();
|
|
const tick = () => {
|
|
pollTimer = null;
|
|
if (pollRefCount === 0) return;
|
|
void pollOnce().finally(() => {
|
|
if (pollRefCount > 0) {
|
|
pollTimer = window.setTimeout(tick, getPerfLivePollIntervalMs());
|
|
}
|
|
});
|
|
};
|
|
void pollOnce().finally(() => {
|
|
if (pollRefCount > 0) {
|
|
pollTimer = window.setTimeout(tick, intervalMs);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Restart the poll loop after interval / snapshot options change. */
|
|
export function bumpPerfLivePollSchedule(): void {
|
|
if (pollRefCount === 0) return;
|
|
pollGeneration += 1;
|
|
if (pollTimer != null) {
|
|
window.clearTimeout(pollTimer);
|
|
pollTimer = null;
|
|
}
|
|
// Fresh baseline after interval / thread-group option changes.
|
|
prevProc = null;
|
|
schedulePoll();
|
|
}
|
|
|
|
registerPerfLivePollScheduleBump(bumpPerfLivePollSchedule);
|
|
|
|
function stopPoll(): void {
|
|
pollGeneration += 1;
|
|
if (pollTimer != null) {
|
|
window.clearTimeout(pollTimer);
|
|
pollTimer = null;
|
|
}
|
|
prevProc = null;
|
|
prevCounters = null;
|
|
prevCountersAt = 0;
|
|
clearPerfLiveHistory();
|
|
setSnapshot({ ...EMPTY });
|
|
}
|
|
|
|
export function acquirePerfLivePoll(_reason: string): () => void {
|
|
const start = pollRefCount === 0;
|
|
pollRefCount += 1;
|
|
if (start) schedulePoll();
|
|
return () => {
|
|
pollRefCount = Math.max(0, pollRefCount - 1);
|
|
if (pollRefCount === 0) stopPoll();
|
|
};
|
|
}
|
|
|
|
export function patchPerfLiveAnalysis(partial: Partial<PerfAnalysisDiag>): void {
|
|
setSnapshot({
|
|
...snapshot,
|
|
analysis: {
|
|
tracksPerMinute: partial.tracksPerMinute ?? snapshot.analysis?.tracksPerMinute ?? 0,
|
|
lastTotalMs: partial.lastTotalMs ?? snapshot.analysis?.lastTotalMs ?? null,
|
|
lastFetchMs: partial.lastFetchMs ?? snapshot.analysis?.lastFetchMs ?? null,
|
|
lastSeedMs: partial.lastSeedMs ?? snapshot.analysis?.lastSeedMs ?? null,
|
|
lastBpmMs: partial.lastBpmMs ?? snapshot.analysis?.lastBpmMs ?? null,
|
|
},
|
|
});
|
|
}
|
|
|
|
export function getPerfLiveSnapshot(): PerfLiveSnapshot {
|
|
return snapshot;
|
|
}
|
|
|
|
export function subscribePerfLiveSnapshot(cb: () => void): () => void {
|
|
listeners.add(cb);
|
|
return () => listeners.delete(cb);
|
|
}
|
|
|
|
export function usePerfLiveSnapshot(): PerfLiveSnapshot {
|
|
return useSyncExternalStore(subscribePerfLiveSnapshot, getPerfLiveSnapshot, () => EMPTY);
|
|
}
|