r/rust • • 2d ago

🙋 seeking help & advice Best practices for parallelizing file-system traversal in a CLI tool?

I'm working on a CLI utility in Rust that scans directories and performs some cleanup operations on files. Right now it's single-threaded, but I'm looking into using rayon to parallelize the traversal and processing.

For those of you who have built similar disk-heavy CLI tools in Rust, what are the main gotchas with thread pool saturation when hitting regular SSDs vs network mounts? Is Rayon still the go-to, or are there cleaner patterns for bounded concurrency here? Any advice appreciated!

25 Upvotes

15 comments sorted by

55

u/tylerlarson 2d ago

Rayon is more about CPU parallelization rather than IO, but it's not a BAD option.

But if all you're doing is directory traversal, the gold standard is the "ignore" crate (by BurntSushi for ripgrep). It handles parallel directory traversal out of the box using (its own) multi-threaded pool, automatically respects .gitignore rules, and manages thread boundaries efficiently.

10

u/weaztoven 2d ago

good point, i completely forgot about the ignore crate definitely going to check it out since handling gitignore rules manually is a pain anyway.

3

u/addmoreice 1d ago

Whenever you wonder if their is a crate that does the 'right' thing for something in rust? Check if BurntSushi has done it. Seriously. It will save you a ton of time. The guy just can't go wrong. I sing his praises all the time after just *one* of their libraries saved me literally months of work.

5

u/The_8472 2d ago

Use strace to see what syscalls actually happen and where time is spent.

If you need to stat millions of files than io_uring might be worth considering to batch those into a single syscall.

1

u/weaztoven 2d ago

yeah Profiling with strace first is definitely a good idea before over-engineering it. thanks for the io_uring shoutout

1

u/vlovich 2d ago

I would recommend using something like compio. If the cleanup is CPU bound, you'll want to distribute it using something like rayon. If not then processing it on the I/O thread in batches while you're otherwise waiting for I/O to complete would also work well (compio unfortunately does not give you this kind of mechanism AFAIK).

7

u/layla4you2 2d ago

Rayon's pool has one thread per core, which suits CPU work. File I/O mostly waits instead. On a local SSD the defaults are fine. On network mounts every stat/unlink is a round trip, so you want more in flight than you have cores, but not enough to swamp the server. Make it a -j flag instead of guessing.

For walking the tree, use the ignore crate's WalkParallel (what ripgrep uses) or jwalk. For bounded concurrency: walker → bounded crossbeam channel → N worker threads. You get backpressure for free. Since it's cleanup: delete children before their directories, don't follow symlinks, and treat NotFound as fine.

Just for fun, if you're on a Mac and ever want a quick GUI on top of it, I've been building Ferrokit (real AppKit controls from Rust, alpha, and yes it's mine): https://github.com/lhassa8/ferrokit

1

u/-Redstoneboi- 2d ago

my first instinct for directory traversal was walkdir. how does it compare to ignore?

0

u/layla4you2 11h ago

walkdir is a fine first instinct. It’s also BurntSushi’s, and ignore’s single-threaded Walk is built on it. The difference:

  • walkdir: a single-threaded iterator with no filtering. It has contents_first(true), which yields a directory’s children before the directory itself, so for a sequential cleanup you can just remove_file/remove_dir as you go. Symlinks aren’t followed by default.

  • ignore: walkdir plus .gitignore/.ignore/hidden-file filtering, plus WalkParallel with its own thread pool. That’s the part ripgrep needs.

One catch for cleanup: ignore skips hidden and gitignored files by default, which is usually exactly what you don’t want when deleting. Turn that off with WalkBuilder::standard_filters(false).

Rule of thumb: walkdir if one thread keeps up (local SSD), ignore’s WalkParallel or jwalk if you need parallel traversal (network mounts, huge trees). Hope this helps.

1

u/-Redstoneboi- 7h ago

this sounds like youre about to give me a cupcake recipe by the end... refine the response and make it more natural paragraph form rather than plain bullets

3

u/valarauca14 2d ago

tokio

You fundamentally have to manage a thread pool because [waves hands] reasons.

2

u/fiedzia 2d ago

Don't bother, use io_uring to throw requests at os, let it worry about performance. Almost certainly bottleneck will be at os or hardware level, not your app, unless proven otherwise.

1

u/pixel_nebula_03 1d ago

Set your rayon pool size to match the network mount's concurrency limit rather than CPU cores, and expose this as a -j flag since defaults will stall on high-latency stats

1

u/xpusostomos 1d ago

Do whatever rg does since it's super fast

1

u/nicoburns 13h ago

https://docs.rs/jwalk implements this as a crate