Skip to content

fix(analysis): make library analysis cancellable + cooperative (closes #286)#288

Merged
InstaZDLL merged 1 commit into
mainfrom
fix/analysis-cancellable-cooperative
Jun 20, 2026
Merged

fix(analysis): make library analysis cancellable + cooperative (closes #286)#288
InstaZDLL merged 1 commit into
mainfrom
fix/analysis-cancellable-cooperative

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Draft pour test manuel. Adresse le freeze laptop signalé dans l'issue #286 sur l'auto-analyse post-scan.

Closes #286

Le bug

L'utilisateur a 4002 MP3, lance l'auto-analyse depuis le wizard onboarding, et après ~1500 tracks le laptop freeze + ventilos à fond. Log montre 1445 lignes `using xing header for duration` de Symphonia — confirmation que l'analyzer était en plein full-file decode quand le freeze a touché.

Cause : `run_analyze_library` était une tight loop `spawn_blocking(analyze_file)` + sqlx upsert, sans `yield_now`, sans sleep, sans cancellation. Sur 4000 tracks ça épingle 1 core à 100% pendant 30+ minutes — zéro slot idle pour l'UI / Windows Defender / scheduler. L'utilisateur n'avait aucune sortie sauf force-killer l'app.

Le fix

Trois primitives couplées qui adressent les trois sous-problèmes :

1. Cancellation

  • `ANALYSIS_CANCEL: AtomicBool` mirror le pattern `PREFETCH_CANCEL` déjà éprouvé dans `crate::commands::lyrics`. Reset à `false` au début de chaque run pour qu'un stale `true` d'un cancel précédent ne short-circuite pas le run suivant.
  • `ANALYSIS_RUNNING: AtomicBool` swap-on-entry double-start guard. Sans lui, un click sur "Analyze library" alors que le post-scan `maybe_auto_analyze` est encore en vol race sur le même `INSERT ... ON CONFLICT(track_id)`. Le second caller bail avec `cancelled = true`.
  • `RunningGuard: Drop` clear `ANALYSIS_RUNNING` sur tous les exit paths (early return, sqlx Err, panic) — sinon une seule erreur SQL coince la flag `true` pour toute la session.
  • Nouvelle commande `#[tauri::command] cancel_library_analysis() -> bool` flip la cancel flag et retourne si un run était effectivement en vol.

2. Cooperative scheduling

Chaque itération de la loop finit avec :

```rust
tokio::task::yield_now().await;
tokio::time::sleep(ANALYSIS_PER_TRACK_PAUSE).await; // 25 ms
```

  • Le `yield_now` donne aux autres tâches tokio (UI events, sync drain, playback commands) un tour sur le reactor entre deux decodes.
  • Le sleep 25 ms rend la main au scheduler OS pour qu'il puisse interleave Defender / Spotlight / mpd entre deux decodes CPU-bound.
  • Coût : ~100 s d'idle pure sur 4000 tracks = ~2% overhead vs le freeze risk évité.

3. Cancellation gate placement

Le check `ANALYSIS_CANCEL` est au TOP de la loop, AVANT le decode et le spawn_blocking. Un click qui tombe entre deux decodes est honoré sans burner un decode symphonia de ~8 sec supplémentaire.

Frontend

  • `cancelLibraryAnalysis(): Promise` dans `src/lib/tauri/analysis.ts`. `LibraryAnalysisSummary` gagne `cancelled: boolean`.
  • `SettingsView` swap le bouton Analyze pour un bouton Stop (rose) tant que `isAnalyzingLib === true`. Pas de tug-of-war : le click cancel résout vite mais le `isAnalyzingLib` reste `true` jusqu'à ce que le `analyzeLibrary` promise unblock avec le summary cancelled.
  • Nouvelle clé i18n `settings.analyze.cancel` ("Stop analysis" / "Arrêter l'analyse" / etc.) sur les 17 locales (16 + alias legacy `kr`).

Ce qui N'EST PAS dans cette PR

  • Le scan initial lent (3:25 vs 19-42s ailleurs) : c'est un coût structurel — WaveFlow hash BLAKE3 + extract covers + écrit per-track rows, ce que d'autres players ne font pas. Mérite sa propre passe perf.
  • Auto-analyze default ON dans onboarding : l'utilisateur a explicitement opt-in via le toggle wizard. Renommage / défault à OFF / warning "may take 30+ minutes" → follow-up UX.
  • Annulation mid-decode : restructure `analyze_file` pour cancel token, refactor non-trivial. Le délai courant est borné par un decode = ~8s par track au pire.

Tests

  • 84 desktop unit tests passent
  • `cargo check --workspace` clean
  • `bun run typecheck` clean
  • Test manuel : Settings → Library → Analyze → Stop click → run s'arrête au prochain track boundary
  • Test manuel : Settings → Library → Analyze → re-click Analyze pendant le run → second click ignoré (double-start guard)
  • Test manuel : monitor CPU usage pendant un run de 100 tracks → CPU NE pegg PAS à 100% en moyenne (devrait osciller avec les sleep 25ms)
  • Test manuel : laisser un run complet aller jusqu'au bout (50-100 tracks) → summary `cancelled: false` returned, UI clear progress
  • Test manuel : cancel mid-run → toast/UI feedback OK, log INFO "library analysis cancelled by user"

Summary by CodeRabbit

Notes de Version

  • Nouvelles Fonctionnalités

    • Ajout d'une capacité d'annulation pour les analyses de bibliothèque en cours d'exécution via un nouveau bouton « Arrêter » dans les paramètres.
    • Amélioration de la gestion des analyses simultanées et des mécanismes d'interruption coopérative.
  • Localization

    • Support multilingue complet : étiquettes d'annulation ajoutées dans 17 langues.

Issue #286 reports that the post-scan "Auto-analyse tracks" run
freezes a mid-tier Windows laptop after ~1500 of 4002 tracks. The
log confirms the analyzer was mid-run when the freeze hit (1445
`using xing header for duration` lines from Symphonia, each marking
a full-file MP3 decode).

The shape of the bug: `run_analyze_library` was a tight loop of
`spawn_blocking(analyze_file)` + sqlx upsert, with no yield, no
sleep, no cancellation gate. On a 4000-track library that pins at
least one core at 100 % for 30+ minutes, leaves zero idle slices for
the UI / Defender's per-file open scan / Windows scheduler. The user
had no way to stop it short of force-killing the app.

This commit adds three coupled primitives that solve all three sub-
problems (no cancel, no breathing room, no double-start guard):

## Cancellation

- `ANALYSIS_CANCEL: AtomicBool` mirrors the existing
  `PREFETCH_CANCEL` flag in `crate::commands::lyrics`. Reset to
  `false` at the start of every run so a stale `true` from a prior
  cancellation doesn't short-circuit the next call before track 0.
- `ANALYSIS_RUNNING: AtomicBool` swap-on-entry double-start guard.
  Without it, a user click on "Analyze library" while the post-scan
  `maybe_auto_analyze` hook is still in flight races on the same
  `INSERT ... ON CONFLICT(track_id)` against `track_analysis` —
  both runs would redo every Symphonia decode for the same set of
  pending tracks (free 2x CPU saturation). The second caller now
  bails with `cancelled = true`.
- `RunningGuard: Drop` clears `ANALYSIS_RUNNING` on every exit path
  — early return, sqlx Err propagation, future `?` insertions,
  even a decode panic — so the flag can't get stuck and brick
  auto-analyze for the rest of the session.
- New `#[tauri::command] cancel_library_analysis() -> bool` flips
  the cancel flag and returns whether a run was actually in flight
  (so the UI can show a confirmation toast). Idempotent.

## Cooperative scheduling

Every loop iteration now ends with:

  tokio::task::yield_now().await;
  tokio::time::sleep(ANALYSIS_PER_TRACK_PAUSE).await; // 25 ms

The yield gives other tokio tasks (UI events, sync drain, playback
commands) a turn on the reactor between decodes. The 25 ms sleep
hands the OS scheduler back enough time to interleave Defender,
Spotlight, mpd, etc. between two CPU-bound decodes. On a
4000-track library that adds ~100 s of idle to wall-clock — about a
2 % overhead vs the freeze risk it prevents.

## Cancellation gate placement

The cancel check sits at the TOP of the loop, BEFORE the decode and
the spawn_blocking. A user click that lands between two decodes is
honoured without burning one extra ~8-second symphonia run. The
post-loop summary carries `cancelled: bool` so the frontend can
render "Cancelled at X / Y" instead of pretending the run finished.

## Frontend

- `cancelLibraryAnalysis(): Promise<boolean>` in
  `src/lib/tauri/analysis.ts`. `LibraryAnalysisSummary` gains
  `cancelled: boolean`.
- `SettingsView` swaps the Analyze button out for a Stop button
  while `isAnalyzingLib === true`. The Stop button stays clickable
  even though the worker loop only exits at the next track boundary
  — the click resolves instantly but the running flag stays set
  until `analyzeLibrary`'s awaited promise unblocks, so there's no
  tug-of-war between the optimistic UI flip and the actual worker
  state.
- New `settings.analyze.cancel` i18n key ("Stop analysis" /
  "Arrêter l'analyse" / etc.) across the 16 supported locales + the
  legacy `kr` alias. Brand-style consistency with the existing
  `settings.analyze.action` ("Analyze") label.

## What this commit does NOT address

- The slow first scan (3 min 25 s for 4002 tracks vs 19-42 s in
  other players) is a separate structural cost — WaveFlow hashes
  every file with BLAKE3 + extracts cover art + writes per-track
  rows, which other players don't all do. Defer to its own perf
  pass.
- The auto-analyze flag defaulting to ON in onboarding is unchanged
  here — the user explicitly opted in via the wizard toggle in the
  reported case. Defer the onboarding UX rewording to a follow-up.

84 desktop unit tests + workspace `cargo check` clean. `bun run
typecheck` clean.
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 17fcd1e8-9dd8-4c5c-9452-a06e1bf8145f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/analysis-cancellable-cooperative

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) type: fix Bug fix size: l 200-500 lines labels Jun 20, 2026
@InstaZDLL InstaZDLL self-assigned this Jun 20, 2026
@InstaZDLL InstaZDLL marked this pull request as ready for review June 20, 2026 21:05
@InstaZDLL InstaZDLL merged commit 8d9866e into main Jun 20, 2026
14 checks passed
@InstaZDLL InstaZDLL deleted the fix/analysis-cancellable-cooperative branch June 20, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: l 200-500 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: first scan folder - Automatic analysis scan .

1 participant