Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

dirperf

Find out which layer is making a directory slow.

A directory with tens of thousands of files can take minutes to open in a file manager, and the usual diagnostic response is to time ls and conclude that ls is slow. That does not identify anything. dirperf measures the same directory at every layer between the disk and the desktop, one syscall apart at a time, so the gap between two adjacent numbers attributes the cost to something specific: the filesystem, the kernel's directory read, per-file metadata lookup, the open path, the data read, userland sorting, or the file manager's own configuration.

It was written for FreeBSD and GhostBSD on ZFS.

dirperf only reads. It never writes to the directory under test.

Installation

Clone the repository, make the binary executable, and install it into your system's executable search path:

git clone https://github.com/xcrsz/dirperf
cd dirperf
chmod +x dirperf
sudo mv dirperf /usr/local/bin/

Verify the installation:

dirperf -h

Usage

dirperf [options] <directory>
dirperf compare <a.json> <b.json>
option meaning
-r, --runs N warm iterations per probe after the cold one (default 5)
-f, --format FMT text, md, json or html (default text)
-o, --out FILE write the report to a file instead of stdout
-j, --threads N threads for the parallel metadata probe (default: CPU count)
--max-files N cap how many entries the per-file probes touch
--read-bytes N bytes read from the head of each file (default 4096)
--cold one cold pass only, no warm iterations
--no-open skip the open and read probes
--no-parallel skip the parallel metadata probe
--no-external skip ls, find and du
--no-desktop skip reading desktop configuration
--no-latency skip per-call latency instrumentation
--gui CMD time a GUI application against the directory, repeatable
--gui-timeout N seconds to wait for a GUI application to settle (default 300)
-q, --quiet no progress output

The design, and why it is shaped this way

The cold pass is reported separately and never averaged in

The complaint that motivates this tool is that opening a directory you have not visited takes minutes. That is the cold case. A benchmark that warms three times and then reports a mean over ten runs is measuring a state the file manager never sees, and it will report that everything is fine.

So the first iteration of every probe is recorded on its own, and the warm distribution is kept apart from it. The ratio between them is itself a reported finding: a high ratio means caching works and the problem is the first visit; a ratio near 1 means the cache is not helping at all, which is a different problem with a different fix.

The census of the directory (file counts, extension histogram, size distribution) is built entirely from data the probes already had to collect. A separate census pass would warm the caches that the cold measurement exists to observe.

dirperf cannot flush the ARC or the page cache, so its cold figures are cold only relative to whatever was resident when it started. For a genuinely cold measurement, export and re-import the pool, or reboot, and then run with --cold. The report says this rather than leaving you to assume otherwise.

The probe ladder

Probes run in ascending order of cache damage. Enumeration only touches the directory's own blocks, so it runs first; reads run last.

probe layer isolates
enumerate_cold kernel the first pass, names retained, also the census source
enumerate_raw kernel getdents(2) into our own buffer, nothing else
enumerate_libc libc opendir/readdir, adding libc's buffering
enumerate_std std std.Io.Dir.iterate(), adding Zig's layer
enumerate_retain userland the same read plus copying every name into memory
sort_names userland sorting, with no filesystem access at all
stat_follow kernel per-file metadata lookup
stat_nofollow kernel the same without symlink resolution
open_close kernel the open path: access control, extended attributes
read_head kernel the first data block, which is what a thumbnailer reads
stat_parallel kernel the same metadata pass across N threads

The last one is the most useful and the least obvious. If spreading the work across threads speeds it up roughly in proportion, the cost is waiting, and an application that issued its lookups concurrently would be much faster. If it does not speed up at all, something in the path is serialised and concurrency will not rescue anything. Those two situations look identical from the outside and have completely different fixes.

ls, ls -f, ls -l, find and du are also timed, but as controls rather than as measurements. The one genuinely diagnostic pair is ls against ls -f: the difference between them is sorting and column layout, entirely in userland, so a large gap there means the answer is not in the filesystem.

Per-call latency, and the clock

For the metadata and open probes, dirperf records a timestamp either side of every individual syscall and reports the distribution: median, p90, p99, p99.9, worst, and the p99-to-median ratio.

This matters because a mean cannot tell the difference between a uniformly slow path and a fast path with a long tail, and those have different causes. A long tail usually means most requests are served from cache and a few go to the device.

The instrumentation costs two clock reads per call. Rather than fold that into the headline number, the instrumented pass runs once more after the warm set and its own wall time is discarded. The cost of a single monotonic clock read is measured at startup and printed in the report, and it is not subtracted from the per-call figures, because subtracting an estimate invents precision the estimate does not have. Read the figure, and treat any per-call median near the clock cost as being at the floor of what this method can measure.

ZFS

Dataset properties are configuration: they explain why a workload is slow and they are what you can change. ARC counters are sampled either side of every probe, which turns "the filesystem is slow" into "N metadata requests during that pass missed the ARC". A low metadata hit rate on the warm passes means data read moments earlier is being fetched again, which is a very specific finding with very specific remedies.

The properties read are chosen because each has a defensible causal story for large-directory latency, not because they were available:

  • atime and relatime. With atime=on and relatime=off, every file read writes back an access time, so browsing 25,000 images generates 25,000 metadata writes.
  • xattr. With xattr=dir every extended attribute lookup becomes a hidden directory lookup, and desktop file managers ask for extended attributes constantly.
  • primarycache and secondarycache, which decide whether metadata is cacheable at all.
  • dnodesize, recordsize, redundant_metadata, special_small_blocks, logbias, sync, compression, checksum.

Outside ZFS, dirperf reads vfs.numvnodes against kern.maxvnodes. A vnode cache close to its limit makes the kernel recycle vnodes to make room, so entries that were just looked up have to be reconstructed. It is a common and easily missed cause of exactly this symptom, and kern.maxvnodes is sized from memory at boot and is frequently too small for a directory of this shape.

ZFS renames its sysctls between releases, so every lookup tries a list of candidate names. A rename shows up as a longer candidate list rather than as a silently absent number.

The desktop layer

For a directory that is mostly images, the setting that decides whether a file manager takes two seconds or ten minutes is often not a filesystem property at all. dirperf reads the MATE and GNOME keys that change how much work is done per file (show-image-thumbnails, thumbnail-limit, show-directory-item-counts, preview-sound, default-folder-viewer, the thumbnail cache limits) and prints them next to the timings, each with a line saying why it is on the list.

If thumbnailing is on and most of the directory is images, the report projects the measured read_head cost across the whole directory and says what the floor on opening it is, before any image is even decoded.

GUI applications can be timed with --gui, but the result is labelled indicative and nothing more. There is no honest way to measure "time until the window is usable" from outside an application. What is actually measured is the time until the process stops accumulating CPU, and the report says that is what was measured rather than dressing it up.

Comparison mode

dirperf -f json -o freebsd.json /zroot/photos
dirperf -f json -o linux.json /mnt/photos
dirperf compare freebsd.json linux.json

Probes are matched by name and diffed. A probe present in one report and missing from the other is listed rather than silently dropped. The warm ratio compares two medians and is the one to quote; the cold ratio comes from a single pass on each side and is a direction, not a measurement.

A note on the analysis

Nothing in analysis.zig changes a measurement. If a rule and a number disagree, the number is right and the rule is wrong. Each rule states what it observed, in the units it observed it, and what follows; a rule that cannot decide says nothing rather than hedging, because a report full of "this may indicate" is a report nobody acts on.

Software License

Copyright (c) 2026 Vester Imanuel Thacker

All rights reserved.

This software is licensed, not sold.

Permission is granted to download, install, and use the unmodified binary form of this software for any lawful purpose, subject to the following conditions:

  1. You may not modify, reverse engineer, decompile, disassemble, or create derivative works of this software except where such restrictions are prohibited by applicable law.
  2. You may not redistribute, sublicense, rent, lease, or sell this software without prior written permission from the copyright holder.
  3. This license does not grant any rights to the source code. No source code is provided or implied.
  4. All copyright, trademark, and other proprietary notices must remain intact.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.

IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THE SOFTWARE.

Any rights not expressly granted by this license are reserved by the copyright holder.

About

Find out which layer is making a directory slow.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors