mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(perf): add on-demand (ui) throughput to cover pipeline cpm (#947)
* feat(perf): add on-demand (ui) throughput to cover pipeline cpm Cover cpm previously measured only the native backfill (lib) via the cover:library-progress done delta. Add a parallel UI series: every completed on-demand Rust cover ensure (grid / now-playing) records a timestamp, surfaced as a covers-per-minute rate. Both are exposed in the live cover diag, shown as separate Backfill (lib) / On-demand (ui) cards in the Monitor tab (each pinnable to the overlay) and as lib/ui rows in the Cover pipeline overlay block. * docs(changelog): note cover on-demand (ui) throughput (PR #947) Add CHANGELOG entry and credits line for the UI cover cpm metric. * fix(perf): source on-demand (ui) cpm from backend produced-cover count The JS ensure-queue counter never tracked: produced covers return hit:true (only misses/errors are hit:false), and ensure-queue dedup/HMR made client counting unreliable. Count on-demand covers natively in ensure_inner on the produce path (non-bulk, past the cache-hit gate), expose a cumulative uiEnsuredTotal in the pipeline stats, and derive the per-minute rate on the frontend from polled deltas — mirroring the lib backfill series. * perf(cover): measure cpm over trailing 5s instead of full minute A 60s rolling average added too much inertia, flattening real bursts and stalls in both the lib backfill and on-demand (ui) cover throughput. Compute the rate from the trailing 5s of samples (still extrapolated to per-minute), so the figure reacts promptly and decays to 0 within the window when idle.
This commit is contained in:
@@ -380,6 +380,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
* 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.
|
||||
|
||||
### Performance Probe — cover pipeline on-demand (ui) throughput
|
||||
|
||||
**By [@cucadmuh](https://github.com/cucadmuh), PR [#947](https://github.com/Psychotoxical/psysonic/pull/947)**
|
||||
|
||||
* Cover cpm previously measured only the native backfill (lib). On-demand UI cover ensures (grid / now-playing) now report their own covers-per-minute rate, shown as separate **Backfill (lib)** and **On-demand (ui)** cards in the Monitor tab (each pinnable) and as `lib`/`ui` rows in the Cover pipeline overlay block.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -24,8 +24,27 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Cumulative count of covers newly produced by on-demand (UI) ensures — the
|
||||
/// source for the Performance Probe "on-demand (ui)" throughput. Library
|
||||
/// backfill (`library_bulk`) is excluded; it reports via `cover:library-progress`.
|
||||
static UI_ENSURE_PRODUCED: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Snapshot of covers produced by on-demand UI ensures since process start.
|
||||
pub fn ui_ensure_produced_total() -> u64 {
|
||||
UI_ENSURE_PRODUCED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Count one freshly produced on-demand cover. Called from `ensure_inner` on the
|
||||
/// produce-success path only (past the early cache-hit gate), so pure cache hits
|
||||
/// and library backfill (`library_bulk`) are excluded.
|
||||
fn note_ui_cover_produced(args: &CoverCacheEnsureArgs) {
|
||||
if !args.library_bulk {
|
||||
UI_ENSURE_PRODUCED.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
@@ -61,6 +80,8 @@ pub struct CoverPipelineQueueStatsDto {
|
||||
pub library_backfill_http_max: u32,
|
||||
pub library_backfill_http_active: u32,
|
||||
pub library_backfill_pass_running: bool,
|
||||
/// Cumulative covers produced by on-demand (UI) ensures since process start.
|
||||
pub ui_ensured_total: u64,
|
||||
}
|
||||
|
||||
fn sem_active(sem: &Semaphore, max: u32) -> u32 {
|
||||
@@ -88,6 +109,7 @@ pub(crate) fn cover_pipeline_queue_stats(
|
||||
library_backfill_http_max,
|
||||
library_backfill_http_active,
|
||||
library_backfill_pass_running,
|
||||
ui_ensured_total: ui_ensure_produced_total(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +359,10 @@ impl CoverCacheState {
|
||||
|
||||
let out_path = tier_path(&dir, requested);
|
||||
if wrote_requested || out_path.is_file() {
|
||||
// Past the early cache-hit gate, so reaching here means this ensure
|
||||
// decoded + (re)encoded a cover. Count on-demand (non-bulk) work for
|
||||
// the Performance Probe "on-demand (ui)" throughput.
|
||||
note_ui_cover_produced(args);
|
||||
if !quiet {
|
||||
if let Some(img) = load_image_from_disk(&dir) {
|
||||
spawn_derive_remaining_tiers(
|
||||
|
||||
@@ -43,6 +43,7 @@ export type CoverPipelineQueueStatsDto = {
|
||||
libraryBackfillHttpMax: number;
|
||||
libraryBackfillHttpActive: number;
|
||||
libraryBackfillPassRunning: boolean;
|
||||
uiEnsuredTotal: number;
|
||||
};
|
||||
|
||||
let coverAutoDownloadEnabled = true;
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from '../utils/perf/perfOverlayMode';
|
||||
import { useAnalysisPerfListener } from '../hooks/useAnalysisPerfListener';
|
||||
import { useCoverPerfListener } from '../hooks/useCoverPerfListener';
|
||||
import { getCoverCachedPerMinute } from '../utils/perf/coverPerfStore';
|
||||
import { getCoverCachedPerMinute, getCoverUiPerMinute } from '../utils/perf/coverPerfStore';
|
||||
|
||||
const SAMPLE_MS = 500;
|
||||
const TPM_REFRESH_MS = 500;
|
||||
@@ -69,6 +69,7 @@ export default function FpsOverlay() {
|
||||
const [fps, setFps] = useState(0);
|
||||
const [tpm, setTpm] = useState(0);
|
||||
const [cpm, setCpm] = useState(0);
|
||||
const [cpmUi, setCpmUi] = useState(0);
|
||||
const [queueStats, setQueueStats] = useState<AnalysisPipelineQueueStatsDto | null>(null);
|
||||
const [coverQueueLines, setCoverQueueLines] = useState<string[]>([]);
|
||||
const last = useAnalysisPerfLast();
|
||||
@@ -135,9 +136,13 @@ export default function FpsOverlay() {
|
||||
useEffect(() => {
|
||||
if (!showCoverPerfOverlay) {
|
||||
setCpm(0);
|
||||
setCpmUi(0);
|
||||
return;
|
||||
}
|
||||
const refresh = () => setCpm(getCoverCachedPerMinute());
|
||||
const refresh = () => {
|
||||
setCpm(getCoverCachedPerMinute());
|
||||
setCpmUi(getCoverUiPerMinute());
|
||||
};
|
||||
refresh();
|
||||
const id = window.setInterval(refresh, TPM_REFRESH_MS);
|
||||
return () => window.clearInterval(id);
|
||||
@@ -270,7 +275,12 @@ export default function FpsOverlay() {
|
||||
<div className="fps-overlay__row">
|
||||
{cpm.toFixed(1)}
|
||||
{' '}
|
||||
<span className="fps-overlay__unit">cpm</span>
|
||||
<span className="fps-overlay__unit">lib cpm</span>
|
||||
</div>
|
||||
<div className="fps-overlay__row">
|
||||
{cpmUi.toFixed(1)}
|
||||
{' '}
|
||||
<span className="fps-overlay__unit">ui cpm</span>
|
||||
</div>
|
||||
{coverQueueLines.length > 0 ? coverQueueLines.map(line => (
|
||||
<div key={line} className="fps-overlay__row fps-overlay__row--steps">
|
||||
|
||||
@@ -223,9 +223,9 @@ export default function SidebarPerfProbeMonitorTab() {
|
||||
)}
|
||||
|
||||
{live.cover && (
|
||||
<PerfProbeMetricSection title="Cover backfill" defaultOpen={false}>
|
||||
<PerfProbeMetricSection title="Cover pipeline" defaultOpen={false}>
|
||||
<PerfProbeMetricCard
|
||||
label="Throughput"
|
||||
label="Backfill (lib)"
|
||||
value={live.cover.cachedPerMinute.toFixed(1)}
|
||||
unit="cpm"
|
||||
detail={live.cover.total > 0
|
||||
@@ -234,6 +234,14 @@ export default function SidebarPerfProbeMonitorTab() {
|
||||
pinned={livePinned('cover:cpm')}
|
||||
onTogglePin={toggleLive('cover:cpm')}
|
||||
/>
|
||||
<PerfProbeMetricCard
|
||||
label="On-demand (ui)"
|
||||
value={live.cover.uiPerMinute.toFixed(1)}
|
||||
unit="cpm"
|
||||
detail="UI cover ensures per minute"
|
||||
pinned={livePinned('cover:cpm:ui')}
|
||||
onTogglePin={toggleLive('cover:cpm:ui')}
|
||||
/>
|
||||
</PerfProbeMetricSection>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -148,6 +148,7 @@ const CONTRIBUTOR_ENTRIES = [
|
||||
'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)',
|
||||
'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)',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { recordCoverProgress } from '../utils/perf/coverPerfStore';
|
||||
import { coverGetPipelineQueueStats } from '../api/coverCache';
|
||||
import { recordCoverProgress, recordCoverUiTotal } from '../utils/perf/coverPerfStore';
|
||||
|
||||
/** How often to sample the backend's cumulative on-demand (UI) ensure count. */
|
||||
const UI_POLL_MS = 1000;
|
||||
|
||||
type CoverLibraryProgressPayload = {
|
||||
serverIndexKey?: string;
|
||||
@@ -37,3 +41,28 @@ export function useCoverPerfListener(active: boolean): void {
|
||||
};
|
||||
}, [active]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the backend's cumulative on-demand (UI) ensure total so the cover store
|
||||
* can derive a per-minute rate. Mount once (always-mounted probe hook) to avoid
|
||||
* duplicate polling.
|
||||
*/
|
||||
export function useCoverUiThroughputPoll(active: boolean): void {
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
let cancelled = false;
|
||||
const sample = (): void => {
|
||||
void coverGetPipelineQueueStats()
|
||||
.then(stats => {
|
||||
if (!cancelled) recordCoverUiTotal(stats.uiEnsuredTotal);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
sample();
|
||||
const timer = window.setInterval(sample, UI_POLL_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [active]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { acquirePerfLivePoll, patchPerfLiveAnalysis } from '../utils/perf/perfLi
|
||||
import { setPerfProbeTelemetryActive } from '../utils/perf/perfTelemetry';
|
||||
import { useAnalysisPerfLast } from '../utils/perf/analysisPerfStore';
|
||||
import { useAnalysisPerfListener } from './useAnalysisPerfListener';
|
||||
import { useCoverPerfListener } from './useCoverPerfListener';
|
||||
import { useCoverPerfListener, useCoverUiThroughputPoll } from './useCoverPerfListener';
|
||||
import {
|
||||
getPerfProbeFlags,
|
||||
subscribePerfProbeFlags,
|
||||
@@ -39,6 +39,7 @@ function useNeedCoverTelemetry(perfProbeOpen: boolean, livePins: ReadonlySet<str
|
||||
perfProbeOpen
|
||||
|| getPerfProbeFlags().showCoverPerfOverlay
|
||||
|| livePins.has('cover:cpm')
|
||||
|| livePins.has('cover:cpm:ui')
|
||||
),
|
||||
() => perfProbeOpen,
|
||||
);
|
||||
@@ -54,6 +55,7 @@ export function useSidebarPerfProbe(): Result {
|
||||
|
||||
useAnalysisPerfListener(needAnalysis);
|
||||
useCoverPerfListener(needCover);
|
||||
useCoverUiThroughputPoll(needCover);
|
||||
|
||||
useEffect(() => {
|
||||
setPerfProbeTelemetryActive(perfProbeOpen);
|
||||
|
||||
@@ -2,7 +2,9 @@ import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest';
|
||||
import {
|
||||
getCoverCachedPerMinute,
|
||||
getCoverPerfState,
|
||||
getCoverUiPerMinute,
|
||||
recordCoverProgress,
|
||||
recordCoverUiTotal,
|
||||
resetCoverPerfStateForTest,
|
||||
} from './coverPerfStore';
|
||||
|
||||
@@ -15,26 +17,29 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('coverPerfStore', () => {
|
||||
it('derives covers-per-minute from done deltas over time', () => {
|
||||
it('derives covers-per-minute from done deltas over the trailing window', () => {
|
||||
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);
|
||||
vi.advanceTimersByTime(1_000);
|
||||
recordCoverProgress({ done: 110, total: 1000, pending: 890 });
|
||||
vi.advanceTimersByTime(1_000);
|
||||
recordCoverProgress({ done: 120, total: 1000, pending: 880 });
|
||||
// +20 covers over the last 2s ≈ 600 cpm (no minute-long inertia).
|
||||
expect(getCoverCachedPerMinute()).toBeCloseTo(600, 0);
|
||||
expect(getCoverPerfState().done).toBe(120);
|
||||
});
|
||||
|
||||
it('returns 0 with a single sample and prunes the window after a minute', () => {
|
||||
it('returns 0 with a single sample and decays once the trailing window empties', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(2_000_000);
|
||||
recordCoverProgress({ done: 10 });
|
||||
expect(getCoverCachedPerMinute()).toBe(0);
|
||||
vi.advanceTimersByTime(20_000);
|
||||
vi.advanceTimersByTime(1_000);
|
||||
recordCoverProgress({ done: 20 });
|
||||
expect(getCoverCachedPerMinute()).toBeGreaterThan(0);
|
||||
vi.advanceTimersByTime(61_000);
|
||||
// No fresh samples for >5s → trailing window empties → back to 0.
|
||||
vi.advanceTimersByTime(6_000);
|
||||
expect(getCoverCachedPerMinute()).toBe(0);
|
||||
});
|
||||
|
||||
@@ -48,4 +53,31 @@ describe('coverPerfStore', () => {
|
||||
expect(getCoverCachedPerMinute()).toBe(0);
|
||||
expect(getCoverPerfState().done).toBe(5);
|
||||
});
|
||||
|
||||
it('derives UI covers-per-minute from backend total deltas over the trailing window', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(4_000_000);
|
||||
expect(getCoverUiPerMinute()).toBe(0);
|
||||
recordCoverUiTotal(100);
|
||||
// A single sample has no delta yet.
|
||||
expect(getCoverUiPerMinute()).toBe(0);
|
||||
vi.advanceTimersByTime(2_000);
|
||||
recordCoverUiTotal(130);
|
||||
// +30 produced over the last 2s ≈ 900 cpm; lib series stays untouched.
|
||||
expect(getCoverUiPerMinute()).toBeCloseTo(900, 0);
|
||||
expect(getCoverCachedPerMinute()).toBe(0);
|
||||
// Idle poll keeps reporting the same total → delta 0 → rate decays to 0.
|
||||
vi.advanceTimersByTime(6_000);
|
||||
recordCoverUiTotal(130);
|
||||
expect(getCoverUiPerMinute()).toBe(0);
|
||||
});
|
||||
|
||||
it('resets the UI window on a backwards jump (process restart)', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(5_000_000);
|
||||
recordCoverUiTotal(500);
|
||||
vi.advanceTimersByTime(5_000);
|
||||
recordCoverUiTotal(3);
|
||||
expect(getCoverUiPerMinute()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,25 +3,45 @@ 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.
|
||||
* Two independent throughput series share the one-minute rolling window:
|
||||
* - **lib**: the native backfill worker emits cumulative `done` (covers
|
||||
* cached) on `cover:library-progress`; we sample it and derive the delta
|
||||
* rate, mirroring analysis tpm.
|
||||
* - **ui**: on-demand cover ensures (grid/now-playing) are counted natively —
|
||||
* the backend exposes a cumulative `uiEnsuredTotal` in the pipeline stats;
|
||||
* we sample it and derive the delta rate, exactly like lib. Sourcing the
|
||||
* count in Rust avoids the webview ensure-queue dedup/HMR pitfalls that made
|
||||
* a JS-side counter unreliable.
|
||||
*/
|
||||
export type CoverProgressSample = {
|
||||
at: number;
|
||||
done: number;
|
||||
};
|
||||
|
||||
type CoverTotalSample = {
|
||||
at: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
type CoverPerfState = {
|
||||
samples: CoverProgressSample[];
|
||||
done: number;
|
||||
total: number;
|
||||
pending: number;
|
||||
/** Cumulative on-demand (UI) ensure totals sampled from the backend (rolling window). */
|
||||
uiSamples: CoverTotalSample[];
|
||||
};
|
||||
|
||||
/** Sample-retention window (kept generous so backwards-jump detection is robust). */
|
||||
const WINDOW_MS = 60_000;
|
||||
/**
|
||||
* Rate is measured over the trailing few seconds only — a full-minute average
|
||||
* has too much inertia and flattens real bursts/stalls. We still extrapolate to
|
||||
* a per-minute figure for display.
|
||||
*/
|
||||
const RATE_WINDOW_MS = 5_000;
|
||||
|
||||
let state: CoverPerfState = { samples: [], done: 0, total: 0, pending: 0 };
|
||||
let state: CoverPerfState = { samples: [], done: 0, total: 0, pending: 0, uiSamples: [] };
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function emit(): void {
|
||||
@@ -33,6 +53,11 @@ function pruneSamples(now: number, samples: readonly CoverProgressSample[]): Cov
|
||||
return samples.filter(s => s.at >= cutoff);
|
||||
}
|
||||
|
||||
function pruneTotals(now: number, samples: readonly CoverTotalSample[]): CoverTotalSample[] {
|
||||
const cutoff = now - WINDOW_MS;
|
||||
return samples.filter(s => s.at >= cutoff);
|
||||
}
|
||||
|
||||
export function recordCoverProgress(payload: {
|
||||
done: number;
|
||||
total?: number;
|
||||
@@ -48,6 +73,7 @@ export function recordCoverProgress(payload: {
|
||||
}
|
||||
samples = [...samples, { at: now, done }];
|
||||
state = {
|
||||
...state,
|
||||
samples,
|
||||
done,
|
||||
total: payload.total ?? state.total,
|
||||
@@ -56,16 +82,49 @@ export function recordCoverProgress(payload: {
|
||||
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);
|
||||
/** Sample the backend's cumulative on-demand (UI) ensure total. */
|
||||
export function recordCoverUiTotal(total: number): void {
|
||||
const now = Date.now();
|
||||
const next = Math.max(0, Math.floor(total));
|
||||
let uiSamples = pruneTotals(now, state.uiSamples);
|
||||
// A backwards jump means the process restarted — drop the stale baseline.
|
||||
if (uiSamples.length > 0 && next < uiSamples[uiSamples.length - 1].total) {
|
||||
uiSamples = [];
|
||||
}
|
||||
uiSamples = [...uiSamples, { at: now, total: next }];
|
||||
state = { ...state, uiSamples };
|
||||
emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-minute rate from a cumulative-counter series, measured over the trailing
|
||||
* `RATE_WINDOW_MS`. Returns 0 when fewer than two recent samples are available
|
||||
* (so a stalled pipeline drops to 0 within the window instead of coasting).
|
||||
*/
|
||||
function recentRatePerMinute<T extends { at: number }>(
|
||||
now: number,
|
||||
samples: readonly T[],
|
||||
valueOf: (sample: T) => number,
|
||||
): number {
|
||||
const cutoff = now - RATE_WINDOW_MS;
|
||||
const recent = samples.filter(s => s.at >= cutoff);
|
||||
if (recent.length < 2) return 0;
|
||||
const first = recent[0];
|
||||
const last = recent[recent.length - 1];
|
||||
const delta = Math.max(0, valueOf(last) - valueOf(first));
|
||||
if (delta === 0) return 0;
|
||||
const spanMs = Math.max(1, Math.min(WINDOW_MS, now - first.at));
|
||||
return (delta / spanMs) * WINDOW_MS;
|
||||
const spanMs = Math.max(1, last.at - first.at);
|
||||
return (delta / spanMs) * 60_000;
|
||||
}
|
||||
|
||||
/** Covers cached per minute, averaged over the trailing few seconds (0 when idle). */
|
||||
export function getCoverCachedPerMinute(now = Date.now()): number {
|
||||
return recentRatePerMinute(now, state.samples, s => s.done);
|
||||
}
|
||||
|
||||
/** On-demand UI covers produced per minute, averaged over the trailing few seconds. */
|
||||
export function getCoverUiPerMinute(now = Date.now()): number {
|
||||
return recentRatePerMinute(now, state.uiSamples, s => s.total);
|
||||
}
|
||||
|
||||
export function getCoverPerfState(): CoverPerfState {
|
||||
@@ -83,6 +142,6 @@ export function useCoverPerfState(): CoverPerfState {
|
||||
|
||||
/** Test-only reset. */
|
||||
export function resetCoverPerfStateForTest(): void {
|
||||
state = { samples: [], done: 0, total: 0, pending: 0 };
|
||||
state = { samples: [], done: 0, total: 0, pending: 0, uiSamples: [] };
|
||||
emit();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('formatCoverPipelineQueueOverlay', () => {
|
||||
libraryBackfillHttpMax: 2,
|
||||
libraryBackfillHttpActive: 1,
|
||||
libraryBackfillPassRunning: true,
|
||||
uiEnsuredTotal: 0,
|
||||
},
|
||||
ensure: {
|
||||
queuedHigh: 2,
|
||||
@@ -46,6 +47,7 @@ describe('formatCoverPipelineQueueOverlay', () => {
|
||||
libraryBackfillHttpMax: 2,
|
||||
libraryBackfillHttpActive: 0,
|
||||
libraryBackfillPassRunning: false,
|
||||
uiEnsuredTotal: 0,
|
||||
},
|
||||
ensure: {
|
||||
queuedHigh: 0,
|
||||
|
||||
@@ -92,7 +92,14 @@ export function buildLiveOverlayItems(
|
||||
} else if (pin === 'cover:cpm' && live.cover) {
|
||||
items.push({
|
||||
id: pin,
|
||||
line: `cover ${live.cover.cachedPerMinute.toFixed(1)} cpm`,
|
||||
line: `cover lib ${live.cover.cachedPerMinute.toFixed(1)} cpm`,
|
||||
kind: 'cover',
|
||||
sparkline: false,
|
||||
});
|
||||
} else if (pin === 'cover:cpm:ui' && live.cover) {
|
||||
items.push({
|
||||
id: pin,
|
||||
line: `cover ui ${live.cover.uiPerMinute.toFixed(1)} cpm`,
|
||||
kind: 'cover',
|
||||
sparkline: false,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useSyncExternalStore } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { clearPerfLiveHistory, syncPerfLiveHistoryFromPoll } from './perfLiveHistory';
|
||||
import { getAnalysisTracksPerMinute } from './analysisPerfStore';
|
||||
import { getCoverCachedPerMinute, getCoverPerfState } from './coverPerfStore';
|
||||
import { getCoverCachedPerMinute, getCoverUiPerMinute, getCoverPerfState } from './coverPerfStore';
|
||||
import { perfLiveCpuSnapshotSupported } from './perfLiveCpuSnapshot';
|
||||
import { getPerfLiveOverlayPins } from './perfOverlayPins';
|
||||
import {
|
||||
@@ -46,6 +46,7 @@ export type PerfAnalysisDiag = {
|
||||
|
||||
export type PerfCoverDiag = {
|
||||
cachedPerMinute: number;
|
||||
uiPerMinute: number;
|
||||
done: number;
|
||||
total: number;
|
||||
pending: number;
|
||||
@@ -132,6 +133,7 @@ function analysisEqual(a: PerfAnalysisDiag | null, b: PerfAnalysisDiag | null):
|
||||
function coverEqual(a: PerfCoverDiag | null, b: PerfCoverDiag | null): boolean {
|
||||
if (a == null || b == null) return a === b;
|
||||
return a.cachedPerMinute === b.cachedPerMinute
|
||||
&& a.uiPerMinute === b.uiPerMinute
|
||||
&& a.done === b.done
|
||||
&& a.total === b.total
|
||||
&& a.pending === b.pending;
|
||||
@@ -184,6 +186,7 @@ function buildCoverDiag(): PerfCoverDiag {
|
||||
const cover = getCoverPerfState();
|
||||
return {
|
||||
cachedPerMinute: getCoverCachedPerMinute(),
|
||||
uiPerMinute: getCoverUiPerMinute(),
|
||||
done: cover.done,
|
||||
total: cover.total,
|
||||
pending: cover.pending,
|
||||
|
||||
@@ -17,7 +17,8 @@ export type PerfLiveOverlayPinId =
|
||||
| 'rate:home'
|
||||
| 'analysis:tpm'
|
||||
| 'analysis:last'
|
||||
| 'cover:cpm';
|
||||
| 'cover:cpm'
|
||||
| 'cover:cpm:ui';
|
||||
|
||||
const PIPELINE_PIN_TO_FLAG = {
|
||||
'pipeline:fps': 'showFpsOverlay',
|
||||
|
||||
Reference in New Issue
Block a user