Skip to content

perf(scan): skip full library re-stat when the tree is unchanged#979

Merged
byrongamatos merged 2 commits into
mainfrom
perf/scan-dir-signature
Jul 15, 2026
Merged

perf(scan): skip full library re-stat when the tree is unchanged#979
byrongamatos merged 2 commits into
mainfrom
perf/scan-dir-signature

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Every startup scan globbed the whole DLC tree twice (*.feedpak, *.wem) and stat()'d every file to detect changes — ~100k filesystem round trips on a 50k-song library. On a slow NTFS-3G FUSE mount this is the "big drive churns on every launch" report.

Fix

Adds/removes/renames of songs all bump the mtime of the containing directory (verified on the target mount). So after a full pass we persist {reldir: mtime_ns} for every library dir to scan_dir_signature.json (keyed by DLC path so switching libraries never matches a stale signature). The next scan re-stats only those dirs — a handful vs 100k ops — and skips the entire listing/stat pass when none changed.

  • Signature recorded at both full-pass completion points (nothing-new and scan-complete).
  • Seeding runs before the fast-path check and is idempotent, so it doesn't perturb mtimes on a settled library.

Blind spot + escape hatch

A pack rewritten in place under the same name bumps the file mtime but not its dir's — the fast path can't see it. Rare for a song library (you add/remove packs, you don't rewrite them under the same name). The manual Refresh covers it: /api/rescan and /api/rescan/full now pass force=True, which skips the fast path and always does the full pass. force threads through kick_scan_scan_runner and coalesces like the existing rescan-pending flag.

Tests

test_dir_signature_fast_path_skips_unchanged_tree: full pass records signature → unchanged tree takes fast path (zero extraction) → new file bumps dir mtime → full pass picks it up → force=True bypasses the fast path. Scan + career + settings suites green (169 passing).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a directory-content signature to speed up library startup scans when nothing has changed.
    • New or modified library files still trigger full scanning when required.
    • Manual and full rescan actions now force a fresh scan, bypassing the fast path.
  • Bug Fixes
    • Improved handling of updates made “in place” by ensuring rescans refresh correctly even when directory timestamps don’t change.
  • Tests
    • Added coverage for the directory-signature fast path, including forced bypass and directory-form song tracking.

Startup scans globbed the whole DLC tree twice (*.feedpak, *.wem) and
stat()'d every file to detect changes — ~100k filesystem round trips on
a 50k-song library, and painful on a slow NTFS-3G FUSE mount (the "big
drive churns on every launch" report).

Adds/removes/renames of songs all bump the mtime of the containing
directory (verified on the target mount), so after a full pass we persist
{reldir: mtime_ns} for every library dir (scan_dir_signature.json, keyed
by DLC path). The next scan re-stats only those dirs — a handful vs 100k
ops — and skips the entire listing/stat pass when none changed.

Blind spot: a pack rewritten in place under the same name bumps the file
mtime but not its dir's. Rare for a song library, and the manual Refresh
(/api/rescan + /api/rescan/full) now passes force=True to always do the
full pass. force threads through kick_scan -> _scan_runner and coalesces
like the rescan-pending flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad38ff25-25b3-4520-ad98-50058410654a

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:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Scan optimization and forced refresh

Layer / File(s) Summary
Directory signature fast path
lib/scan.py, tests/test_feedpak_extension.py
Directory mtimes are persisted and checked to skip unchanged scans; tests cover unchanged trees, added songs, directory-form songs, and forced scans.
Forced scan scheduling and API wiring
lib/scan.py, server.py
Forced intent is carried through queued scan passes, and both rescan endpoints request forced scans.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RescanEndpoint
  participant kick_scan
  participant _scan_runner
  participant background_scan
  RescanEndpoint->>kick_scan: kick_scan(force=True)
  kick_scan->>_scan_runner: preserve forced intent
  _scan_runner->>background_scan: background_scan(force=True)
  background_scan-->>_scan_runner: complete scan pass
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main performance change.
Description check ✅ Passed The description covers the problem, fix, escape hatch, and tests, so it is mostly complete despite missing the template sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/scan-dir-signature

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@lib/scan.py`:
- Around line 100-114: The _library_dirs function currently records only each
song entry’s parent, so directory-form song roots are omitted from the fast-path
signature. When iterating over all_songs, add the relative directory entry
itself to rels whenever f.is_dir(), while preserving the existing parent and
ancestor tracking.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03d6f07c-62c3-47ca-9d44-6bd15d99b5aa

📥 Commits

Reviewing files that changed from the base of the PR and between 2f2a095 and de7cb48.

📒 Files selected for processing (3)
  • lib/scan.py
  • server.py
  • tests/test_feedpak_extension.py

Comment thread lib/scan.py
CodeRabbit: _library_dirs recorded only each song's parent. For a
directory-form song (loose-song folder or directory sloppak bundle),
adding/removing/replacing a file INSIDE the folder bumps that folder's
own mtime, not its parent's — so the fast path would skip a rescan it
should run. Record the song's own dir when f.is_dir(). File-form
sloppaks (a single .feedpak zip) aren't dirs, so the flat file library
is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos

Copy link
Copy Markdown
Contributor Author

Fixed in 20a5074_library_dirs now records a directory-form song's own dir (via f.is_dir()), so an in-place change inside a loose-song folder / directory bundle invalidates the fast path. File-form sloppaks are unaffected. Added a unit test. @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cd /repo 2>/dev/null || true
git log --oneline -5
git show 20a5074 --stat
git show 20a5074 -- lib/scan.py | head -150

Length of output: 3583


@byrongamatos Verified — commit 20a5074 correctly addresses the concern. _library_dirs now adds the song's own directory (via rel.as_posix()) when f.is_dir() is true, so in-place edits inside a loose-song folder or directory sloppak bundle will bump a tracked mtime and correctly invalidate the fast path, while file-form .feedpak entries (not dirs) remain unaffected as before. The accompanying unit test in tests/test_feedpak_extension.py looks like a solid regression guard for this case. Thanks for the quick turnaround — kicking off a fresh review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@byrongamatos
byrongamatos merged commit 0b4b174 into main Jul 15, 2026
6 checks passed
@byrongamatos
byrongamatos deleted the perf/scan-dir-signature branch July 15, 2026 19:24
@coderabbitai coderabbitai Bot mentioned this pull request Jul 16, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant