1#![deny(unsafe_code)]
24#![deny(missing_docs)]
25
26use crossbeam::{
27 deque::{Injector, Steal, Stealer, Worker},
28 sync::{Parker, Unparker},
29};
30use std::{
31 collections::HashMap,
32 io,
33 path::{Path, PathBuf},
34 sync::{
35 Arc,
36 atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
37 mpsc::{Receiver, SyncSender, sync_channel},
38 },
39 thread,
40};
41
42#[cfg(any(not(any(windows, target_os = "macos")), test))]
43use std::{ffi::OsString, fs};
44
45#[cfg(not(any(windows, target_os = "macos")))]
46pub use std::fs::{FileType, Metadata};
47
48#[cfg(target_os = "macos")]
49#[allow(unsafe_code)]
50mod macos;
51
52#[cfg(windows)]
53#[allow(unsafe_code)]
54mod windows;
55
56#[cfg(target_os = "macos")]
57pub use macos::{Entry, FileType, Metadata};
58
59#[cfg(target_os = "macos")]
60use macos::ReadDir as NativeReadDir;
61
62#[cfg(windows)]
63pub use windows::{Entry, FileType, Metadata};
64
65#[cfg(windows)]
66use windows::ReadDir as NativeReadDir;
67
68type Descend = dyn Fn(usize, &Entry) -> bool + Send + Sync;
71type Batch = io::Result<Vec<io::Result<Entry>>>;
75const ENTRY_CHUNK_SIZE: usize = 4;
78
79#[derive(Clone, Copy)]
81pub enum Order {
82 Completion,
84 ParentFirst,
86}
87
88#[cfg(not(any(windows, target_os = "macos")))]
90pub struct Entry {
91 pub depth: usize,
93 pub file_name: OsString,
95 pub file_type: FileType,
97 pub metadata: io::Result<Metadata>,
99 pub parent_path: Arc<Path>,
101}
102
103enum Job {
104 ReadDir {
106 root_idx: usize,
107 path: Arc<Path>,
108 entry_depth: usize,
111 },
112 #[cfg(not(any(windows, target_os = "macos")))]
114 StatCompletion {
115 root_idx: usize,
116 path: Arc<Path>,
117 entry_depth: usize,
119 entries: Vec<fs::DirEntry>,
120 },
121}
122
123impl Job {
124 fn root_idx(&self) -> usize {
126 match self {
127 Job::ReadDir { root_idx, .. } => *root_idx,
128 #[cfg(not(any(windows, target_os = "macos")))]
129 Job::StatCompletion { root_idx, .. } => *root_idx,
130 }
131 }
132}
133
134enum Event {
136 Batch {
137 root_idx: usize,
138 batch: Batch,
139 },
140 RootFinished {
143 root_idx: usize,
144 },
145 Finished,
147}
148
149pub enum RootEvent {
155 Entry(io::Result<Entry>),
157 Finished,
159}
160
161struct PoolShared {
162 injector: Injector<Job>,
164 stealers: Vec<Stealer<Job>>,
165 stop: AtomicBool,
166 descend: Arc<Descend>,
167 events: SyncSender<Event>,
168 active_roots: AtomicUsize,
170 jobs_per_root: HashMap<usize, AtomicUsize>,
173 order: Order,
174 unparkers: Vec<Unparker>,
176 idle: Vec<AtomicBool>,
179 next_wake: AtomicUsize,
181}
182
183struct Pool {
184 shared: Arc<PoolShared>,
185 events: Receiver<Event>,
186 handles: Vec<thread::JoinHandle<()>>,
187}
188
189pub struct RootWalk {
192 next: Vec<(usize, RootEvent)>,
194 pool: Option<Pool>,
196}
197
198pub struct Walk {
201 next: Vec<io::Result<Entry>>,
208 pool: Option<Pool>,
212}
213
214#[cfg(any(windows, target_os = "macos"))]
220pub fn read_dir(path: &Path) -> io::Result<impl Iterator<Item = io::Result<Entry>>> {
221 NativeReadDir::open(Arc::from(path), 0)
222}
223
224pub fn walk(
228 root: &Path,
229 threads: usize,
230 order: Order,
231 descend: impl Fn(&Entry) -> bool + Send + Sync + 'static,
232) -> Walk {
233 let root = Entry::from_path(root);
234 let pool = match &root {
235 Ok(entry) if entry.file_type.is_dir() && descend(entry) => {
236 let path = Arc::from(entry.path());
237 let pool = start_pool(
238 threads.max(1),
239 HashMap::from([(0, AtomicUsize::new(0))]),
240 order,
241 Arc::new(move |_, entry| descend(entry)),
242 );
243 start_jobs(
244 &pool,
245 vec![Job::ReadDir {
246 root_idx: 0,
247 path,
248 entry_depth: 1,
249 }],
250 );
251 Some(pool)
252 }
253 _ => None,
254 };
255 Walk {
256 next: vec![root],
257 pool,
258 }
259}
260
261impl Iterator for Walk {
262 type Item = io::Result<Entry>;
263
264 fn next(&mut self) -> Option<Self::Item> {
265 loop {
266 if let Some(entry) = self.next.pop() {
267 return Some(entry);
268 }
269
270 match self.pool.as_ref()?.events.recv() {
271 Ok(Event::Batch {
272 batch: Ok(entries), ..
273 }) => {
274 self.next.extend(entries.into_iter().rev());
275 }
276 Ok(Event::Batch {
277 batch: Err(err), ..
278 }) => return Some(Err(err)),
279 Ok(Event::RootFinished { .. }) => {}
280 Ok(Event::Finished) => {
281 self.pool = None;
282 return None;
283 }
284 Err(_) => return Some(Err(io::Error::other("directory worker stopped"))),
285 }
286 }
287 }
288}
289
290pub fn walk_roots(
300 roots: impl IntoIterator<Item = (usize, PathBuf)>,
301 threads: usize,
302 order: Order,
303 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
304) -> RootWalk {
305 start_root_walk(
306 roots.into_iter().collect(),
307 threads,
308 order,
309 descend,
310 |path: PathBuf| Entry::from_path(&path),
311 )
312}
313
314pub fn walk_root_entries(
325 roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
326 threads: usize,
327 order: Order,
328 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
329) -> RootWalk {
330 start_root_walk(
331 roots.into_iter().collect(),
332 threads,
333 order,
334 descend,
335 std::convert::identity,
336 )
337}
338
339fn start_root_walk<Root>(
340 roots: Vec<(usize, Root)>,
341 threads: usize,
342 order: Order,
343 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
344 prepare: impl Fn(Root) -> io::Result<Entry>,
345) -> RootWalk {
346 let jobs_per_root = roots
347 .iter()
348 .map(|(root_idx, _)| (*root_idx, AtomicUsize::new(0)))
349 .collect::<HashMap<_, _>>();
350 assert_eq!(
351 jobs_per_root.len(),
352 roots.len(),
353 "root indices must be unique"
354 );
355 let descend = Arc::new(descend);
356 let (next, root_jobs) = begin_walks(
357 roots
358 .into_iter()
359 .map(|(root_idx, root)| (root_idx, prepare(root))),
360 descend.as_ref(),
361 );
362 let pool = if root_jobs.is_empty() {
363 None
364 } else {
365 let pool = start_pool(threads.max(1), jobs_per_root, order, descend);
366 start_jobs(&pool, root_jobs);
367 Some(pool)
368 };
369 RootWalk { next, pool }
370}
371
372impl Iterator for RootWalk {
373 type Item = (usize, RootEvent);
374
375 fn next(&mut self) -> Option<Self::Item> {
376 loop {
377 if let Some(entry) = self.next.pop() {
378 return Some(entry);
379 }
380 match self.pool.as_ref()?.events.recv() {
381 Ok(Event::Batch {
382 root_idx,
383 batch: Ok(entries),
384 }) => self.next.extend(
385 entries
386 .into_iter()
387 .rev()
388 .map(|entry| (root_idx, RootEvent::Entry(entry))),
389 ),
390 Ok(Event::Batch {
391 root_idx,
392 batch: Err(err),
393 }) => return Some((root_idx, RootEvent::Entry(Err(err)))),
394 Ok(Event::RootFinished { root_idx }) => {
395 return Some((root_idx, RootEvent::Finished));
396 }
397 Ok(Event::Finished) => {
398 self.pool = None;
399 return None;
400 }
401 Err(_) => {
402 return Some((
403 0,
404 RootEvent::Entry(Err(io::Error::other("directory worker stopped"))),
405 ));
406 }
407 }
408 }
409 }
410}
411
412impl PoolShared {
413 fn wake_worker(&self) {
415 let len = self.idle.len();
416 let start = self.next_wake.fetch_add(1, AtomicOrdering::Relaxed) % len;
419 for offset in 0..len {
420 let idx = (start + offset) % len;
421 if self.idle[idx]
422 .compare_exchange(true, false, AtomicOrdering::AcqRel, AtomicOrdering::Relaxed)
423 .is_ok()
424 {
425 self.unparkers[idx].unpark();
426 break;
427 }
428 }
429 }
430
431 fn wake_workers(&self) {
433 for unparker in &self.unparkers {
434 unparker.unpark();
435 }
436 }
437}
438
439#[cfg(not(any(windows, target_os = "macos")))]
440impl Entry {
441 #[must_use]
443 pub fn path(&self) -> PathBuf {
444 self.parent_path.join(&self.file_name)
445 }
446
447 pub fn from_path(path: &Path) -> io::Result<Self> {
449 let metadata = fs::symlink_metadata(path)?;
450 Ok(Self {
451 depth: 0,
452 file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
453 file_type: metadata.file_type(),
454 metadata: Ok(metadata),
455 parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
456 })
457 }
458
459 fn from_dir_entry(
460 depth: usize,
461 parent_path: Arc<Path>,
462 entry: fs::DirEntry,
463 ) -> io::Result<Self> {
464 Ok(Self {
465 depth,
466 file_name: entry.file_name(),
467 file_type: entry.file_type()?,
468 metadata: entry.metadata(),
469 parent_path,
470 })
471 }
472}
473
474fn start_pool(
475 threads: usize,
476 jobs_per_root: HashMap<usize, AtomicUsize>,
477 order: Order,
478 descend: Arc<Descend>,
479) -> Pool {
480 let workers: Vec<_> = (0..threads).map(|_| Worker::new_lifo()).collect();
481 let parkers: Vec<_> = (0..threads).map(|_| Parker::new()).collect();
482 let (event_tx, event_rx) = sync_channel(threads * 2);
483 let shared = Arc::new(PoolShared {
484 injector: Injector::new(),
485 stealers: workers.iter().map(Worker::stealer).collect(),
486 stop: AtomicBool::new(false),
487 descend,
488 events: event_tx,
489 active_roots: AtomicUsize::new(0),
490 jobs_per_root,
491 order,
492 unparkers: parkers
493 .iter()
494 .map(|parker| parker.unparker().clone())
495 .collect(),
496 idle: (0..threads).map(|_| AtomicBool::new(false)).collect(),
497 next_wake: AtomicUsize::new(0),
498 });
499 let handles: Vec<_> = workers
500 .into_iter()
501 .zip(parkers)
502 .enumerate()
503 .map(|(idx, (worker, parker))| {
504 let shared = Arc::clone(&shared);
505 thread::Builder::new()
506 .name(format!("dua-fs-walk-{idx}"))
507 .spawn(move || worker_loop(idx, worker, parker, shared))
508 .expect("filesystem worker thread can be spawned")
509 })
510 .collect();
511
512 Pool {
513 shared,
514 events: event_rx,
515 handles,
516 }
517}
518
519fn begin_walks(
522 roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
523 descend: &Descend,
524) -> (Vec<(usize, RootEvent)>, Vec<Job>) {
525 let mut next = Vec::new();
526 let mut jobs = Vec::new();
527 for (root_idx, mut entry) in roots {
528 if let Ok(entry) = &mut entry {
529 entry.depth = 0;
530 }
531 let has_job = if let Ok(entry) = &entry
532 && entry.metadata.is_ok()
533 && entry.file_type.is_dir()
534 && descend(root_idx, entry)
535 {
536 jobs.push(Job::ReadDir {
537 root_idx,
538 path: Arc::from(entry.path()),
539 entry_depth: 1,
540 });
541 true
542 } else {
543 false
544 };
545 next.push((root_idx, RootEvent::Entry(entry)));
546 if !has_job {
547 next.push((root_idx, RootEvent::Finished));
548 }
549 }
550 next.reverse();
551 (next, jobs)
552}
553
554fn start_jobs(pool: &Pool, root_jobs: Vec<Job>) {
557 let wake_all = root_jobs.len() > 1;
558 debug_assert_eq!(
559 pool.shared.active_roots.load(AtomicOrdering::Relaxed),
560 0,
561 "initial jobs must be started on an idle pool"
562 );
563 debug_assert!(
564 root_jobs.iter().all(|j| match j {
565 Job::ReadDir { entry_depth, .. } => *entry_depth,
566 #[cfg(not(any(windows, target_os = "macos")))]
567 Job::StatCompletion { entry_depth, .. } => *entry_depth,
568 } == 1),
569 "the first jobs should be root jobs, so active_root counts match"
570 );
571 pool.shared
572 .active_roots
573 .store(root_jobs.len(), AtomicOrdering::Relaxed);
574 for job in &root_jobs {
575 add_pending(job.root_idx(), 1, &pool.shared);
576 }
577 for job in root_jobs {
578 pool.shared.injector.push(job);
579 }
580 if wake_all {
581 pool.shared.wake_workers();
582 } else {
583 pool.shared.wake_worker();
584 }
585}
586
587fn worker_loop(idx: usize, worker: Worker<Job>, parker: Parker, shared: Arc<PoolShared>) {
588 while !shared.stop.load(AtomicOrdering::Relaxed) {
589 let found = if let Some(found) = find_job(&worker, &shared) {
590 found
591 } else {
592 shared.idle[idx].store(true, AtomicOrdering::Release);
593 let Some(found) = find_job(&worker, &shared) else {
594 parker.park();
595 shared.idle[idx].store(false, AtomicOrdering::Release);
596 continue;
597 };
598 shared.idle[idx].store(false, AtomicOrdering::Release);
599 found
600 };
601 let (job, stolen) = found;
602 if stolen {
603 shared.wake_worker();
606 }
607 run_job(job, &worker, &shared);
608 }
609}
610
611impl Drop for Pool {
612 fn drop(&mut self) {
613 self.shared.stop.store(true, AtomicOrdering::Relaxed);
614 self.shared.wake_workers();
615 for handle in self.handles.drain(..) {
616 handle.join().ok();
617 }
618 }
619}
620
621fn find_job(worker: &Worker<Job>, shared: &PoolShared) -> Option<(Job, bool)> {
632 loop {
633 if let Some(job) = worker.pop() {
634 return Some((job, false));
635 }
636
637 match shared.injector.steal_batch_and_pop(worker) {
638 Steal::Success(job) => return Some((job, false)),
639 Steal::Retry => continue,
640 Steal::Empty => {}
641 }
642
643 let mut retry = false;
644 for stealer in &shared.stealers {
645 match stealer.steal() {
646 Steal::Success(job) => return Some((job, true)),
647 Steal::Retry => retry = true,
648 Steal::Empty => {}
649 }
650 }
651 if !retry {
652 return None;
653 }
654 }
655}
656
657fn run_job(job: Job, worker: &Worker<Job>, shared: &PoolShared) {
658 match job {
659 Job::ReadDir {
660 root_idx: root,
661 path,
662 entry_depth,
663 } => {
664 if matches!(shared.order, Order::Completion) {
665 read_dir_completion(root, path, entry_depth, worker, shared);
666 } else {
667 read_dir_parent_first(root, path, entry_depth, worker, shared);
668 }
669 }
670 #[cfg(not(any(windows, target_os = "macos")))]
671 Job::StatCompletion {
672 root_idx: root,
673 path,
674 entry_depth,
675 entries,
676 } => stat_entries_completion(root, path, entry_depth, entries, worker, shared),
677 }
678}
679
680#[cfg(not(any(windows, target_os = "macos")))]
686fn read_dir_completion(
687 root_idx: usize,
688 path: Arc<Path>,
689 entry_depth: usize,
690 worker: &Worker<Job>,
691 shared: &PoolShared,
692) {
693 let dir_entries = match fs::read_dir(&path) {
694 Ok(entries) => entries,
695 Err(err) => {
696 if shared
697 .events
698 .send(Event::Batch {
699 root_idx,
700 batch: Err(err),
701 })
702 .is_err()
703 {
704 shared.stop.store(true, AtomicOrdering::Relaxed);
705 }
706 finish_pending(root_idx, shared);
707 return;
708 }
709 };
710 let mut chunk = Vec::with_capacity(ENTRY_CHUNK_SIZE);
711 let mut errors = Vec::new();
712 let mut has_jobs = false;
713 for entry in dir_entries {
714 match entry {
715 Ok(entry) => {
716 chunk.push(entry);
717 if chunk.len() == ENTRY_CHUNK_SIZE {
718 add_pending(root_idx, 1, shared);
719 worker.push(Job::StatCompletion {
720 root_idx,
721 path: Arc::clone(&path),
722 entry_depth,
723 entries: std::mem::replace(
724 &mut chunk,
725 Vec::with_capacity(ENTRY_CHUNK_SIZE),
726 ),
727 });
728 has_jobs = true;
729 }
730 }
731 Err(err) => errors.push(Err(err)),
732 }
733 }
734 if !chunk.is_empty() {
735 add_pending(root_idx, 1, shared);
736 worker.push(Job::StatCompletion {
737 root_idx,
738 path,
739 entry_depth,
740 entries: chunk,
741 });
742 has_jobs = true;
743 }
744 if has_jobs {
745 shared.wake_worker();
746 }
747 if !errors.is_empty()
748 && shared
749 .events
750 .send(Event::Batch {
751 root_idx,
752 batch: Ok(errors),
753 })
754 .is_err()
755 {
756 shared.stop.store(true, AtomicOrdering::Relaxed);
757 }
758 finish_pending(root_idx, shared);
759}
760
761#[cfg(any(windows, target_os = "macos"))]
767fn read_dir_completion(
768 root_idx: usize,
769 path: Arc<Path>,
770 depth: usize,
771 worker: &Worker<Job>,
772 shared: &PoolShared,
773) {
774 let dir_entries = match NativeReadDir::open(path, depth) {
775 Ok(entries) => entries,
776 Err(err) => {
777 if shared
778 .events
779 .send(Event::Batch {
780 root_idx,
781 batch: Err(err),
782 })
783 .is_err()
784 {
785 shared.stop.store(true, AtomicOrdering::Relaxed);
786 }
787 finish_pending(root_idx, shared);
788 return;
789 }
790 };
791 let mut entries = Vec::with_capacity(ENTRY_CHUNK_SIZE);
792 let mut jobs = Vec::new();
793 for entry in dir_entries {
794 if let Ok(entry) = &entry
795 && entry.file_type.is_dir()
796 && (shared.descend)(root_idx, entry)
797 {
798 jobs.push(Job::ReadDir {
799 root_idx,
800 path: Arc::from(entry.path()),
801 entry_depth: depth + 1,
802 });
803 }
804 entries.push(entry);
805 if entries.len() == ENTRY_CHUNK_SIZE
806 && !publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared)
807 {
808 finish_pending(root_idx, shared);
809 return;
810 }
811 }
812 if !entries.is_empty() {
813 publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared);
814 }
815 finish_pending(root_idx, shared);
816}
817
818#[cfg(any(windows, target_os = "macos"))]
819fn publish_completion_batch(
820 root_idx: usize,
821 entries: &mut Vec<io::Result<Entry>>,
822 jobs: &mut Vec<Job>,
823 worker: &Worker<Job>,
824 shared: &PoolShared,
825) -> bool {
826 add_pending(root_idx, jobs.len(), shared);
827 schedule_jobs(std::mem::take(jobs), worker, shared);
828 if shared
829 .events
830 .send(Event::Batch {
831 root_idx,
832 batch: Ok(std::mem::replace(
833 entries,
834 Vec::with_capacity(ENTRY_CHUNK_SIZE),
835 )),
836 })
837 .is_err()
838 {
839 shared.stop.store(true, AtomicOrdering::Relaxed);
840 false
841 } else {
842 true
843 }
844}
845
846#[cfg(any(windows, target_os = "macos"))]
847fn read_dir_parent_first(
848 root_idx: usize,
849 path: Arc<Path>,
850 depth: usize,
851 worker: &Worker<Job>,
852 shared: &PoolShared,
853) {
854 let dir_entries = match NativeReadDir::open(path, depth) {
855 Ok(entries) => entries,
856 Err(err) => {
857 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
858 return;
859 }
860 };
861 let mut jobs = Vec::new();
862 let entries = dir_entries
863 .map(|entry| {
864 entry.inspect(|entry| {
865 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
866 jobs.push(Job::ReadDir {
867 root_idx,
868 path: Arc::from(entry.path()),
869 entry_depth: depth + 1,
870 });
871 }
872 })
873 })
874 .collect();
875 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
876}
877
878#[cfg(not(any(windows, target_os = "macos")))]
879fn stat_entries_completion(
880 root_idx: usize,
881 path: Arc<Path>,
882 depth: usize,
883 entries: Vec<fs::DirEntry>,
884 worker: &Worker<Job>,
885 shared: &PoolShared,
886) {
887 let mut jobs = Vec::new();
888 let entries = entries
889 .into_iter()
890 .map(|entry| {
891 Entry::from_dir_entry(depth, Arc::clone(&path), entry).inspect(|entry| {
892 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
893 jobs.push(Job::ReadDir {
894 root_idx,
895 path: Arc::from(entry.path()),
896 entry_depth: entry.depth + 1,
897 });
898 }
899 })
900 })
901 .collect();
902 add_pending(root_idx, jobs.len(), shared);
903 schedule_jobs(jobs, worker, shared);
904 if shared
905 .events
906 .send(Event::Batch {
907 root_idx,
908 batch: Ok(entries),
909 })
910 .is_err()
911 {
912 shared.stop.store(true, AtomicOrdering::Relaxed);
913 }
914 finish_pending(root_idx, shared);
915}
916
917#[cfg(not(any(windows, target_os = "macos")))]
925fn read_dir_parent_first(
926 root_idx: usize,
927 path: Arc<Path>,
928 depth: usize,
929 worker: &Worker<Job>,
930 shared: &PoolShared,
931) {
932 read_dir_inline(root_idx, path, depth, worker, shared);
933}
934
935#[cfg(not(any(windows, target_os = "macos")))]
939fn read_dir_inline(
940 root_idx: usize,
941 path: Arc<Path>,
942 depth: usize,
943 worker: &Worker<Job>,
944 shared: &PoolShared,
945) {
946 let dir_entries = match fs::read_dir(&path) {
947 Ok(entries) => entries,
948 Err(err) => {
949 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
950 return;
951 }
952 };
953 let mut jobs = Vec::new();
954 let entries = dir_entries
955 .map(|entry| {
956 entry
957 .and_then(|entry| Entry::from_dir_entry(depth, Arc::clone(&path), entry))
958 .inspect(|entry| {
959 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
960 jobs.push(Job::ReadDir {
961 root_idx,
962 path: Arc::from(entry.path()),
963 entry_depth: depth + 1,
964 });
965 }
966 })
967 })
968 .collect();
969 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
970}
971
972fn finish_directory(
976 root_idx: usize,
977 batch: Batch,
978 jobs: Vec<Job>,
979 worker: &Worker<Job>,
980 shared: &PoolShared,
981) {
982 add_pending(root_idx, jobs.len(), shared);
983
984 match shared.order {
985 Order::ParentFirst => {
986 if shared
987 .events
988 .send(Event::Batch { root_idx, batch })
989 .is_err()
990 {
991 shared.stop.store(true, AtomicOrdering::Relaxed);
992 return;
993 }
994 schedule_jobs(jobs, worker, shared);
995 }
996 Order::Completion => {
997 schedule_jobs(jobs, worker, shared);
998 if shared
999 .events
1000 .send(Event::Batch { root_idx, batch })
1001 .is_err()
1002 {
1003 shared.stop.store(true, AtomicOrdering::Relaxed);
1004 return;
1005 }
1006 }
1007 }
1008
1009 finish_pending(root_idx, shared);
1010}
1011
1012fn add_pending(root: usize, count: usize, shared: &PoolShared) {
1013 shared.jobs_per_root[&root].fetch_add(count, AtomicOrdering::Relaxed);
1014}
1015
1016fn finish_pending(root_idx: usize, shared: &PoolShared) {
1019 if shared.jobs_per_root[&root_idx].fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1020 shared.events.send(Event::RootFinished { root_idx }).ok();
1021 if shared.active_roots.fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1022 shared.events.send(Event::Finished).ok();
1023 }
1024 }
1025}
1026
1027fn schedule_jobs(jobs: Vec<Job>, worker: &Worker<Job>, shared: &PoolShared) {
1028 let has_jobs = !jobs.is_empty();
1029 for job in jobs {
1030 worker.push(job);
1031 }
1032 if has_jobs {
1033 shared.wake_worker();
1034 }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040
1041 #[test]
1042 fn parallel_walk_is_parent_first_and_does_not_follow_symlinks() {
1043 let dir = tempfile::tempdir().unwrap();
1044 fs::create_dir_all(dir.path().join("b/child")).unwrap();
1045 fs::create_dir(dir.path().join("a")).unwrap();
1046 fs::write(dir.path().join("b/child/file"), b"x").unwrap();
1047
1048 #[cfg(unix)]
1049 std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("link")).unwrap();
1050
1051 #[cfg(unix)]
1052 let expected = ["", "a", "b", "b/child", "b/child/file", "link"];
1053 #[cfg(not(unix))]
1054 let expected = ["", "a", "b", "b/child", "b/child/file"];
1055 let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
1056
1057 for threads in [1, 4] {
1058 let paths = walk(dir.path(), threads, Order::ParentFirst, |_| true)
1059 .map(|entry| {
1060 entry
1061 .unwrap()
1062 .path()
1063 .strip_prefix(dir.path())
1064 .unwrap()
1065 .to_owned()
1066 })
1067 .collect::<Vec<_>>();
1068 let mut sorted_paths = paths.clone();
1069 sorted_paths.sort();
1070 assert_eq!(
1071 sorted_paths, expected,
1072 "walk with {threads} threads should visit every expected path exactly once"
1073 );
1074
1075 for path in paths.iter().filter(|path| path.components().count() > 1) {
1076 let parent = path.parent().unwrap();
1077 assert!(
1078 paths.iter().position(|path| path == parent)
1079 < paths.iter().position(|candidate| candidate == path),
1080 "parent {parent:?} should precede child {path:?} with {threads} threads; \
1081 traversal order: {paths:?}"
1082 );
1083 }
1084 }
1085 }
1086
1087 #[test]
1088 fn pruning_keeps_the_directory_and_missing_roots_are_errors() {
1089 let dir = tempfile::tempdir().unwrap();
1090 fs::create_dir_all(dir.path().join("skip/child")).unwrap();
1091
1092 let paths = walk(dir.path(), 2, Order::Completion, |entry| {
1093 entry.file_name != "skip"
1094 })
1095 .map(|entry| entry.unwrap().file_name)
1096 .collect::<Vec<_>>();
1097 assert_eq!(
1098 paths,
1099 vec![
1100 dir.path().file_name().unwrap().to_owned(),
1101 OsString::from("skip")
1102 ],
1103 "a pruned directory should be yielded without traversing its children"
1104 );
1105
1106 assert!(
1107 walk(&dir.path().join("missing"), 2, Order::Completion, |_| true)
1108 .next()
1109 .unwrap()
1110 .is_err(),
1111 "a missing root should be yielded as an I/O error"
1112 );
1113 }
1114
1115 #[test]
1116 fn concurrent_roots_keep_their_identity() {
1117 let dir = tempfile::tempdir().unwrap();
1118 let roots = [dir.path().join("a"), dir.path().join("b")];
1119 for root in &roots {
1120 fs::create_dir_all(root.join("child")).unwrap();
1121 }
1122
1123 let events = walk_roots(
1124 roots.iter().cloned().enumerate(),
1125 2,
1126 Order::Completion,
1127 |_, _| true,
1128 )
1129 .collect::<Vec<_>>();
1130 let mut paths = Vec::new();
1131 let mut last_entry = [0; 2];
1132 let mut finished = [None; 2];
1133 for (position, (root_idx, event)) in events.into_iter().enumerate() {
1134 match event {
1135 RootEvent::Entry(entry) => {
1136 last_entry[root_idx] = position;
1137 paths.push((
1138 root_idx,
1139 entry
1140 .unwrap()
1141 .path()
1142 .strip_prefix(&roots[root_idx])
1143 .unwrap()
1144 .to_owned(),
1145 ));
1146 }
1147 RootEvent::Finished => finished[root_idx] = Some(position),
1148 }
1149 }
1150 paths.sort();
1151 assert_eq!(
1152 paths,
1153 [
1154 (0, PathBuf::new()),
1155 (0, PathBuf::from("child")),
1156 (1, PathBuf::new()),
1157 (1, PathBuf::from("child")),
1158 ]
1159 );
1160 for root_idx in 0..roots.len() {
1161 assert!(
1162 last_entry[root_idx] < finished[root_idx].unwrap(),
1163 "root {root_idx} must finish after its last entry",
1164 );
1165 }
1166 }
1167
1168 #[test]
1169 fn prepared_roots_rebase_existing_descendants() {
1170 let directory = tempfile::tempdir().unwrap();
1171 let descendant = directory.path().join("descendant");
1172 fs::create_dir(&descendant).unwrap();
1173 let child = descendant.join("child");
1174 fs::write(&child, b"nested file").unwrap();
1175
1176 let descendant_entry = walk(directory.path(), 2, Order::ParentFirst, |_| true)
1177 .find_map(|entry| {
1178 let entry = entry.unwrap();
1179 (entry.path() == descendant).then_some(entry)
1180 })
1181 .expect("the initial walk should yield the descendant directory");
1182 assert_eq!(descendant_entry.depth, 1);
1183
1184 let mut events = walk_root_entries(
1185 [(7, Ok(descendant_entry))],
1186 2,
1187 Order::ParentFirst,
1188 |root_idx, entry| {
1189 assert_eq!(root_idx, 7);
1190 assert_eq!(entry.depth, 0, "the predicate should see a re-rooted entry");
1191 true
1192 },
1193 );
1194
1195 let Some((7, RootEvent::Entry(Ok(mut root)))) = events.next() else {
1196 panic!("the prepared descendant should be emitted as the new root");
1197 };
1198 assert_eq!(root.path(), descendant);
1199 assert_eq!(
1200 root.depth, 0,
1201 "the entry originally found at depth 1 must become the new traversal root"
1202 );
1203
1204 let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1205 panic!("the re-rooted directory should emit its child");
1206 };
1207 assert_eq!(entry.path(), child);
1208 assert_eq!(
1209 entry.depth, 1,
1210 "the child depth must be relative to the prepared entry used as the new root"
1211 );
1212 assert_eq!(
1213 events
1214 .next()
1215 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1216 Some((7, true))
1217 );
1218 assert_eq!(
1219 events.next().map(|(root_idx, _)| root_idx),
1220 None,
1221 "nothing left after the Finished event"
1222 );
1223
1224 root.metadata = Err(io::Error::from(io::ErrorKind::PermissionDenied));
1225 let mut events = walk_root_entries([(7, Ok(root))], 2, Order::ParentFirst, |_, _| {
1226 panic!("a directory with inaccessible metadata must not be descended")
1227 });
1228 let Some((7, RootEvent::Entry(Ok(root)))) = events.next() else {
1229 panic!("the prepared directory must retain its metadata error");
1230 };
1231 let error = root
1232 .metadata
1233 .err()
1234 .expect("the inaccessible root must retain its metadata error");
1235 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
1236 assert_eq!(
1237 events
1238 .next()
1239 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1240 Some((7, true))
1241 );
1242 assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1243 }
1244
1245 #[cfg(any(windows, target_os = "macos"))]
1246 #[test]
1247 fn prepared_roots_reuse_native_directory_metadata() {
1248 let directory = tempfile::tempdir().unwrap();
1249 let path = directory.path().join("file");
1250 fs::write(&path, b"cached metadata").unwrap();
1251 let expected_len = fs::metadata(&path).unwrap().len();
1252
1253 let entry = read_dir(directory.path()).unwrap().next().unwrap().unwrap();
1254 assert_eq!(entry.depth, 0);
1255 assert_eq!(entry.path(), path);
1256 fs::remove_file(&path).unwrap();
1257
1258 let mut events = walk_root_entries([(7, Ok(entry))], 1, Order::Completion, |_, _| true);
1259 let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1260 panic!(
1261 "prepared root must be yielded without querying its removed path which would fail"
1262 );
1263 };
1264 assert_eq!(entry.path(), path);
1265 assert_eq!(entry.metadata.unwrap().len(), expected_len);
1266 assert_eq!(
1267 events
1268 .next()
1269 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1270 Some((7, true))
1271 );
1272 assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1273 }
1274
1275 #[test]
1276 fn wide_walk_wakes_multiple_idle_workers() {
1277 let dir = tempfile::tempdir().unwrap();
1278 for idx in 0..32 {
1279 fs::create_dir_all(dir.path().join(format!("{idx}/child"))).unwrap();
1280 }
1281
1282 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1283 let seen_threads = Arc::clone(&worker_threads);
1284 walk(dir.path(), 8, Order::Completion, move |entry| {
1285 if entry.depth == 1 {
1286 thread::sleep(std::time::Duration::from_millis(1));
1287 } else if entry.depth == 2 {
1288 seen_threads.lock().unwrap().insert(thread::current().id());
1289 thread::sleep(std::time::Duration::from_millis(10));
1290 }
1291 true
1292 })
1293 .for_each(drop);
1294
1295 assert!(
1296 worker_threads.lock().unwrap().len() >= 4,
1297 "a wide directory should engage more than the producer and one thief"
1298 );
1299 }
1300
1301 #[cfg(any(windows, target_os = "macos"))]
1302 #[test]
1303 fn native_metadata_is_collected_by_the_directory_worker() {
1304 let dir = tempfile::tempdir().unwrap();
1305 for idx in 0..32 {
1306 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1307 }
1308
1309 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1310 let seen_threads = Arc::clone(&worker_threads);
1311 walk(dir.path(), 8, Order::Completion, move |entry| {
1312 if entry.depth == 1 {
1313 seen_threads.lock().unwrap().insert(thread::current().id());
1314 thread::sleep(std::time::Duration::from_millis(2));
1315 }
1316 true
1317 })
1318 .for_each(drop);
1319
1320 assert_eq!(
1321 worker_threads.lock().unwrap().len(),
1322 1,
1323 "native directory-entry metadata should stay on the enumerating worker"
1324 );
1325 }
1326
1327 #[cfg(any(windows, target_os = "macos"))]
1328 #[test]
1329 fn native_completion_streams_metadata_before_enumeration_finishes() {
1330 let dir = tempfile::tempdir().unwrap();
1331 for idx in 0..=ENTRY_CHUNK_SIZE {
1332 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1333 }
1334
1335 let (continue_tx, continue_rx) = std::sync::mpsc::sync_channel(0);
1336 let continue_rx = Arc::new(std::sync::Mutex::new(continue_rx));
1337 let seen = Arc::new(AtomicUsize::new(0));
1338 let seen_in_worker = Arc::clone(&seen);
1339 let mut entries = walk(dir.path(), 2, Order::Completion, move |entry| {
1340 if entry.depth == 1
1341 && seen_in_worker.fetch_add(1, AtomicOrdering::Relaxed) == ENTRY_CHUNK_SIZE
1342 {
1343 continue_rx
1344 .lock()
1345 .unwrap()
1346 .recv_timeout(std::time::Duration::from_secs(2))
1347 .expect("the first metadata batch should arrive before enumeration finishes");
1348 }
1349 true
1350 });
1351
1352 assert_eq!(
1353 entries.next().unwrap().unwrap().depth,
1354 0,
1355 "the root entry should be yielded first"
1356 );
1357 assert_eq!(
1358 entries.next().unwrap().unwrap().depth,
1359 1,
1360 "the first metadata batch should be yielded before enumeration resumes"
1361 );
1362 continue_tx.send(()).unwrap();
1363 entries.for_each(drop);
1364 assert_eq!(
1365 seen.load(AtomicOrdering::Relaxed),
1366 ENTRY_CHUNK_SIZE + 1,
1367 "all directory entries should be inspected"
1368 );
1369 }
1370}