1use crossbeam::deque::{Injector, Steal, Stealer, Worker};
23use std::{
24 ffi::OsString,
25 fs::{self, FileType, Metadata},
26 io,
27 path::{Path, PathBuf},
28 sync::{
29 Arc, Mutex,
30 atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
31 mpsc::{Receiver, SyncSender, sync_channel},
32 },
33 thread,
34};
35
36type Descend = dyn Fn(&Entry) -> bool + Send + Sync;
37type Batch = io::Result<Vec<io::Result<Entry>>>;
38
39#[derive(Clone, Copy)]
41pub enum Order {
42 Completion,
44 ParentFirst,
46}
47
48pub struct Entry {
50 pub depth: usize,
52 pub file_name: OsString,
54 pub file_type: FileType,
56 pub metadata: io::Result<Metadata>,
58 pub parent_path: Arc<Path>,
60}
61
62struct Job {
63 path: Arc<Path>,
64 depth: usize,
65}
66
67enum Event {
68 Batch(Batch),
69 Finished,
70}
71
72struct PoolShared {
73 injector: Injector<Job>,
75 stealers: Vec<Stealer<Job>>,
76 stop: AtomicBool,
77 descend: Arc<Descend>,
78 events: SyncSender<Event>,
79 pending: AtomicUsize,
81 order: Order,
82 threads: Mutex<Vec<thread::Thread>>,
83 next_wake: AtomicUsize,
85}
86
87struct Pool {
88 shared: Arc<PoolShared>,
89 handles: Vec<thread::JoinHandle<()>>,
90}
91
92pub struct Walk {
94 next: Vec<io::Result<Entry>>,
101 events: Option<Receiver<Event>>,
105 pool: Option<Pool>,
109}
110
111pub fn walk(
113 root: &Path,
114 threads: usize,
115 order: Order,
116 descend: impl Fn(&Entry) -> bool + Send + Sync + 'static,
117) -> Walk {
118 let threads = threads.max(1);
119 let root = Entry::from_path(root);
120 let mut pool = None;
121 let mut events = None;
122
123 if let Ok(entry) = &root
124 && entry.file_type.is_dir()
125 && descend(entry)
126 {
127 let (new_pool, event_rx) = start_pool(
128 Job {
129 path: Arc::from(entry.path()),
130 depth: 1,
131 },
132 threads,
133 order,
134 Arc::new(descend),
135 );
136 pool = Some(new_pool);
137 events = Some(event_rx);
138 }
139
140 Walk {
141 next: vec![root],
142 events,
143 pool,
144 }
145}
146
147impl Iterator for Walk {
148 type Item = io::Result<Entry>;
149
150 fn next(&mut self) -> Option<Self::Item> {
151 loop {
152 if let Some(entry) = self.next.pop() {
153 return Some(entry);
154 }
155
156 match self.events.as_ref()?.recv() {
157 Ok(Event::Batch(Ok(entries))) => {
158 self.next.extend(entries.into_iter().rev());
159 }
160 Ok(Event::Batch(Err(err))) => return Some(Err(err)),
161 Ok(Event::Finished) => {
162 self.events = None;
163 self.pool = None;
164 return None;
165 }
166 Err(_) => return Some(Err(io::Error::other("directory worker stopped"))),
167 }
168 }
169 }
170}
171
172impl PoolShared {
173 fn wake_one_worker(&self) {
180 let threads = self.threads.lock().expect("worker list lock");
181 if let Some(thread) =
182 threads.get(self.next_wake.fetch_add(1, AtomicOrdering::Relaxed) % threads.len().max(1))
183 {
184 thread.unpark();
185 }
186 }
187
188 fn wake_workers(&self) {
189 for thread in self.threads.lock().expect("worker list lock").iter() {
190 thread.unpark();
191 }
192 }
193}
194
195impl Entry {
196 #[must_use]
198 pub fn path(&self) -> PathBuf {
199 self.parent_path.join(&self.file_name)
200 }
201
202 fn from_path(path: &Path) -> io::Result<Self> {
203 let metadata = fs::symlink_metadata(path)?;
204 Ok(Self {
205 depth: 0,
206 file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
207 file_type: metadata.file_type(),
208 metadata: Ok(metadata),
209 parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
210 })
211 }
212
213 fn from_dir_entry(
214 depth: usize,
215 parent_path: Arc<Path>,
216 entry: fs::DirEntry,
217 ) -> io::Result<Self> {
218 Ok(Self {
219 depth,
220 file_name: entry.file_name(),
221 file_type: entry.file_type()?,
222 metadata: entry.metadata(),
223 parent_path,
224 })
225 }
226}
227
228fn start_pool(
229 first_job: Job,
230 threads: usize,
231 order: Order,
232 descend: Arc<Descend>,
233) -> (Pool, Receiver<Event>) {
234 let workers: Vec<_> = (0..threads).map(|_| Worker::new_fifo()).collect();
235 let (event_tx, event_rx) = sync_channel(threads * 2);
236 let shared = Arc::new(PoolShared {
237 injector: Injector::new(),
238 stealers: workers.iter().map(Worker::stealer).collect(),
239 stop: AtomicBool::new(false),
240 descend,
241 events: event_tx,
242 pending: AtomicUsize::new(1),
243 order,
244 threads: Mutex::new(Vec::with_capacity(threads)),
245 next_wake: AtomicUsize::new(0),
246 });
247 shared.injector.push(first_job);
248
249 let handles: Vec<_> = workers
250 .into_iter()
251 .enumerate()
252 .map(|(idx, worker)| {
253 let shared = Arc::clone(&shared);
254 thread::Builder::new()
255 .name(format!("dua-fs-walk-{idx}"))
256 .spawn(move || worker_loop(worker, shared))
257 .expect("filesystem worker thread can be spawned")
258 })
259 .collect();
260 *shared.threads.lock().expect("worker list lock") = handles
261 .iter()
262 .map(|handle| handle.thread().clone())
263 .collect();
264 shared.wake_workers();
265
266 (Pool { shared, handles }, event_rx)
267}
268
269fn worker_loop(worker: Worker<Job>, shared: Arc<PoolShared>) {
270 while !shared.stop.load(AtomicOrdering::Relaxed) {
271 if let Some(job) = find_job(&worker, &shared) {
272 run_job(job, &worker, &shared);
273 } else {
274 thread::park();
275 }
276 }
277}
278
279impl Drop for Pool {
280 fn drop(&mut self) {
281 self.shared.stop.store(true, AtomicOrdering::Relaxed);
282 self.shared.wake_workers();
283 for handle in self.handles.drain(..) {
284 handle.join().ok();
285 }
286 }
287}
288
289fn find_job(worker: &Worker<Job>, shared: &PoolShared) -> Option<Job> {
297 loop {
298 if let Some(job) = worker.pop() {
299 return Some(job);
300 }
301
302 match shared.injector.steal_batch_and_pop(worker) {
303 Steal::Success(job) => return Some(job),
304 Steal::Retry => continue,
305 Steal::Empty => {}
306 }
307
308 let mut retry = false;
309 for stealer in &shared.stealers {
310 match stealer.steal() {
311 Steal::Success(job) => return Some(job),
312 Steal::Retry => retry = true,
313 Steal::Empty => {}
314 }
315 }
316 if !retry {
317 return None;
318 }
319 }
320}
321
322fn run_job(job: Job, worker: &Worker<Job>, shared: &PoolShared) {
323 let (batch, jobs) = read_dir(&job.path, job.depth, shared);
324 shared
325 .pending
326 .fetch_add(jobs.len(), AtomicOrdering::Relaxed);
327
328 match shared.order {
329 Order::ParentFirst => {
330 if shared.events.send(Event::Batch(batch)).is_err() {
331 shared.stop.store(true, AtomicOrdering::Relaxed);
332 return;
333 }
334 schedule_jobs(jobs, worker, shared);
335 }
336 Order::Completion => {
337 schedule_jobs(jobs, worker, shared);
338 if shared.events.send(Event::Batch(batch)).is_err() {
339 shared.stop.store(true, AtomicOrdering::Relaxed);
340 return;
341 }
342 }
343 }
344
345 let only_this_thread_left = shared.pending.fetch_sub(1, AtomicOrdering::Relaxed) == 1;
346 if only_this_thread_left {
347 shared.events.send(Event::Finished).ok();
348 }
349}
350
351fn schedule_jobs(jobs: Vec<Job>, worker: &Worker<Job>, shared: &PoolShared) {
352 let has_jobs = !jobs.is_empty();
353 for job in jobs {
354 worker.push(job);
355 }
356 if has_jobs {
357 shared.wake_one_worker();
358 }
359}
360
361fn read_dir(path: &Arc<Path>, depth: usize, shared: &PoolShared) -> (Batch, Vec<Job>) {
362 let entries = match fs::read_dir(path) {
363 Ok(entries) => entries,
364 Err(err) => return (Err(err), Vec::new()),
365 };
366 let mut jobs = Vec::new();
367 let entries = entries
368 .map(|entry| {
369 entry
370 .and_then(|entry| Entry::from_dir_entry(depth, Arc::clone(path), entry))
371 .inspect(|entry| {
372 if entry.file_type.is_dir() && (shared.descend)(entry) {
373 jobs.push(Job {
374 path: Arc::from(entry.path()),
375 depth: depth + 1,
376 });
377 }
378 })
379 })
380 .collect();
381 (Ok(entries), jobs)
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn parallel_walk_is_parent_first_and_does_not_follow_symlinks() {
390 let dir = tempfile::tempdir().unwrap();
391 fs::create_dir_all(dir.path().join("b/child")).unwrap();
392 fs::create_dir(dir.path().join("a")).unwrap();
393 fs::write(dir.path().join("b/child/file"), b"x").unwrap();
394
395 #[cfg(unix)]
396 std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("link")).unwrap();
397
398 #[cfg(unix)]
399 let expected = ["", "a", "b", "b/child", "b/child/file", "link"];
400 #[cfg(not(unix))]
401 let expected = ["", "a", "b", "b/child", "b/child/file"];
402 let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
403
404 for threads in [1, 4] {
405 let paths = walk(dir.path(), threads, Order::ParentFirst, |_| true)
406 .map(|entry| {
407 entry
408 .unwrap()
409 .path()
410 .strip_prefix(dir.path())
411 .unwrap()
412 .to_owned()
413 })
414 .collect::<Vec<_>>();
415 let mut sorted_paths = paths.clone();
416 sorted_paths.sort();
417 assert_eq!(
418 sorted_paths, expected,
419 "walk with {threads} threads should visit every expected path exactly once"
420 );
421
422 for path in paths.iter().filter(|path| path.components().count() > 1) {
423 let parent = path.parent().unwrap();
424 assert!(
425 paths.iter().position(|path| path == parent)
426 < paths.iter().position(|candidate| candidate == path),
427 "parent {parent:?} should precede child {path:?} with {threads} threads; \
428 traversal order: {paths:?}"
429 );
430 }
431 }
432 }
433
434 #[test]
435 fn pruning_keeps_the_directory_and_missing_roots_are_errors() {
436 let dir = tempfile::tempdir().unwrap();
437 fs::create_dir_all(dir.path().join("skip/child")).unwrap();
438
439 let paths = walk(dir.path(), 2, Order::Completion, |entry| {
440 entry.file_name != "skip"
441 })
442 .map(|entry| entry.unwrap().file_name)
443 .collect::<Vec<_>>();
444 assert_eq!(
445 paths,
446 vec![
447 dir.path().file_name().unwrap().to_owned(),
448 OsString::from("skip")
449 ],
450 "a pruned directory should be yielded without traversing its children"
451 );
452
453 assert!(
454 walk(&dir.path().join("missing"), 2, Order::Completion, |_| true)
455 .next()
456 .unwrap()
457 .is_err(),
458 "a missing root should be yielded as an I/O error"
459 );
460 }
461}