Skip to content

perf(scan): partial file hash + per-phase timing - #319

Merged
InstaZDLL merged 5 commits into
mainfrom
perf/scanner-instrumentation
Jun 26, 2026
Merged

perf(scan): partial file hash + per-phase timing#319
InstaZDLL merged 5 commits into
mainfrom
perf/scanner-instrumentation

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Résumé

Item E du re-scope v1.6.0 (perf scanner sur grosses libs). Deux commits :

  1. Instrumentation (diagnostic permanent) : découpage par phase sur la ligne scan complete (walk / meta-load / stat / extract+DB / post + temps CPU hash vs tag cumulés sur les threads parallèles).
  2. Hash partiel : le vrai fix.

Diagnostic (données réelles, lib 4000+)

Scan total extract+db hash (cumul 12 threads) tag (lofty)
902 titres 99 s 97.7 s 150 s 1.2 s
623 MP3 50 s 49.5 s 122 s 0.35 s

Le hash BLAKE3 du fichier entier = ~99% du coût, borné par le débit disque (lecture de ~9 Go pour 902 titres). lofty/tags négligeable.

Fix

scanner::hash_file digère taille + 1er Mio + dernier Mio pour les fichiers > 2 Mio, au lieu du fichier entier → lecture ~2 Mio/fichier au lieu de la taille complète (~5×+ moins d'I/O).

Identité préservée :

  • copies déplacées/renommées → mêmes octets → même hash (dedup tient) ;
  • réécriture de tags → octets décalés dans la fenêtre tête/queue (ID3v2 tête, ID3v1/APE queue) → hash change, donc re-extraction correcte ;
  • la taille est foldée dans le digest → pas de collision même-tête/queue-taille-différente.

Migration-free : le fast-path rescan clé sur (mtime, size), pas le hash → aucun re-scan forcé, les hash full existants restent valides jusqu'à modif du fichier.

Blind spot assumé : deux fichiers distincts de même taille + mêmes 1er/dernier Mio mais milieu différent collisionneraient — inexistant sur de la vraie musique (tête + taille diffèrent déjà). Documenté + testé.

Tests

3 tests core : chemin whole-file (petits fichiers), sensibilité tête + taille, blind-spot milieu documenté. cargo test -p waveflow-core hash_file ✅. clippy workspace ✅.

Doc

À mesurer après merge

L'user relance un scan de la même lib → la ligne scan complete (instrumentation gardée) confirmera le gain réel.

Follow-up séparé (pas dans cette PR)

Les logs ont aussi révélé que l'analyse (BPM/loudness) tourne en concurrence du scan et se bat pour le lock SQLite (database is locked, INSERT à 5.6 s). À traiter à part — contribue à la lenteur perçue end-to-end.

Milestone v1.6.0.

Summary by CodeRabbit

  • Bug Fixes
    • La déduplication utilise désormais une empreinte partielle (taille + début + fin) pour présélectionner, puis vérifie le contenu complet avant de proposer des groupes à fusionner/supprimer.
    • Les fichiers dont le calcul de l’empreinte complète échoue ne sont plus pris en compte.
    • Les très gros fichiers bénéficient d’un traitement plus rapide, avec une détection basée sur des fenêtres (modifs uniquement au milieu peuvent être non détectées).
  • Documentation
    • Mise à jour de la documentation “Duplicate detection” : stratégie d’empreinte partielle, zone aveugle potentielle et contrôle par hachage complet.

Instrument scan_folder_inner with wall-clock markers (walk / metadata
load / fast-path stat / extract+DB / post-processing) plus cumulative
BLAKE3-hash vs lofty-tag CPU time summed across the parallel extraction
tasks. Logged once on the existing "scan complete" line so a slow scan
on a large library is diagnosable without a profiler — and so we can
choose the right optimisation (hash-bound vs tag-bound vs DB-bound)
from real data.

Diagnostics only; no behaviour change.
Full-file BLAKE3 hashing was the dominant scan cost — instrumentation
on a 902-track folder showed extract+DB = 97.7 s of which the hash read
~99% (tag/lofty was 1 s). The scan was disk-throughput bound, reading
every byte of every file (~9 GB).

hash_file now digests `size + first 1 MiB + last 1 MiB` for files over
2 MiB, instead of the whole file. Strong identity for music:
- moved/renamed copies keep the same bytes → same hash (dedup holds),
- tag rewrites shift bytes in the head/tail window → hash changes, so
  edited files still re-extract,
- file length is folded in so same-head/tail/different-size can't collide.

Migration-free: the rescan fast path keys on (mtime, size), not the
hash, so existing rows are never force-rehashed; stored full-file
hashes stay valid until the file changes.

Tests cover the small/whole-file path, head + size sensitivity, and the
documented middle-byte blind spot.
@InstaZDLL InstaZDLL added this to the v1.6.0 milestone Jun 26, 2026
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets type: perf Performance improvement labels Jun 26, 2026
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 89ed0e94-dbc4-4275-9c83-d89eda302518

📥 Commits

Reviewing files that changed from the base of the PR and between 7a367c2 and da492da.

📒 Files selected for processing (1)
  • docs/features/library.md

📝 Walkthrough

Walkthrough

file_hash passe à un calcul partiel basé sur la taille et les fenêtres début/fin. La déduplication revalide les candidats avec un hash complet. Le scan ajoute des durées par phase et des totaux CPU séparés au log final.

Changes

Hash partiel, doublons et scan

Layer / File(s) Summary
Hash partiel et validation
src-tauri/crates/core/src/scanner/extract.rs, docs/features/library.md
hash_file utilise la taille et les fenêtres début/fin, et les tests couvrent les cas de petite taille, d’en-tête et de blind spot au milieu.
Vérification des doublons
src-tauri/crates/app/src/commands/duplicates.rs
find_duplicates filtre par taille puis revalide les groupes avec hash_file_full, en excluant les pistes illisibles.
Chronométrage de l’extraction
src-tauri/crates/app/src/commands/scan.rs
extract_file reçoit ScanTimings et scan_folder_inner découpe les durées de scan par phase avant le log final.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#181 : ajustement des réexports et imports autour de scanner, dans la même zone de code.

Poem

Un hash mord la tête et la queue 🐟
puis les doublons passent au tamis.
Le scan bat la mesure, phase après phase,
et le log final aligne ses aiguilles ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre résume bien le fix principal (hachage partiel) et l'instrumentation par phase, avec un format de commit correct.
Description check ✅ Passed La description couvre le résumé, les tests et la doc, avec un niveau de détail suffisant malgré l'absence du checklist et du lien d'issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/scanner-instrumentation

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

@InstaZDLL InstaZDLL added the size: m 50-200 lines label Jun 26, 2026
@InstaZDLL InstaZDLL self-assigned this Jun 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/core/src/scanner/extract.rs`:
- Around line 40-55: The partial hash in extract.rs should not be used as the
sole deduplication key because Files larger than 2 * HASH_CHUNK_BYTES can
collide when only the middle bytes differ. Update the dedup flow that consumes
file_hash so it either computes/retains a full-content hash for grouping or
performs a stronger secondary check before forming the duplicate bucket and
showing the removal UI, and verify the logic around the file_hash generation and
the dedup grouping path that the test covering middle-byte changes exercises.
- Around line 682-700: Le test actuel de hash_file couvre la sensibilité à la
tête et à la taille, mais pas la fenêtre de fin, donc une régression sur le
seek/read_exact du tail pourrait passer inaperçue. Étendez
hash_file_large_detects_head_and_size_changes pour modifier un octet dans la
queue du fichier et vérifier que hash_file change aussi, en gardant les autres
vérifications existantes intactes; utilisez les symboles hash_file et
hash_file_large_detects_head_and_size_changes pour localiser la suite de tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a9a0dd21-1e94-4671-bb0b-f210fee4184f

📥 Commits

Reviewing files that changed from the base of the PR and between c9bfaff and a6eb3fd.

📒 Files selected for processing (3)
  • docs/features/library.md
  • src-tauri/crates/app/src/commands/scan.rs
  • src-tauri/crates/core/src/scanner/extract.rs

Comment thread src-tauri/crates/core/src/scanner/extract.rs
Comment thread src-tauri/crates/core/src/scanner/extract.rs
The scan-time file_hash is now a partial digest (size + head + tail), so
a duplicate group keyed on it is only a candidate — two distinct files
could in theory share it but differ in unread middle bytes. Since the
duplicates UI deletes from a group, find_duplicates now re-verifies each
candidate with a full-content hash (scanner::hash_file_full, off-thread,
only on the few candidate files), splitting any collision into separate
groups and dropping singletons.

Also extend the hash_file test to flip a tail-window byte (guards the
seek-to-end + read_exact of the tail chunk).
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Les deux findings valides, fixés dans 51ee28f :

  1. Dedup sur hash partiel = risque sur une opération destructivefind_duplicates re-vérifie maintenant chaque groupe candidat (groupé par le hash partiel stocké en SQL) avec un hash full-content (scanner::hash_file_full, en spawn_blocking, uniquement sur les quelques fichiers candidats) avant de retourner. Toute collision du milieu est éclatée en groupes séparés et les singletons sont retirés. Le scan reste rapide (hash partiel), le dedup redevient exact (vérif full seulement sur les candidats, rares). Doc module + library.md mises à jour.
  2. Test ne couvrait pas la fenêtre de queuehash_file_large_detects_head_and_size_changes modifie désormais aussi un octet en queue et vérifie que le hash change (garde le seek(End) + read_exact du tail).

clippy workspace ✅ · cargo test -p waveflow-core hash_file ✅.

@InstaZDLL InstaZDLL added size: l 200-500 lines and removed size: m 50-200 lines labels Jun 26, 2026
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Bien vu, merci.

La revalidation full-content dans find_duplicates lève bien le risque lié au hash partiel avant une opération destructive, et l’extension du test à la fenêtre de queue couvre le trou de validation que j’avais signalé.

Je considère donc ces deux points comme réglés. 🐇✅


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/319

Timestamp: 2026-06-26T20:55:51.150Z
Learning: In `src-tauri/crates/core/src/scanner/extract.rs` and related duplicate-detection flow in WaveFlow, scanning uses a partial file hash for performance, but `find_duplicates` now re-validates each candidate group with a full-content hash via `scanner::hash_file_full` in `spawn_blocking` before returning results. This makes duplicate detection exact again despite the middle-byte blind spot of the partial hash.

Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/319

Timestamp: 2026-06-26T20:55:51.150Z
Learning: In WaveFlow's Rust scanner tests, the large-file hash regression test `hash_file_large_detects_head_and_size_changes` was extended to also mutate a byte in the tail window and assert that the hash changes, ensuring the tail-read path is covered.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/app/src/commands/duplicates.rs`:
- Around line 5-7: The duplicates command currently groups candidates only by
track.file_hash, so legacy full hashes and newer partial hashes for identical
files will never meet and will be missed. Update the logic around the duplicate
search and hash_file_full revalidation to support both hash formats by either
migrating/versioning existing hashes or broadening the SQL prefilter with a
stable field like file_size before grouping, and add a compatibility test that
covers legacy full-hash profiles versus newly scanned partial hashes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c294259c-0ba0-40a7-93c8-676cb5aff013

📥 Commits

Reviewing files that changed from the base of the PR and between a6eb3fd and 51ee28f.

📒 Files selected for processing (4)
  • docs/features/library.md
  • src-tauri/crates/app/src/commands/duplicates.rs
  • src-tauri/crates/core/src/scanner/extract.rs
  • src-tauri/crates/core/src/scanner/mod.rs

Comment thread src-tauri/crates/app/src/commands/duplicates.rs Outdated
A legacy full hash and a newer partial hash for the same file never
match (both are 64-char blake3 hex, indistinguishable), so grouping
candidates by file_hash missed duplicates that span the partial-hash
rollout. Prefilter by byte size instead — a format-stable field every
real duplicate shares (size is also folded into the partial hash) —
then let the existing full-content verification form the exact groups.
Simplifies the consuming code to a flat candidate list + one bucketing
pass, with a deterministic output order.

Add a compatibility test: byte-identical files group by content
regardless of any stored hash, and unreadable files are excluded.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Finding valide, fixé dans 7a367c2.

Hash legacy full vs partiel indistinguables → doublons cross-migration manqués : exact, les deux sont des blake3 hex 64 chars, impossible de les distinguer pour filtrer sélectivement. Solution retenue (ta 2ᵉ option) : prefilter par file_size au lieu de file_hash.

  • La taille est un champ stable indépendant du format de hash, et tout vrai doublon la partage (elle est même foldée dans le hash partiel). GROUP BY file_size HAVING COUNT>1 capture donc TOUS les doublons byte-identiques, qu'ils aient été scannés avant (full) ou après (partiel).
  • La vérif full-content déjà en place forme ensuite les groupes exacts (un fichier de même taille mais contenu différent part dans son propre bucket → singleton → retiré).
  • Bonus : ça simplifie le code (liste plate de candidats + un seul bucketing par full-hash) avec un ordre de sortie déterministe.

Test de compat ajouté : des fichiers byte-identiques groupent par contenu quel que soit le hash stocké (DuplicateTrack ne porte d'ailleurs aucun hash — l'identité est dérivée du contenu), + fichiers illisibles exclus.

Note coût : on full-hash les fichiers de même taille. Sur de l'audio les tailles sont quasi-uniques (VBR/FLAC au byte près), donc les groupes de taille ≈ les vrais doublons ; le dedup étant rare + user-initiated, c'est acceptable et désormais correct.

clippy app --all-targets ✅. Doc module + library.md mises à jour.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Bien vu, merci.

Le passage du préfiltre de file_hash vers file_size corrige bien le cas cross-migration full/partiel, et la vérif full-content derrière garde la déduplication exacte. Le test de compat + l’exclusion des fichiers illisibles couvrent aussi les régressions importantes.

Je considère ce point comme réglé. 🐇✅


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/319

Timestamp: 2026-06-26T21:07:00.515Z
Learning: In WaveFlow duplicate detection, legacy full hashes and new partial hashes are both stored as 64-character BLAKE3 hex strings, so they cannot be distinguished by format. To remain correct across migration states, duplicate candidate selection should prefilter by `file_size` rather than `file_hash`, then use full-content hashing to form exact groups.

Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/319

Timestamp: 2026-06-26T21:07:00.515Z
Learning: In WaveFlow's duplicate detection flow, `DuplicateTrack` does not carry a stored hash as identity. Exact duplicate grouping is derived from full file content during verification, and unreadable files are excluded from the returned duplicate groups.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/features/library.md`:
- Line 54: The opening sentence in the library docs is outdated:
`commands/duplicates.rs::find_duplicates` no longer groups directly by
`file_hash` alone. Update the wording so it says the command first prefilters
candidates by `file_size` and then verifies/group results using the full-content
hash from `scanner::hash_file_full`, matching the behavior described later in
the paragraph.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d4ca61f4-50ed-404f-8779-badb13ee97f3

📥 Commits

Reviewing files that changed from the base of the PR and between 51ee28f and 7a367c2.

📒 Files selected for processing (2)
  • docs/features/library.md
  • src-tauri/crates/app/src/commands/duplicates.rs

Comment thread docs/features/library.md Outdated
find_duplicates prefilters by file_size and groups with a full-content
hash, not by the partial scan-time file_hash directly.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Valide, fixé dans da492da — la phrase d'ouverture dit désormais que find_duplicates prefiltre par file_size puis groupe via le hash full-content (scanner::hash_file_full), au lieu de « groups by file_hash ». Cohérent avec le reste du paragraphe.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Bien vu, merci.

La doc est maintenant cohérente avec l’implémentation : préfiltre par file_size, puis regroupement via scanner::hash_file_full. Ce point est donc réglé. ✅

@InstaZDLL
InstaZDLL merged commit 53801bd into main Jun 26, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the perf/scanner-instrumentation branch June 26, 2026 21:55
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: docs Docs, README, assets size: l 200-500 lines type: perf Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant