diff --git a/CHANGELOG.md b/CHANGELOG.md index 352d1d31..e665711f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -388,6 +388,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +### Performance Probe — responsive throughput windows (tpm / cpm) + +**By [@cucadmuh](https://github.com/cucadmuh), PR [#948](https://github.com/Psychotoxical/psysonic/pull/948)** + +* Analysis **tpm** and cover **cpm** (lib + ui) now measure throughput over the trailing **5 seconds** instead of a full-minute rolling average. The figure is still extrapolated to per-minute, but reacts promptly to bursts/stalls and decays to 0 within the window when idle, instead of coasting on minute-long inertia. + + ## Fixed diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index 059897cd..3ca718d7 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -149,6 +149,7 @@ const CONTRIBUTOR_ENTRIES = [ 'Performance Probe: cover pipeline covers-per-minute (cpm) throughput, the cover analogue of analysis tpm (PR #945)', 'Performance Probe: live runtime logs tab with depth switch, line cap, and ordered include/exclude word filter (PR #946)', 'Performance Probe: on-demand (ui) cover throughput alongside backfill (lib) cpm (PR #947)', + 'Performance Probe: throughput (analysis tpm, cover cpm) measured over a trailing 5s window so the rate reacts promptly instead of coasting on minute-long inertia (PR #948)', ], }, { diff --git a/src/utils/perf/analysisPerfStore.test.ts b/src/utils/perf/analysisPerfStore.test.ts index 37884ab8..48f59e2d 100644 --- a/src/utils/perf/analysisPerfStore.test.ts +++ b/src/utils/perf/analysisPerfStore.test.ts @@ -14,31 +14,36 @@ afterEach(() => { }); describe('analysisPerfStore', () => { - it('records last track timings and rolling tpm', () => { + const record = (trackId: string): void => + recordAnalysisTrackPerf({ trackId, fetchMs: 1, seedMs: 1, bpmMs: 1, totalMs: 3 }); + + it('records last track timings and trailing-window tpm', () => { vi.useFakeTimers(); vi.setSystemTime(1_000_000); - recordAnalysisTrackPerf({ - trackId: 't1', - fetchMs: 1000, - seedMs: 2000, - bpmMs: 500, - totalMs: 3500, - }); + record('t1'); vi.advanceTimersByTime(100); - expect(getAnalysisTracksPerMinute()).toBeGreaterThan(0); + // 1 completion in the trailing 5s → 1 / 5s × 60 = 12 tpm. + expect(getAnalysisTracksPerMinute()).toBeCloseTo(12, 0); }); - it('prunes completions older than one minute from tpm window', () => { + it('extrapolates a burst over the trailing 5s window', () => { vi.useFakeTimers(); vi.setSystemTime(2_000_000); - recordAnalysisTrackPerf({ - trackId: 'old', - fetchMs: 1, - seedMs: 1, - bpmMs: 1, - totalMs: 3, - }); - vi.advanceTimersByTime(61_000); + record('a'); + vi.advanceTimersByTime(1_000); + record('b'); + vi.advanceTimersByTime(1_000); + record('c'); + // 3 completions within the last 5s → 3 / 5s × 60 = 36 tpm. + expect(getAnalysisTracksPerMinute()).toBeCloseTo(36, 0); + }); + + it('decays to 0 once completions fall outside the trailing window', () => { + vi.useFakeTimers(); + vi.setSystemTime(3_000_000); + record('old'); + expect(getAnalysisTracksPerMinute()).toBeGreaterThan(0); + vi.advanceTimersByTime(6_000); expect(getAnalysisTracksPerMinute()).toBe(0); }); }); diff --git a/src/utils/perf/analysisPerfStore.ts b/src/utils/perf/analysisPerfStore.ts index aca8714d..f6ab58d1 100644 --- a/src/utils/perf/analysisPerfStore.ts +++ b/src/utils/perf/analysisPerfStore.ts @@ -14,7 +14,14 @@ type AnalysisPerfState = { completedAt: number[]; }; +/** Completion-timestamp retention (kept generous; the live rate uses a shorter window). */ const WINDOW_MS = 60_000; +/** + * Throughput is measured over the trailing few seconds only — a full-minute + * average has too much inertia and flattens real bursts/stalls. The count in + * this window is extrapolated to a per-minute figure for display. + */ +const RATE_WINDOW_MS = 5_000; let state: AnalysisPerfState = { last: null, completedAt: [] }; const listeners = new Set<() => void>(); @@ -51,11 +58,12 @@ export function recordAnalysisTrackPerf(payload: { emit(); } +/** Tracks analyzed per minute, measured over the trailing few seconds (0 when idle). */ export function getAnalysisTracksPerMinute(now = Date.now()): number { - const completedAt = pruneCompletedAt(now); - if (completedAt.length === 0) return 0; - const spanMs = Math.max(1, Math.min(WINDOW_MS, now - completedAt[0])); - return (completedAt.length / spanMs) * WINDOW_MS; + const cutoff = now - RATE_WINDOW_MS; + const count = state.completedAt.filter(t => t >= cutoff).length; + if (count === 0) return 0; + return (count / RATE_WINDOW_MS) * 60_000; } export function getAnalysisPerfState(): AnalysisPerfState {