Skip to content

Stop stat-ing every entry when building the gallery - #48

Merged
nnmarcoo merged 2 commits into
mainfrom
faster-gallery-scan
Aug 21, 2026
Merged

Stop stat-ing every entry when building the gallery#48
nnmarcoo merged 2 commits into
mainfrom
faster-gallery-scan

Conversation

@nnmarcoo

Copy link
Copy Markdown
Owner

Gallery::new called Path::is_file() on every directory entry, which is a stat syscall. DirEntry::file_type() answers the same question from the d_type field that readdir already returned, with no syscall.

Measured on two 50,000 file folders. 86.6 ms to 18.0 ms for 5,000 images among 45,000 other files. 104.3 ms to 22.3 ms for a folder of 50,000 images.

is_file() follows symlinks and file_type() does not, so a plain swap drops symlinked images from the gallery. The is_symlink() branch falls back to the stat for those entries only.

gallery.rs had no tests. Three were added, covering the filter rules, symlinked images, and dangling symlinks. Removing the symlink fallback fails two of them.

Gallery::new called Path::is_file() on every directory entry, which is a
stat syscall, and it ran before the extension test because && evaluates
left to right. Opening any file therefore stat'd the whole folder, images
and unrelated files alike, on the path between the click and the first
pixel.

DirEntry::file_type() answers the same question from the d_type field that
readdir already returned, with no syscall at all on Linux. That single
swap is where the time goes. Measured on two 50,000 file directories:
86.6 ms to 18.0 ms for a folder holding 5,000 images among 45,000 other
files, and 104.3 ms to 22.3 ms for one holding 50,000 images. The second
case is the proof it is the syscall and not the filter, since the
extension test rejects nothing there and it still gets 4.7x faster.

The extension test now runs first anyway, so a rejected name never builds
a PathBuf. Only survivors allocate.

is_file() follows symlinks and file_type() does not, so the naive swap
silently drops symlinked images. That is the whole reason for the
is_symlink() branch: it falls back to the stat for those entries only,
which keeps the old semantics at a cost nothing outside a folder of
symlinks will ever notice. On filesystems that report DT_UNKNOWN,
file_type() falls back to an lstat internally, so the worst case is what
this code already did.

gallery.rs had no tests. It now pins the filtering rules, including that
extensions match case-insensitively, that a directory named like an image
is excluded, that a dangling symlink is excluded, and that opening a
symlinked image lands the gallery on that symlink rather than falling back
to index 0 and reporting the wrong position.

Verified discriminating: removing the is_symlink() fallback fails both
symlink tests, listing ["real.jpg"] where ["linked.jpg", "real.jpg"] is
expected, while the plain filtering test stays green because it does not
depend on that branch.

Two things measured and rejected. A HashSet lookup keyed on the extension
lost to the linear scan in the all-images case, 17.3 ms against 16.2 ms,
because jpg is the first entry in SUPPORTED. Sorting SUPPORTED to allow a
binary search is not available either, since tasks.rs hands that same slice
to the file dialog as its filter list and the order is user visible. The
sort_unstable over 50,000 paths costs 3.6 ms and is not worth touching.
cargo fmt --check runs ahead of clippy in CI and rejected two hand-written
lines in the new test module. No behaviour change.
@nnmarcoo
nnmarcoo merged commit a419b62 into main Aug 21, 2026
1 check passed
@nnmarcoo
nnmarcoo deleted the faster-gallery-scan branch August 21, 2026 02:40
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