fix(analysis): dedupe concurrent seed_from_bytes for the same track_id

Six independent paths reach analysis_cache::seed_from_bytes for the same
track_id during a cache-miss play under LUFS:

- track_download_task legacy stream capture-complete
- ranged_download_task HTTP buffer full
- audio_preload bytes-ready
- psysonic-local file-read complete
- spawn_analysis_seed_from_in_memory_bytes (gapless reuse / preloaded /
  stream cache)
- analysis_enqueue_seed_from_url (frontend backfill triggered by
  refresh:miss while a playback path is mid-seed)

Whenever a track is streamed for the first time with loudness on, two of
these fire in parallel and Symphonia + EBU R128 decode the same buffer
twice — ~30 s of duplicate CPU per track, plus a redundant SQLite write
of an identical row. Reproduced on multiple cache-miss tracks (KjP… ranged
+ backfill, mPdz… preloaded bytes + backfill).

Coordinate at the funnel:

- New SeedFromBytesOutcome::SkippedConcurrent variant.
- SEEDS_IN_FLIGHT static (Mutex<HashSet<String>>) tracks track_ids whose
  analysis is currently running. First caller into seed_from_bytes
  wins the slot via HashSet::insert; later callers return SkippedConcurrent
  immediately and let the winner publish the analysis:waveform-updated
  event.
- SeedInFlightGuard (RAII) releases the slot on every exit path including
  panics, so a crashing analysis cannot leak the marker.
- enqueue_analysis_seed in lib.rs distinguishes the SkippedConcurrent log
  from the genuine "seed result" log so the frontend backfill case
  (when it arrives second) doesn't read like a backfill failure.

Existing call-site match arms in audio.rs already use Ok(_) => {} for
non-Upserted outcomes — no changes needed there. enqueue_analysis_seed in
lib.rs already gates the waveform-updated emit on outcome == Upserted, so
the new variant is silent there too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-26 17:44:32 +02:00
committed by Maxim Isaev
parent 2d222e2691
commit 5cb233e1c9
2 changed files with 70 additions and 7 deletions
+18 -6
View File
@@ -1406,12 +1406,24 @@ fn enqueue_analysis_seed(app: &tauri::AppHandle, track_id: &str, bytes: &[u8]) -
.try_state::<analysis_cache::AnalysisCache>()
.and_then(|cache| cache.get_latest_loudness_for_track(track_id).ok().flatten())
.is_some();
crate::app_deprintln!(
"[analysis] seed result track_id={} bytes={} has_loudness={}",
track_id,
bytes.len(),
has_loudness
);
// SkippedConcurrent gets logged with the actual outcome so the line doesn't
// read "seed result has_loudness=false" when in fact another path is mid-seed
// and will publish the row in seconds.
if outcome == analysis_cache::SeedFromBytesOutcome::SkippedConcurrent {
crate::app_deprintln!(
"[analysis] seed deferred to in-flight peer track_id={} bytes={} has_loudness_now={}",
track_id,
bytes.len(),
has_loudness
);
} else {
crate::app_deprintln!(
"[analysis] seed result track_id={} bytes={} has_loudness={}",
track_id,
bytes.len(),
has_loudness
);
}
Ok(has_loudness)
}