mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
fix(analysis): native library backfill coordinator for advanced strategy (#881)
* feat(analysis): native library backfill coordinator for advanced strategy Move advanced analytics scheduling from the webview loop into a Rust background worker (configure + spawn_blocking batch/enqueue), matching cover backfill. Scan hash+BPM gap tracks instead of the full library; suppress low-priority analysis UI events; limit loudness refresh IPC to the playback window. * fix(analysis): flat backfill configure IPC and restore probe track-perf Use flattened Tauri args like cover backfill so release/prod invoke works; keep emitting analysis:track-perf for library low-priority work so Performance Probe tpm/last-track stats update while waveform/enrichment UI events stay suppressed. * chore(analysis): fix clippy and drop duplicate TS backfill policy Allow too_many_arguments on library_analysis_backfill_configure for CI; remove unused frontend batch API and TS watermark helpers now owned in Rust; clarify analysis_emits_ui_events comment for low-priority track-perf. * docs: CHANGELOG and credits for PR #881 native analysis backfill
This commit is contained in:
@@ -1,64 +1,38 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { buildStreamUrlForServer } from '../api/subsonicStreamUrl';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
analysisEnqueueSeedFromUrl,
|
||||
analysisGetPipelineQueueStats,
|
||||
analysisSetPipelineParallelism,
|
||||
libraryAnalysisBackfillBatch,
|
||||
libraryCountLiveTracks,
|
||||
libraryAnalysisProgress,
|
||||
libraryAnalysisBackfillConfigure,
|
||||
} from '../api/analysis';
|
||||
import { librarySqlServerId } from '../api/coverCache';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useAnalysisStrategyStore } from '../store/analysisStrategyStore';
|
||||
import { DEFAULT_ADVANCED_PARALLELISM } from '../utils/library/analysisStrategy';
|
||||
import {
|
||||
libraryBackfillNeedsTopUp,
|
||||
libraryBackfillTopUpLimit,
|
||||
} from '../utils/library/libraryAnalysisBackfillPolicy';
|
||||
import { libraryIsReady } from '../utils/library/libraryReady';
|
||||
import { serverIndexKeyForProfile } from '../utils/server/serverIndexKey';
|
||||
|
||||
const TOP_UP_POLL_MS = 500;
|
||||
const STEADY_POLL_MS = 2000;
|
||||
const READY_POLL_MS = 5000;
|
||||
const EXHAUSTED_PAUSE_MS = 15_000;
|
||||
const COMPLETED_RECHECK_MS = 5 * 60_000;
|
||||
/** Consecutive exhausted scans with zero enqueue before treating the library as done. */
|
||||
const EXHAUSTED_DONE_STREAK = 2;
|
||||
|
||||
const EMPTY_PIPELINE_STATS = {
|
||||
pipelineWorkers: 1,
|
||||
httpQueued: 0,
|
||||
httpQueuedHigh: 0,
|
||||
httpQueuedMiddle: 0,
|
||||
httpQueuedLow: 0,
|
||||
httpDownloadActive: 0,
|
||||
httpDownloadActiveHigh: 0,
|
||||
httpDownloadActiveMiddle: 0,
|
||||
httpDownloadActiveLow: 0,
|
||||
cpuQueued: 0,
|
||||
cpuQueuedHigh: 0,
|
||||
cpuQueuedMiddle: 0,
|
||||
cpuQueuedLow: 0,
|
||||
cpuDecodeActive: 0,
|
||||
cpuDecodeActiveHigh: 0,
|
||||
cpuDecodeActiveMiddle: 0,
|
||||
cpuDecodeActiveLow: 0,
|
||||
};
|
||||
const DISABLED_CONFIGURE = {
|
||||
enabled: false,
|
||||
serverIndexKey: '',
|
||||
libraryServerId: '',
|
||||
serverUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
workers: DEFAULT_ADVANCED_PARALLELISM,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Advanced analytics strategy: keep the HTTP backfill backlog above worker count
|
||||
* (watermark target ≈ workers × 3) so parallel downloads stay busy. Library
|
||||
* tracks enqueue at low priority; playback tiers stay ahead in Rust.
|
||||
* Advanced analytics strategy: native coordinator in Rust (see `library_analysis_backfill`).
|
||||
* Webview only passes session + worker count — no planning loop or batch IPC.
|
||||
*/
|
||||
export function useLibraryAnalysisBackfill(enabled = true): void {
|
||||
const activeServerId = useAuthStore(s => s.activeServerId);
|
||||
const server = useAuthStore(s =>
|
||||
s.activeServerId ? s.servers.find(srv => srv.id === s.activeServerId) : undefined,
|
||||
);
|
||||
const getBaseUrl = useAuthStore(s => s.getBaseUrl);
|
||||
const strategy = useAnalysisStrategyStore(s => s.getStrategyForServer(activeServerId));
|
||||
const advancedParallelism = useAnalysisStrategyStore(
|
||||
s => s.getAdvancedParallelismForServer(activeServerId),
|
||||
);
|
||||
const cursorRef = useRef<string | null>(null);
|
||||
const completedTotalRef = useRef<number | null>(null);
|
||||
const exhaustedIdleStreakRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
@@ -68,110 +42,38 @@ export function useLibraryAnalysisBackfill(enabled = true): void {
|
||||
}, [strategy, advancedParallelism, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || strategy !== 'advanced' || !activeServerId) return;
|
||||
|
||||
let cancelled = false;
|
||||
const serverId = activeServerId;
|
||||
|
||||
void (async () => {
|
||||
while (!cancelled) {
|
||||
if (completedTotalRef.current !== null) {
|
||||
const totalTracks = await libraryCountLiveTracks(serverId).catch(() => null);
|
||||
if (!Number.isFinite(totalTracks)) {
|
||||
await new Promise(r => setTimeout(r, COMPLETED_RECHECK_MS));
|
||||
continue;
|
||||
}
|
||||
if (totalTracks === completedTotalRef.current) {
|
||||
await new Promise(r => setTimeout(r, COMPLETED_RECHECK_MS));
|
||||
continue;
|
||||
}
|
||||
completedTotalRef.current = null;
|
||||
cursorRef.current = null;
|
||||
exhaustedIdleStreakRef.current = 0;
|
||||
}
|
||||
|
||||
if (!(await libraryIsReady(serverId))) {
|
||||
await new Promise(r => setTimeout(r, READY_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
const workers = advancedParallelism;
|
||||
const stats = await analysisGetPipelineQueueStats().catch(
|
||||
() => EMPTY_PIPELINE_STATS,
|
||||
);
|
||||
|
||||
if (!libraryBackfillNeedsTopUp(stats, workers)) {
|
||||
await new Promise(r => setTimeout(r, STEADY_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
const fetchLimit = libraryBackfillTopUpLimit(stats, workers);
|
||||
if (fetchLimit <= 0) {
|
||||
await new Promise(r => setTimeout(r, TOP_UP_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
const batch = await libraryAnalysisBackfillBatch(
|
||||
serverId,
|
||||
cursorRef.current,
|
||||
fetchLimit,
|
||||
).catch(() => null);
|
||||
|
||||
if (!batch || cancelled) {
|
||||
await new Promise(r => setTimeout(r, TOP_UP_POLL_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
cursorRef.current = batch.nextCursor;
|
||||
|
||||
await Promise.all(
|
||||
batch.trackIds.map(trackId =>
|
||||
analysisEnqueueSeedFromUrl(
|
||||
trackId,
|
||||
buildStreamUrlForServer(serverId, trackId),
|
||||
serverId,
|
||||
'low',
|
||||
).catch(() => {
|
||||
/* Rust skips completed tracks */
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (batch.trackIds.length > 0) {
|
||||
exhaustedIdleStreakRef.current = 0;
|
||||
}
|
||||
|
||||
if (batch.exhausted) {
|
||||
cursorRef.current = null;
|
||||
const progress = await libraryAnalysisProgress(serverId).catch(() => null);
|
||||
const pending = progress?.pendingTracks ?? -1;
|
||||
if (pending <= 0 && batch.trackIds.length === 0) {
|
||||
exhaustedIdleStreakRef.current += 1;
|
||||
if (exhaustedIdleStreakRef.current >= EXHAUSTED_DONE_STREAK && progress) {
|
||||
completedTotalRef.current = progress.totalTracks;
|
||||
await new Promise(r => setTimeout(r, COMPLETED_RECHECK_MS));
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
exhaustedIdleStreakRef.current = 0;
|
||||
}
|
||||
if (pending > 0) {
|
||||
continue;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, EXHAUSTED_PAUSE_MS));
|
||||
} else if (batch.trackIds.length === 0) {
|
||||
await new Promise(r => setTimeout(r, TOP_UP_POLL_MS));
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cursorRef.current = null;
|
||||
completedTotalRef.current = null;
|
||||
exhaustedIdleStreakRef.current = 0;
|
||||
const disable = () => {
|
||||
void libraryAnalysisBackfillConfigure(DISABLED_CONFIGURE);
|
||||
};
|
||||
}, [strategy, activeServerId, advancedParallelism, enabled]);
|
||||
|
||||
if (!enabled || strategy !== 'advanced' || !activeServerId || !server) {
|
||||
disable();
|
||||
return disable;
|
||||
}
|
||||
|
||||
const indexKey = serverIndexKeyForProfile(server);
|
||||
const baseUrl = getBaseUrl();
|
||||
void libraryAnalysisBackfillConfigure({
|
||||
enabled: true,
|
||||
serverIndexKey: indexKey,
|
||||
libraryServerId: librarySqlServerId(activeServerId),
|
||||
serverUrl: baseUrl,
|
||||
username: server.username,
|
||||
password: server.password,
|
||||
workers: advancedParallelism,
|
||||
}).catch(() => {
|
||||
/* coordinator optional; avoid unhandled rejection noise in release */
|
||||
});
|
||||
|
||||
return disable;
|
||||
}, [
|
||||
enabled,
|
||||
strategy,
|
||||
activeServerId,
|
||||
server?.url,
|
||||
server?.username,
|
||||
server?.password,
|
||||
advancedParallelism,
|
||||
getBaseUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user