mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(perf): cover pipeline throughput (cpm) in performance probe (#945)
* feat(perf): cover pipeline throughput (cpm) in performance probe Mirror the analysis pipeline's tpm for covers. A new coverPerfStore samples the backfill `done` progress from cover:library-progress events and derives a rolling one-minute covers-per-minute rate. Surfaced as a live diag in perfLiveStore, a pinnable "Cover backfill" throughput card in the Monitor tab, and a cpm row in the Cover pipeline overlay block. * docs(changelog): note cover pipeline cpm metric (PR #945) Add CHANGELOG entry and credits line for the cover-pipeline covers-per-minute throughput metric in the Performance Probe.
This commit is contained in:
@@ -367,6 +367,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* **Performance Probe** gains a runtime cover-thread control (`library_cover_backfill_set_parallel`) that resizes the HTTP/encode pools live; "Run full pass now" forces a pass and clears fetch-failed backoff.
|
* **Performance Probe** gains a runtime cover-thread control (`library_cover_backfill_set_parallel`) that resizes the HTTP/encode pools live; "Run full pass now" forces a pass and clears fetch-failed backoff.
|
||||||
* Clearing the active server's cover cache re-arms the idle gate and wakes the worker, and in-pass progress is emitted on a ticker so the offline & cache view keeps counting through the whole scan.
|
* Clearing the active server's cover cache re-arms the idle gate and wakes the worker, and in-pass progress is emitted on a ticker so the offline & cache view keeps counting through the whole scan.
|
||||||
|
|
||||||
|
### Performance Probe — cover pipeline throughput (cpm)
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#945](https://github.com/Psychotoxical/psysonic/pull/945)**
|
||||||
|
|
||||||
|
* The cover pipeline now reports a covers-per-minute throughput (cpm), the analogue of the analysis pipeline's tpm: a rolling one-minute rate derived from the backfill `done` progress. Shown in the Monitor tab "Cover backfill" card (pinnable to the overlay) and in the Cover pipeline overlay block.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import {
|
|||||||
usePerfOverlayMode,
|
usePerfOverlayMode,
|
||||||
} from '../utils/perf/perfOverlayMode';
|
} from '../utils/perf/perfOverlayMode';
|
||||||
import { useAnalysisPerfListener } from '../hooks/useAnalysisPerfListener';
|
import { useAnalysisPerfListener } from '../hooks/useAnalysisPerfListener';
|
||||||
|
import { useCoverPerfListener } from '../hooks/useCoverPerfListener';
|
||||||
|
import { getCoverCachedPerMinute } from '../utils/perf/coverPerfStore';
|
||||||
|
|
||||||
const SAMPLE_MS = 500;
|
const SAMPLE_MS = 500;
|
||||||
const TPM_REFRESH_MS = 500;
|
const TPM_REFRESH_MS = 500;
|
||||||
@@ -66,6 +68,7 @@ export default function FpsOverlay() {
|
|||||||
const overlayAppearance = usePerfOverlayAppearance();
|
const overlayAppearance = usePerfOverlayAppearance();
|
||||||
const [fps, setFps] = useState(0);
|
const [fps, setFps] = useState(0);
|
||||||
const [tpm, setTpm] = useState(0);
|
const [tpm, setTpm] = useState(0);
|
||||||
|
const [cpm, setCpm] = useState(0);
|
||||||
const [queueStats, setQueueStats] = useState<AnalysisPipelineQueueStatsDto | null>(null);
|
const [queueStats, setQueueStats] = useState<AnalysisPipelineQueueStatsDto | null>(null);
|
||||||
const [coverQueueLines, setCoverQueueLines] = useState<string[]>([]);
|
const [coverQueueLines, setCoverQueueLines] = useState<string[]>([]);
|
||||||
const last = useAnalysisPerfLast();
|
const last = useAnalysisPerfLast();
|
||||||
@@ -93,6 +96,7 @@ export default function FpsOverlay() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useAnalysisPerfListener(showAnalysisPerfOverlay || livePins.has('analysis:tpm') || livePins.has('analysis:last'));
|
useAnalysisPerfListener(showAnalysisPerfOverlay || livePins.has('analysis:tpm') || livePins.has('analysis:last'));
|
||||||
|
useCoverPerfListener(showCoverPerfOverlay || livePins.has('cover:cpm'));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showAnalysisPerfOverlay) {
|
if (!showAnalysisPerfOverlay) {
|
||||||
@@ -128,6 +132,17 @@ export default function FpsOverlay() {
|
|||||||
};
|
};
|
||||||
}, [showAnalysisPerfOverlay]);
|
}, [showAnalysisPerfOverlay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showCoverPerfOverlay) {
|
||||||
|
setCpm(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const refresh = () => setCpm(getCoverCachedPerMinute());
|
||||||
|
refresh();
|
||||||
|
const id = window.setInterval(refresh, TPM_REFRESH_MS);
|
||||||
|
return () => window.clearInterval(id);
|
||||||
|
}, [showCoverPerfOverlay]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showCoverPerfOverlay) {
|
if (!showCoverPerfOverlay) {
|
||||||
setCoverQueueLines([]);
|
setCoverQueueLines([]);
|
||||||
@@ -252,6 +267,11 @@ export default function FpsOverlay() {
|
|||||||
{showCoverPerfOverlay && (
|
{showCoverPerfOverlay && (
|
||||||
<div className="fps-overlay__block">
|
<div className="fps-overlay__block">
|
||||||
<div className="fps-overlay__block-title">Cover pipeline</div>
|
<div className="fps-overlay__block-title">Cover pipeline</div>
|
||||||
|
<div className="fps-overlay__row">
|
||||||
|
{cpm.toFixed(1)}
|
||||||
|
{' '}
|
||||||
|
<span className="fps-overlay__unit">cpm</span>
|
||||||
|
</div>
|
||||||
{coverQueueLines.length > 0 ? coverQueueLines.map(line => (
|
{coverQueueLines.length > 0 ? coverQueueLines.map(line => (
|
||||||
<div key={line} className="fps-overlay__row fps-overlay__row--steps">
|
<div key={line} className="fps-overlay__row fps-overlay__row--steps">
|
||||||
{line}
|
{line}
|
||||||
|
|||||||
@@ -221,6 +221,21 @@ export default function SidebarPerfProbeMonitorTab() {
|
|||||||
)}
|
)}
|
||||||
</PerfProbeMetricSection>
|
</PerfProbeMetricSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{live.cover && (
|
||||||
|
<PerfProbeMetricSection title="Cover backfill" defaultOpen={false}>
|
||||||
|
<PerfProbeMetricCard
|
||||||
|
label="Throughput"
|
||||||
|
value={live.cover.cachedPerMinute.toFixed(1)}
|
||||||
|
unit="cpm"
|
||||||
|
detail={live.cover.total > 0
|
||||||
|
? `${live.cover.done.toLocaleString()} / ${live.cover.total.toLocaleString()} cached`
|
||||||
|
: 'covers cached per minute'}
|
||||||
|
pinned={livePinned('cover:cpm')}
|
||||||
|
onTogglePin={toggleLive('cover:cpm')}
|
||||||
|
/>
|
||||||
|
</PerfProbeMetricSection>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ const CONTRIBUTOR_ENTRIES = [
|
|||||||
'Performance: idle Rust CPU — backfill coordinator park, probe overlay stability, throttled idle polls, lazy cover prefetch restore (PR #939)',
|
'Performance: idle Rust CPU — backfill coordinator park, probe overlay stability, throttled idle polls, lazy cover prefetch restore (PR #939)',
|
||||||
'Cover backfill: disk-free idle gate, snapshot-diff worklist, live-tunable parallelism, transient-error retries, and memoized offline & cache stats to stop idle CPU spin (PR #943)',
|
'Cover backfill: disk-free idle gate, snapshot-diff worklist, live-tunable parallelism, transient-error retries, and memoized offline & cache stats to stop idle CPU spin (PR #943)',
|
||||||
'Cover art: fix per-song cover over-fetch on Navidrome — only genuine per-disc artwork expands, collapsing ~520k per-track fetches to one cover per album (PR #944)',
|
'Cover art: fix per-song cover over-fetch on Navidrome — only genuine per-disc artwork expands, collapsing ~520k per-track fetches to one cover per album (PR #944)',
|
||||||
|
'Performance Probe: cover pipeline covers-per-minute (cpm) throughput, the cover analogue of analysis tpm (PR #945)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { recordCoverProgress } from '../utils/perf/coverPerfStore';
|
||||||
|
|
||||||
|
type CoverLibraryProgressPayload = {
|
||||||
|
serverIndexKey?: string;
|
||||||
|
done?: number;
|
||||||
|
total?: number;
|
||||||
|
pending?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Wire Rust `cover:library-progress` events into the cover perf store. */
|
||||||
|
export function useCoverPerfListener(active: boolean): void {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) return;
|
||||||
|
let cancelled = false;
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
void listen<CoverLibraryProgressPayload>('cover:library-progress', ({ payload }) => {
|
||||||
|
if (cancelled || typeof payload?.done !== 'number') return;
|
||||||
|
recordCoverProgress({
|
||||||
|
done: payload.done,
|
||||||
|
total: payload.total,
|
||||||
|
pending: payload.pending,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(fn => {
|
||||||
|
if (cancelled) {
|
||||||
|
fn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unlisten = fn;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
unlisten?.();
|
||||||
|
};
|
||||||
|
}, [active]);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { acquirePerfLivePoll, patchPerfLiveAnalysis } from '../utils/perf/perfLi
|
|||||||
import { setPerfProbeTelemetryActive } from '../utils/perf/perfTelemetry';
|
import { setPerfProbeTelemetryActive } from '../utils/perf/perfTelemetry';
|
||||||
import { useAnalysisPerfLast } from '../utils/perf/analysisPerfStore';
|
import { useAnalysisPerfLast } from '../utils/perf/analysisPerfStore';
|
||||||
import { useAnalysisPerfListener } from './useAnalysisPerfListener';
|
import { useAnalysisPerfListener } from './useAnalysisPerfListener';
|
||||||
|
import { useCoverPerfListener } from './useCoverPerfListener';
|
||||||
import {
|
import {
|
||||||
getPerfProbeFlags,
|
getPerfProbeFlags,
|
||||||
subscribePerfProbeFlags,
|
subscribePerfProbeFlags,
|
||||||
@@ -31,14 +32,28 @@ function useNeedAnalysisTelemetry(perfProbeOpen: boolean, livePins: ReadonlySet<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useNeedCoverTelemetry(perfProbeOpen: boolean, livePins: ReadonlySet<string>): boolean {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
subscribePerfProbeFlags,
|
||||||
|
() => (
|
||||||
|
perfProbeOpen
|
||||||
|
|| getPerfProbeFlags().showCoverPerfOverlay
|
||||||
|
|| livePins.has('cover:cpm')
|
||||||
|
),
|
||||||
|
() => perfProbeOpen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Wires Ctrl+Shift+D probe modal and shared live metric polling. */
|
/** Wires Ctrl+Shift+D probe modal and shared live metric polling. */
|
||||||
export function useSidebarPerfProbe(): Result {
|
export function useSidebarPerfProbe(): Result {
|
||||||
const [perfProbeOpen, setPerfProbeOpen] = useState(false);
|
const [perfProbeOpen, setPerfProbeOpen] = useState(false);
|
||||||
const livePins = usePerfLiveOverlayPins();
|
const livePins = usePerfLiveOverlayPins();
|
||||||
const analysisLast = useAnalysisPerfLast();
|
const analysisLast = useAnalysisPerfLast();
|
||||||
const needAnalysis = useNeedAnalysisTelemetry(perfProbeOpen, livePins);
|
const needAnalysis = useNeedAnalysisTelemetry(perfProbeOpen, livePins);
|
||||||
|
const needCover = useNeedCoverTelemetry(perfProbeOpen, livePins);
|
||||||
|
|
||||||
useAnalysisPerfListener(needAnalysis);
|
useAnalysisPerfListener(needAnalysis);
|
||||||
|
useCoverPerfListener(needCover);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPerfProbeTelemetryActive(perfProbeOpen);
|
setPerfProbeTelemetryActive(perfProbeOpen);
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
getCoverCachedPerMinute,
|
||||||
|
getCoverPerfState,
|
||||||
|
recordCoverProgress,
|
||||||
|
resetCoverPerfStateForTest,
|
||||||
|
} from './coverPerfStore';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetCoverPerfStateForTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('coverPerfStore', () => {
|
||||||
|
it('derives covers-per-minute from done deltas over time', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(1_000_000);
|
||||||
|
recordCoverProgress({ done: 100, total: 1000, pending: 900 });
|
||||||
|
vi.advanceTimersByTime(30_000);
|
||||||
|
recordCoverProgress({ done: 130, total: 1000, pending: 870 });
|
||||||
|
// +30 covers over 30s ≈ 60 cpm.
|
||||||
|
expect(getCoverCachedPerMinute()).toBeCloseTo(60, 0);
|
||||||
|
expect(getCoverPerfState().done).toBe(130);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 with a single sample and prunes the window after a minute', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(2_000_000);
|
||||||
|
recordCoverProgress({ done: 10 });
|
||||||
|
expect(getCoverCachedPerMinute()).toBe(0);
|
||||||
|
vi.advanceTimersByTime(20_000);
|
||||||
|
recordCoverProgress({ done: 20 });
|
||||||
|
expect(getCoverCachedPerMinute()).toBeGreaterThan(0);
|
||||||
|
vi.advanceTimersByTime(61_000);
|
||||||
|
expect(getCoverCachedPerMinute()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the window on a backwards jump (server switch / cache clear)', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(3_000_000);
|
||||||
|
recordCoverProgress({ done: 500 });
|
||||||
|
vi.advanceTimersByTime(5_000);
|
||||||
|
recordCoverProgress({ done: 5 });
|
||||||
|
// Only the new baseline remains → no rate yet.
|
||||||
|
expect(getCoverCachedPerMinute()).toBe(0);
|
||||||
|
expect(getCoverPerfState().done).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cover-pipeline throughput store — the cover analogue of `analysisPerfStore`.
|
||||||
|
*
|
||||||
|
* The backfill worker emits cumulative `done` (covers cached) on the
|
||||||
|
* `cover:library-progress` event. We sample `done` over a rolling one-minute
|
||||||
|
* window and derive covers-per-minute (cpm), mirroring analysis tpm.
|
||||||
|
*/
|
||||||
|
export type CoverProgressSample = {
|
||||||
|
at: number;
|
||||||
|
done: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CoverPerfState = {
|
||||||
|
samples: CoverProgressSample[];
|
||||||
|
done: number;
|
||||||
|
total: number;
|
||||||
|
pending: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WINDOW_MS = 60_000;
|
||||||
|
|
||||||
|
let state: CoverPerfState = { samples: [], done: 0, total: 0, pending: 0 };
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
function emit(): void {
|
||||||
|
listeners.forEach(fn => fn());
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneSamples(now: number, samples: readonly CoverProgressSample[]): CoverProgressSample[] {
|
||||||
|
const cutoff = now - WINDOW_MS;
|
||||||
|
return samples.filter(s => s.at >= cutoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordCoverProgress(payload: {
|
||||||
|
done: number;
|
||||||
|
total?: number;
|
||||||
|
pending?: number;
|
||||||
|
}): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const done = Math.max(0, Math.floor(payload.done));
|
||||||
|
let samples = pruneSamples(now, state.samples);
|
||||||
|
// A backwards jump means a different pass (server switch / cache clear) — start
|
||||||
|
// a fresh window so the old baseline doesn't inflate or zero out the rate.
|
||||||
|
if (samples.length > 0 && done < samples[samples.length - 1].done) {
|
||||||
|
samples = [];
|
||||||
|
}
|
||||||
|
samples = [...samples, { at: now, done }];
|
||||||
|
state = {
|
||||||
|
samples,
|
||||||
|
done,
|
||||||
|
total: payload.total ?? state.total,
|
||||||
|
pending: payload.pending ?? state.pending,
|
||||||
|
};
|
||||||
|
emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Covers cached per minute over the rolling window (0 when idle). */
|
||||||
|
export function getCoverCachedPerMinute(now = Date.now()): number {
|
||||||
|
const samples = pruneSamples(now, state.samples);
|
||||||
|
if (samples.length < 2) return 0;
|
||||||
|
const first = samples[0];
|
||||||
|
const last = samples[samples.length - 1];
|
||||||
|
const delta = Math.max(0, last.done - first.done);
|
||||||
|
if (delta === 0) return 0;
|
||||||
|
const spanMs = Math.max(1, Math.min(WINDOW_MS, now - first.at));
|
||||||
|
return (delta / spanMs) * WINDOW_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCoverPerfState(): CoverPerfState {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeCoverPerf(cb: () => void): () => void {
|
||||||
|
listeners.add(cb);
|
||||||
|
return () => listeners.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCoverPerfState(): CoverPerfState {
|
||||||
|
return useSyncExternalStore(subscribeCoverPerf, getCoverPerfState, () => state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only reset. */
|
||||||
|
export function resetCoverPerfStateForTest(): void {
|
||||||
|
state = { samples: [], done: 0, total: 0, pending: 0 };
|
||||||
|
emit();
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PerfLiveSnapshot } from './perfLiveStore';
|
import type { PerfLiveSnapshot } from './perfLiveStore';
|
||||||
|
|
||||||
export type LiveOverlayItemKind = 'cpu' | 'memory' | 'rate' | 'analysis';
|
export type LiveOverlayItemKind = 'cpu' | 'memory' | 'rate' | 'analysis' | 'cover';
|
||||||
|
|
||||||
export type LiveOverlayItem = {
|
export type LiveOverlayItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -89,6 +89,13 @@ export function buildLiveOverlayItems(
|
|||||||
kind: 'analysis',
|
kind: 'analysis',
|
||||||
sparkline: false,
|
sparkline: false,
|
||||||
});
|
});
|
||||||
|
} else if (pin === 'cover:cpm' && live.cover) {
|
||||||
|
items.push({
|
||||||
|
id: pin,
|
||||||
|
line: `cover ${live.cover.cachedPerMinute.toFixed(1)} cpm`,
|
||||||
|
kind: 'cover',
|
||||||
|
sparkline: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useSyncExternalStore } from 'react';
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { clearPerfLiveHistory, syncPerfLiveHistoryFromPoll } from './perfLiveHistory';
|
import { clearPerfLiveHistory, syncPerfLiveHistoryFromPoll } from './perfLiveHistory';
|
||||||
import { getAnalysisTracksPerMinute } from './analysisPerfStore';
|
import { getAnalysisTracksPerMinute } from './analysisPerfStore';
|
||||||
|
import { getCoverCachedPerMinute, getCoverPerfState } from './coverPerfStore';
|
||||||
import { perfLiveCpuSnapshotSupported } from './perfLiveCpuSnapshot';
|
import { perfLiveCpuSnapshotSupported } from './perfLiveCpuSnapshot';
|
||||||
import { getPerfLiveOverlayPins } from './perfOverlayPins';
|
import { getPerfLiveOverlayPins } from './perfOverlayPins';
|
||||||
import {
|
import {
|
||||||
@@ -43,10 +44,18 @@ export type PerfAnalysisDiag = {
|
|||||||
lastBpmMs: number | null;
|
lastBpmMs: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PerfCoverDiag = {
|
||||||
|
cachedPerMinute: number;
|
||||||
|
done: number;
|
||||||
|
total: number;
|
||||||
|
pending: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type PerfLiveSnapshot = {
|
export type PerfLiveSnapshot = {
|
||||||
cpu: PerfLiveCpu | null;
|
cpu: PerfLiveCpu | null;
|
||||||
diagRates: PerfDiagRates | null;
|
diagRates: PerfDiagRates | null;
|
||||||
analysis: PerfAnalysisDiag | null;
|
analysis: PerfAnalysisDiag | null;
|
||||||
|
cover: PerfCoverDiag | null;
|
||||||
collecting: boolean;
|
collecting: boolean;
|
||||||
/** Wall time of the last displayed sample change (memory / diag / rates). */
|
/** Wall time of the last displayed sample change (memory / diag / rates). */
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
@@ -68,6 +77,7 @@ const EMPTY: PerfLiveSnapshot = {
|
|||||||
cpu: null,
|
cpu: null,
|
||||||
diagRates: null,
|
diagRates: null,
|
||||||
analysis: null,
|
analysis: null,
|
||||||
|
cover: null,
|
||||||
collecting: false,
|
collecting: false,
|
||||||
updatedAt: 0,
|
updatedAt: 0,
|
||||||
sampleAt: 0,
|
sampleAt: 0,
|
||||||
@@ -119,6 +129,14 @@ function analysisEqual(a: PerfAnalysisDiag | null, b: PerfAnalysisDiag | null):
|
|||||||
&& a.lastBpmMs === b.lastBpmMs;
|
&& a.lastBpmMs === b.lastBpmMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coverEqual(a: PerfCoverDiag | null, b: PerfCoverDiag | null): boolean {
|
||||||
|
if (a == null || b == null) return a === b;
|
||||||
|
return a.cachedPerMinute === b.cachedPerMinute
|
||||||
|
&& a.done === b.done
|
||||||
|
&& a.total === b.total
|
||||||
|
&& a.pending === b.pending;
|
||||||
|
}
|
||||||
|
|
||||||
function cpuEqual(a: PerfLiveCpu | null, b: PerfLiveCpu | null): boolean {
|
function cpuEqual(a: PerfLiveCpu | null, b: PerfLiveCpu | null): boolean {
|
||||||
if (a == null || b == null) return a === b;
|
if (a == null || b == null) return a === b;
|
||||||
return a.app === b.app
|
return a.app === b.app
|
||||||
@@ -132,7 +150,8 @@ function publishLiveSnapshot(next: PerfLiveSnapshot): void {
|
|||||||
const cpuChanged = !cpuEqual(snapshot.cpu, next.cpu);
|
const cpuChanged = !cpuEqual(snapshot.cpu, next.cpu);
|
||||||
const diagChanged = !diagRatesEqual(snapshot.diagRates, next.diagRates);
|
const diagChanged = !diagRatesEqual(snapshot.diagRates, next.diagRates);
|
||||||
const analysisChanged = !analysisEqual(snapshot.analysis, next.analysis);
|
const analysisChanged = !analysisEqual(snapshot.analysis, next.analysis);
|
||||||
if (!cpuChanged && !diagChanged && !analysisChanged && next.updatedAt === snapshot.updatedAt) {
|
const coverChanged = !coverEqual(snapshot.cover, next.cover);
|
||||||
|
if (!cpuChanged && !diagChanged && !analysisChanged && !coverChanged && next.updatedAt === snapshot.updatedAt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (next.sampleAt > snapshot.sampleAt && next.cpu?.supported) {
|
if (next.sampleAt > snapshot.sampleAt && next.cpu?.supported) {
|
||||||
@@ -161,6 +180,16 @@ function buildAnalysisDiag(): PerfAnalysisDiag {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCoverDiag(): PerfCoverDiag {
|
||||||
|
const cover = getCoverPerfState();
|
||||||
|
return {
|
||||||
|
cachedPerMinute: getCoverCachedPerMinute(),
|
||||||
|
done: cover.done,
|
||||||
|
total: cover.total,
|
||||||
|
pending: cover.pending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function nextDiagRates(
|
function nextDiagRates(
|
||||||
nextCounters: { progress: number; waveform: number; home: number },
|
nextCounters: { progress: number; waveform: number; home: number },
|
||||||
now: number,
|
now: number,
|
||||||
@@ -191,6 +220,7 @@ function applyJsMetricsSnapshot(now: number): void {
|
|||||||
cpu: snapshot.cpu ?? UNSUPPORTED_CPU,
|
cpu: snapshot.cpu ?? UNSUPPORTED_CPU,
|
||||||
diagRates,
|
diagRates,
|
||||||
analysis: buildAnalysisDiag(),
|
analysis: buildAnalysisDiag(),
|
||||||
|
cover: buildCoverDiag(),
|
||||||
collecting: false,
|
collecting: false,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
sampleAt: snapshot.sampleAt,
|
sampleAt: snapshot.sampleAt,
|
||||||
@@ -221,6 +251,7 @@ async function pollOnce(): Promise<void> {
|
|||||||
cpu: UNSUPPORTED_CPU,
|
cpu: UNSUPPORTED_CPU,
|
||||||
diagRates,
|
diagRates,
|
||||||
analysis: buildAnalysisDiag(),
|
analysis: buildAnalysisDiag(),
|
||||||
|
cover: buildCoverDiag(),
|
||||||
collecting: false,
|
collecting: false,
|
||||||
updatedAt: completedAt,
|
updatedAt: completedAt,
|
||||||
sampleAt: snapshot.sampleAt,
|
sampleAt: snapshot.sampleAt,
|
||||||
@@ -295,6 +326,7 @@ async function pollOnce(): Promise<void> {
|
|||||||
},
|
},
|
||||||
diagRates,
|
diagRates,
|
||||||
analysis: buildAnalysisDiag(),
|
analysis: buildAnalysisDiag(),
|
||||||
|
cover: buildCoverDiag(),
|
||||||
collecting: false,
|
collecting: false,
|
||||||
updatedAt: nextUpdatedAt,
|
updatedAt: nextUpdatedAt,
|
||||||
sampleAt: nextSampleAt,
|
sampleAt: nextSampleAt,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export type PerfLiveOverlayPinId =
|
|||||||
| 'rate:waveform'
|
| 'rate:waveform'
|
||||||
| 'rate:home'
|
| 'rate:home'
|
||||||
| 'analysis:tpm'
|
| 'analysis:tpm'
|
||||||
| 'analysis:last';
|
| 'analysis:last'
|
||||||
|
| 'cover:cpm';
|
||||||
|
|
||||||
const PIPELINE_PIN_TO_FLAG = {
|
const PIPELINE_PIN_TO_FLAG = {
|
||||||
'pipeline:fps': 'showFpsOverlay',
|
'pipeline:fps': 'showFpsOverlay',
|
||||||
@@ -129,7 +130,7 @@ export function hasAnyPerfOverlayVisible(): boolean {
|
|||||||
|
|
||||||
function livePinsNeedJsPoll(pins: ReadonlySet<string>): boolean {
|
function livePinsNeedJsPoll(pins: ReadonlySet<string>): boolean {
|
||||||
for (const id of pins) {
|
for (const id of pins) {
|
||||||
if (id.startsWith('rate:') || id.startsWith('analysis:')) return true;
|
if (id.startsWith('rate:') || id.startsWith('analysis:') || id.startsWith('cover:')) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user