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
214pub fn walk(
218 root: &Path,
219 threads: usize,
220 order: Order,
221 descend: impl Fn(&Entry) -> bool + Send + Sync + 'static,
222) -> Walk {
223 let root = Entry::from_path(root);
224 let pool = match &root {
225 Ok(entry) if entry.file_type.is_dir() && descend(entry) => {
226 let path = Arc::from(entry.path());
227 let pool = start_pool(
228 threads.max(1),
229 HashMap::from([(0, AtomicUsize::new(0))]),
230 order,
231 Arc::new(move |_, entry| descend(entry)),
232 );
233 start_jobs(
234 &pool,
235 vec![Job::ReadDir {
236 root_idx: 0,
237 path,
238 entry_depth: 1,
239 }],
240 );
241 Some(pool)
242 }
243 _ => None,
244 };
245 Walk {
246 next: vec![root],
247 pool,
248 }
249}
250
251impl Iterator for Walk {
252 type Item = io::Result<Entry>;
253
254 fn next(&mut self) -> Option<Self::Item> {
255 loop {
256 if let Some(entry) = self.next.pop() {
257 return Some(entry);
258 }
259
260 match self.pool.as_ref()?.events.recv() {
261 Ok(Event::Batch {
262 batch: Ok(entries), ..
263 }) => {
264 self.next.extend(entries.into_iter().rev());
265 }
266 Ok(Event::Batch {
267 batch: Err(err), ..
268 }) => return Some(Err(err)),
269 Ok(Event::RootFinished { .. }) => {}
270 Ok(Event::Finished) => {
271 self.pool = None;
272 return None;
273 }
274 Err(_) => return Some(Err(io::Error::other("directory worker stopped"))),
275 }
276 }
277 }
278}
279
280pub fn walk_roots(
287 roots: impl IntoIterator<Item = (usize, PathBuf)>,
288 threads: usize,
289 order: Order,
290 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
291) -> RootWalk {
292 let roots = roots.into_iter().collect::<Vec<_>>();
293 let jobs_per_root = roots
294 .iter()
295 .map(|(root_idx, _)| (*root_idx, AtomicUsize::new(0)))
296 .collect::<HashMap<_, _>>();
297 assert_eq!(
298 jobs_per_root.len(),
299 roots.len(),
300 "root indices must be unique"
301 );
302 let descend = Arc::new(descend);
303 let (next, root_jobs) = begin_walks(roots, descend.as_ref());
304 let pool = if root_jobs.is_empty() {
305 None
306 } else {
307 let pool = start_pool(threads.max(1), jobs_per_root, order, descend);
308 start_jobs(&pool, root_jobs);
309 Some(pool)
310 };
311 RootWalk { next, pool }
312}
313
314impl Iterator for RootWalk {
315 type Item = (usize, RootEvent);
316
317 fn next(&mut self) -> Option<Self::Item> {
318 loop {
319 if let Some(entry) = self.next.pop() {
320 return Some(entry);
321 }
322 match self.pool.as_ref()?.events.recv() {
323 Ok(Event::Batch {
324 root_idx,
325 batch: Ok(entries),
326 }) => self.next.extend(
327 entries
328 .into_iter()
329 .rev()
330 .map(|entry| (root_idx, RootEvent::Entry(entry))),
331 ),
332 Ok(Event::Batch {
333 root_idx,
334 batch: Err(err),
335 }) => return Some((root_idx, RootEvent::Entry(Err(err)))),
336 Ok(Event::RootFinished { root_idx }) => {
337 return Some((root_idx, RootEvent::Finished));
338 }
339 Ok(Event::Finished) => {
340 self.pool = None;
341 return None;
342 }
343 Err(_) => {
344 return Some((
345 0,
346 RootEvent::Entry(Err(io::Error::other("directory worker stopped"))),
347 ));
348 }
349 }
350 }
351 }
352}
353
354impl PoolShared {
355 fn wake_worker(&self) {
357 let len = self.idle.len();
358 let start = self.next_wake.fetch_add(1, AtomicOrdering::Relaxed) % len;
361 for offset in 0..len {
362 let idx = (start + offset) % len;
363 if self.idle[idx]
364 .compare_exchange(true, false, AtomicOrdering::AcqRel, AtomicOrdering::Relaxed)
365 .is_ok()
366 {
367 self.unparkers[idx].unpark();
368 break;
369 }
370 }
371 }
372
373 fn wake_workers(&self) {
375 for unparker in &self.unparkers {
376 unparker.unpark();
377 }
378 }
379}
380
381#[cfg(not(any(windows, target_os = "macos")))]
382impl Entry {
383 #[must_use]
385 pub fn path(&self) -> PathBuf {
386 self.parent_path.join(&self.file_name)
387 }
388
389 pub fn from_path(path: &Path) -> io::Result<Self> {
391 let metadata = fs::symlink_metadata(path)?;
392 Ok(Self {
393 depth: 0,
394 file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
395 file_type: metadata.file_type(),
396 metadata: Ok(metadata),
397 parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
398 })
399 }
400
401 fn from_dir_entry(
402 depth: usize,
403 parent_path: Arc<Path>,
404 entry: fs::DirEntry,
405 ) -> io::Result<Self> {
406 Ok(Self {
407 depth,
408 file_name: entry.file_name(),
409 file_type: entry.file_type()?,
410 metadata: entry.metadata(),
411 parent_path,
412 })
413 }
414}
415
416fn start_pool(
417 threads: usize,
418 jobs_per_root: HashMap<usize, AtomicUsize>,
419 order: Order,
420 descend: Arc<Descend>,
421) -> Pool {
422 let workers: Vec<_> = (0..threads).map(|_| Worker::new_lifo()).collect();
423 let parkers: Vec<_> = (0..threads).map(|_| Parker::new()).collect();
424 let (event_tx, event_rx) = sync_channel(threads * 2);
425 let shared = Arc::new(PoolShared {
426 injector: Injector::new(),
427 stealers: workers.iter().map(Worker::stealer).collect(),
428 stop: AtomicBool::new(false),
429 descend,
430 events: event_tx,
431 active_roots: AtomicUsize::new(0),
432 jobs_per_root,
433 order,
434 unparkers: parkers
435 .iter()
436 .map(|parker| parker.unparker().clone())
437 .collect(),
438 idle: (0..threads).map(|_| AtomicBool::new(false)).collect(),
439 next_wake: AtomicUsize::new(0),
440 });
441 let handles: Vec<_> = workers
442 .into_iter()
443 .zip(parkers)
444 .enumerate()
445 .map(|(idx, (worker, parker))| {
446 let shared = Arc::clone(&shared);
447 thread::Builder::new()
448 .name(format!("dua-fs-walk-{idx}"))
449 .spawn(move || worker_loop(idx, worker, parker, shared))
450 .expect("filesystem worker thread can be spawned")
451 })
452 .collect();
453
454 Pool {
455 shared,
456 events: event_rx,
457 handles,
458 }
459}
460
461fn begin_walks(
464 roots: impl IntoIterator<Item = (usize, PathBuf)>,
465 descend: &Descend,
466) -> (Vec<(usize, RootEvent)>, Vec<Job>) {
467 let mut next = Vec::new();
468 let mut jobs = Vec::new();
469 for (root_idx, path) in roots {
470 let entry = Entry::from_path(&path);
471 let has_job = if let Ok(entry) = &entry
472 && entry.file_type.is_dir()
473 && descend(root_idx, entry)
474 {
475 jobs.push(Job::ReadDir {
476 root_idx,
477 path: Arc::from(entry.path()),
478 entry_depth: 1,
479 });
480 true
481 } else {
482 false
483 };
484 next.push((root_idx, RootEvent::Entry(entry)));
485 if !has_job {
486 next.push((root_idx, RootEvent::Finished));
487 }
488 }
489 next.reverse();
490 (next, jobs)
491}
492
493fn start_jobs(pool: &Pool, root_jobs: Vec<Job>) {
496 let wake_all = root_jobs.len() > 1;
497 debug_assert_eq!(
498 pool.shared.active_roots.load(AtomicOrdering::Relaxed),
499 0,
500 "initial jobs must be started on an idle pool"
501 );
502 debug_assert!(
503 root_jobs.iter().all(|j| match j {
504 Job::ReadDir { entry_depth, .. } => *entry_depth,
505 #[cfg(not(any(windows, target_os = "macos")))]
506 Job::StatCompletion { entry_depth, .. } => *entry_depth,
507 } == 1),
508 "the first jobs should be root jobs, so active_root counts match"
509 );
510 pool.shared
511 .active_roots
512 .store(root_jobs.len(), AtomicOrdering::Relaxed);
513 for job in &root_jobs {
514 add_pending(job.root_idx(), 1, &pool.shared);
515 }
516 for job in root_jobs {
517 pool.shared.injector.push(job);
518 }
519 if wake_all {
520 pool.shared.wake_workers();
521 } else {
522 pool.shared.wake_worker();
523 }
524}
525
526fn worker_loop(idx: usize, worker: Worker<Job>, parker: Parker, shared: Arc<PoolShared>) {
527 while !shared.stop.load(AtomicOrdering::Relaxed) {
528 let found = if let Some(found) = find_job(&worker, &shared) {
529 found
530 } else {
531 shared.idle[idx].store(true, AtomicOrdering::Release);
532 let Some(found) = find_job(&worker, &shared) else {
533 parker.park();
534 shared.idle[idx].store(false, AtomicOrdering::Release);
535 continue;
536 };
537 shared.idle[idx].store(false, AtomicOrdering::Release);
538 found
539 };
540 let (job, stolen) = found;
541 if stolen {
542 shared.wake_worker();
545 }
546 run_job(job, &worker, &shared);
547 }
548}
549
550impl Drop for Pool {
551 fn drop(&mut self) {
552 self.shared.stop.store(true, AtomicOrdering::Relaxed);
553 self.shared.wake_workers();
554 for handle in self.handles.drain(..) {
555 handle.join().ok();
556 }
557 }
558}
559
560fn find_job(worker: &Worker<Job>, shared: &PoolShared) -> Option<(Job, bool)> {
571 loop {
572 if let Some(job) = worker.pop() {
573 return Some((job, false));
574 }
575
576 match shared.injector.steal_batch_and_pop(worker) {
577 Steal::Success(job) => return Some((job, false)),
578 Steal::Retry => continue,
579 Steal::Empty => {}
580 }
581
582 let mut retry = false;
583 for stealer in &shared.stealers {
584 match stealer.steal() {
585 Steal::Success(job) => return Some((job, true)),
586 Steal::Retry => retry = true,
587 Steal::Empty => {}
588 }
589 }
590 if !retry {
591 return None;
592 }
593 }
594}
595
596fn run_job(job: Job, worker: &Worker<Job>, shared: &PoolShared) {
597 match job {
598 Job::ReadDir {
599 root_idx: root,
600 path,
601 entry_depth,
602 } => {
603 if matches!(shared.order, Order::Completion) {
604 read_dir_completion(root, path, entry_depth, worker, shared);
605 } else {
606 read_dir_parent_first(root, path, entry_depth, worker, shared);
607 }
608 }
609 #[cfg(not(any(windows, target_os = "macos")))]
610 Job::StatCompletion {
611 root_idx: root,
612 path,
613 entry_depth,
614 entries,
615 } => stat_entries_completion(root, path, entry_depth, entries, worker, shared),
616 }
617}
618
619#[cfg(not(any(windows, target_os = "macos")))]
625fn read_dir_completion(
626 root_idx: usize,
627 path: Arc<Path>,
628 entry_depth: usize,
629 worker: &Worker<Job>,
630 shared: &PoolShared,
631) {
632 let dir_entries = match fs::read_dir(&path) {
633 Ok(entries) => entries,
634 Err(err) => {
635 if shared
636 .events
637 .send(Event::Batch {
638 root_idx,
639 batch: Err(err),
640 })
641 .is_err()
642 {
643 shared.stop.store(true, AtomicOrdering::Relaxed);
644 }
645 finish_pending(root_idx, shared);
646 return;
647 }
648 };
649 let mut chunk = Vec::with_capacity(ENTRY_CHUNK_SIZE);
650 let mut errors = Vec::new();
651 let mut has_jobs = false;
652 for entry in dir_entries {
653 match entry {
654 Ok(entry) => {
655 chunk.push(entry);
656 if chunk.len() == ENTRY_CHUNK_SIZE {
657 add_pending(root_idx, 1, shared);
658 worker.push(Job::StatCompletion {
659 root_idx,
660 path: Arc::clone(&path),
661 entry_depth,
662 entries: std::mem::replace(
663 &mut chunk,
664 Vec::with_capacity(ENTRY_CHUNK_SIZE),
665 ),
666 });
667 has_jobs = true;
668 }
669 }
670 Err(err) => errors.push(Err(err)),
671 }
672 }
673 if !chunk.is_empty() {
674 add_pending(root_idx, 1, shared);
675 worker.push(Job::StatCompletion {
676 root_idx,
677 path,
678 entry_depth,
679 entries: chunk,
680 });
681 has_jobs = true;
682 }
683 if has_jobs {
684 shared.wake_worker();
685 }
686 if !errors.is_empty()
687 && shared
688 .events
689 .send(Event::Batch {
690 root_idx,
691 batch: Ok(errors),
692 })
693 .is_err()
694 {
695 shared.stop.store(true, AtomicOrdering::Relaxed);
696 }
697 finish_pending(root_idx, shared);
698}
699
700#[cfg(any(windows, target_os = "macos"))]
706fn read_dir_completion(
707 root_idx: usize,
708 path: Arc<Path>,
709 depth: usize,
710 worker: &Worker<Job>,
711 shared: &PoolShared,
712) {
713 let dir_entries = match NativeReadDir::open(path, depth) {
714 Ok(entries) => entries,
715 Err(err) => {
716 if shared
717 .events
718 .send(Event::Batch {
719 root_idx,
720 batch: Err(err),
721 })
722 .is_err()
723 {
724 shared.stop.store(true, AtomicOrdering::Relaxed);
725 }
726 finish_pending(root_idx, shared);
727 return;
728 }
729 };
730 let mut entries = Vec::with_capacity(ENTRY_CHUNK_SIZE);
731 let mut jobs = Vec::new();
732 for entry in dir_entries {
733 if let Ok(entry) = &entry
734 && entry.file_type.is_dir()
735 && (shared.descend)(root_idx, entry)
736 {
737 jobs.push(Job::ReadDir {
738 root_idx,
739 path: Arc::from(entry.path()),
740 entry_depth: depth + 1,
741 });
742 }
743 entries.push(entry);
744 if entries.len() == ENTRY_CHUNK_SIZE
745 && !publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared)
746 {
747 finish_pending(root_idx, shared);
748 return;
749 }
750 }
751 if !entries.is_empty() {
752 publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared);
753 }
754 finish_pending(root_idx, shared);
755}
756
757#[cfg(any(windows, target_os = "macos"))]
758fn publish_completion_batch(
759 root_idx: usize,
760 entries: &mut Vec<io::Result<Entry>>,
761 jobs: &mut Vec<Job>,
762 worker: &Worker<Job>,
763 shared: &PoolShared,
764) -> bool {
765 add_pending(root_idx, jobs.len(), shared);
766 schedule_jobs(std::mem::take(jobs), worker, shared);
767 if shared
768 .events
769 .send(Event::Batch {
770 root_idx,
771 batch: Ok(std::mem::replace(
772 entries,
773 Vec::with_capacity(ENTRY_CHUNK_SIZE),
774 )),
775 })
776 .is_err()
777 {
778 shared.stop.store(true, AtomicOrdering::Relaxed);
779 false
780 } else {
781 true
782 }
783}
784
785#[cfg(any(windows, target_os = "macos"))]
786fn read_dir_parent_first(
787 root_idx: usize,
788 path: Arc<Path>,
789 depth: usize,
790 worker: &Worker<Job>,
791 shared: &PoolShared,
792) {
793 let dir_entries = match NativeReadDir::open(path, depth) {
794 Ok(entries) => entries,
795 Err(err) => {
796 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
797 return;
798 }
799 };
800 let mut jobs = Vec::new();
801 let entries = dir_entries
802 .map(|entry| {
803 entry.inspect(|entry| {
804 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
805 jobs.push(Job::ReadDir {
806 root_idx,
807 path: Arc::from(entry.path()),
808 entry_depth: depth + 1,
809 });
810 }
811 })
812 })
813 .collect();
814 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
815}
816
817#[cfg(not(any(windows, target_os = "macos")))]
818fn stat_entries_completion(
819 root_idx: usize,
820 path: Arc<Path>,
821 depth: usize,
822 entries: Vec<fs::DirEntry>,
823 worker: &Worker<Job>,
824 shared: &PoolShared,
825) {
826 let mut jobs = Vec::new();
827 let entries = entries
828 .into_iter()
829 .map(|entry| {
830 Entry::from_dir_entry(depth, Arc::clone(&path), entry).inspect(|entry| {
831 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
832 jobs.push(Job::ReadDir {
833 root_idx,
834 path: Arc::from(entry.path()),
835 entry_depth: entry.depth + 1,
836 });
837 }
838 })
839 })
840 .collect();
841 add_pending(root_idx, jobs.len(), shared);
842 schedule_jobs(jobs, worker, shared);
843 if shared
844 .events
845 .send(Event::Batch {
846 root_idx,
847 batch: Ok(entries),
848 })
849 .is_err()
850 {
851 shared.stop.store(true, AtomicOrdering::Relaxed);
852 }
853 finish_pending(root_idx, shared);
854}
855
856#[cfg(not(any(windows, target_os = "macos")))]
864fn read_dir_parent_first(
865 root_idx: usize,
866 path: Arc<Path>,
867 depth: usize,
868 worker: &Worker<Job>,
869 shared: &PoolShared,
870) {
871 read_dir_inline(root_idx, path, depth, worker, shared);
872}
873
874#[cfg(not(any(windows, target_os = "macos")))]
878fn read_dir_inline(
879 root_idx: usize,
880 path: Arc<Path>,
881 depth: usize,
882 worker: &Worker<Job>,
883 shared: &PoolShared,
884) {
885 let dir_entries = match fs::read_dir(&path) {
886 Ok(entries) => entries,
887 Err(err) => {
888 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
889 return;
890 }
891 };
892 let mut jobs = Vec::new();
893 let entries = dir_entries
894 .map(|entry| {
895 entry
896 .and_then(|entry| Entry::from_dir_entry(depth, Arc::clone(&path), entry))
897 .inspect(|entry| {
898 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
899 jobs.push(Job::ReadDir {
900 root_idx,
901 path: Arc::from(entry.path()),
902 entry_depth: depth + 1,
903 });
904 }
905 })
906 })
907 .collect();
908 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
909}
910
911fn finish_directory(
915 root_idx: usize,
916 batch: Batch,
917 jobs: Vec<Job>,
918 worker: &Worker<Job>,
919 shared: &PoolShared,
920) {
921 add_pending(root_idx, jobs.len(), shared);
922
923 match shared.order {
924 Order::ParentFirst => {
925 if shared
926 .events
927 .send(Event::Batch { root_idx, batch })
928 .is_err()
929 {
930 shared.stop.store(true, AtomicOrdering::Relaxed);
931 return;
932 }
933 schedule_jobs(jobs, worker, shared);
934 }
935 Order::Completion => {
936 schedule_jobs(jobs, worker, shared);
937 if shared
938 .events
939 .send(Event::Batch { root_idx, batch })
940 .is_err()
941 {
942 shared.stop.store(true, AtomicOrdering::Relaxed);
943 return;
944 }
945 }
946 }
947
948 finish_pending(root_idx, shared);
949}
950
951fn add_pending(root: usize, count: usize, shared: &PoolShared) {
952 shared.jobs_per_root[&root].fetch_add(count, AtomicOrdering::Relaxed);
953}
954
955fn finish_pending(root_idx: usize, shared: &PoolShared) {
958 if shared.jobs_per_root[&root_idx].fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
959 shared.events.send(Event::RootFinished { root_idx }).ok();
960 if shared.active_roots.fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
961 shared.events.send(Event::Finished).ok();
962 }
963 }
964}
965
966fn schedule_jobs(jobs: Vec<Job>, worker: &Worker<Job>, shared: &PoolShared) {
967 let has_jobs = !jobs.is_empty();
968 for job in jobs {
969 worker.push(job);
970 }
971 if has_jobs {
972 shared.wake_worker();
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 #[test]
981 fn parallel_walk_is_parent_first_and_does_not_follow_symlinks() {
982 let dir = tempfile::tempdir().unwrap();
983 fs::create_dir_all(dir.path().join("b/child")).unwrap();
984 fs::create_dir(dir.path().join("a")).unwrap();
985 fs::write(dir.path().join("b/child/file"), b"x").unwrap();
986
987 #[cfg(unix)]
988 std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("link")).unwrap();
989
990 #[cfg(unix)]
991 let expected = ["", "a", "b", "b/child", "b/child/file", "link"];
992 #[cfg(not(unix))]
993 let expected = ["", "a", "b", "b/child", "b/child/file"];
994 let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
995
996 for threads in [1, 4] {
997 let paths = walk(dir.path(), threads, Order::ParentFirst, |_| true)
998 .map(|entry| {
999 entry
1000 .unwrap()
1001 .path()
1002 .strip_prefix(dir.path())
1003 .unwrap()
1004 .to_owned()
1005 })
1006 .collect::<Vec<_>>();
1007 let mut sorted_paths = paths.clone();
1008 sorted_paths.sort();
1009 assert_eq!(
1010 sorted_paths, expected,
1011 "walk with {threads} threads should visit every expected path exactly once"
1012 );
1013
1014 for path in paths.iter().filter(|path| path.components().count() > 1) {
1015 let parent = path.parent().unwrap();
1016 assert!(
1017 paths.iter().position(|path| path == parent)
1018 < paths.iter().position(|candidate| candidate == path),
1019 "parent {parent:?} should precede child {path:?} with {threads} threads; \
1020 traversal order: {paths:?}"
1021 );
1022 }
1023 }
1024 }
1025
1026 #[test]
1027 fn pruning_keeps_the_directory_and_missing_roots_are_errors() {
1028 let dir = tempfile::tempdir().unwrap();
1029 fs::create_dir_all(dir.path().join("skip/child")).unwrap();
1030
1031 let paths = walk(dir.path(), 2, Order::Completion, |entry| {
1032 entry.file_name != "skip"
1033 })
1034 .map(|entry| entry.unwrap().file_name)
1035 .collect::<Vec<_>>();
1036 assert_eq!(
1037 paths,
1038 vec![
1039 dir.path().file_name().unwrap().to_owned(),
1040 OsString::from("skip")
1041 ],
1042 "a pruned directory should be yielded without traversing its children"
1043 );
1044
1045 assert!(
1046 walk(&dir.path().join("missing"), 2, Order::Completion, |_| true)
1047 .next()
1048 .unwrap()
1049 .is_err(),
1050 "a missing root should be yielded as an I/O error"
1051 );
1052 }
1053
1054 #[test]
1055 fn concurrent_roots_keep_their_identity() {
1056 let dir = tempfile::tempdir().unwrap();
1057 let roots = [dir.path().join("a"), dir.path().join("b")];
1058 for root in &roots {
1059 fs::create_dir_all(root.join("child")).unwrap();
1060 }
1061
1062 let events = walk_roots(
1063 roots.iter().cloned().enumerate(),
1064 2,
1065 Order::Completion,
1066 |_, _| true,
1067 )
1068 .collect::<Vec<_>>();
1069 let mut paths = Vec::new();
1070 let mut last_entry = [0; 2];
1071 let mut finished = [None; 2];
1072 for (position, (root_idx, event)) in events.into_iter().enumerate() {
1073 match event {
1074 RootEvent::Entry(entry) => {
1075 last_entry[root_idx] = position;
1076 paths.push((
1077 root_idx,
1078 entry
1079 .unwrap()
1080 .path()
1081 .strip_prefix(&roots[root_idx])
1082 .unwrap()
1083 .to_owned(),
1084 ));
1085 }
1086 RootEvent::Finished => finished[root_idx] = Some(position),
1087 }
1088 }
1089 paths.sort();
1090 assert_eq!(
1091 paths,
1092 [
1093 (0, PathBuf::new()),
1094 (0, PathBuf::from("child")),
1095 (1, PathBuf::new()),
1096 (1, PathBuf::from("child")),
1097 ]
1098 );
1099 for root_idx in 0..roots.len() {
1100 assert!(
1101 last_entry[root_idx] < finished[root_idx].unwrap(),
1102 "root {root_idx} must finish after its last entry",
1103 );
1104 }
1105 }
1106
1107 #[test]
1108 fn wide_walk_wakes_multiple_idle_workers() {
1109 let dir = tempfile::tempdir().unwrap();
1110 for idx in 0..32 {
1111 fs::create_dir_all(dir.path().join(format!("{idx}/child"))).unwrap();
1112 }
1113
1114 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1115 let seen_threads = Arc::clone(&worker_threads);
1116 walk(dir.path(), 8, Order::Completion, move |entry| {
1117 if entry.depth == 1 {
1118 thread::sleep(std::time::Duration::from_millis(1));
1119 } else if entry.depth == 2 {
1120 seen_threads.lock().unwrap().insert(thread::current().id());
1121 thread::sleep(std::time::Duration::from_millis(10));
1122 }
1123 true
1124 })
1125 .for_each(drop);
1126
1127 assert!(
1128 worker_threads.lock().unwrap().len() >= 4,
1129 "a wide directory should engage more than the producer and one thief"
1130 );
1131 }
1132
1133 #[cfg(any(windows, target_os = "macos"))]
1134 #[test]
1135 fn native_metadata_is_collected_by_the_directory_worker() {
1136 let dir = tempfile::tempdir().unwrap();
1137 for idx in 0..32 {
1138 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1139 }
1140
1141 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1142 let seen_threads = Arc::clone(&worker_threads);
1143 walk(dir.path(), 8, Order::Completion, move |entry| {
1144 if entry.depth == 1 {
1145 seen_threads.lock().unwrap().insert(thread::current().id());
1146 thread::sleep(std::time::Duration::from_millis(2));
1147 }
1148 true
1149 })
1150 .for_each(drop);
1151
1152 assert_eq!(
1153 worker_threads.lock().unwrap().len(),
1154 1,
1155 "native directory-entry metadata should stay on the enumerating worker"
1156 );
1157 }
1158
1159 #[cfg(any(windows, target_os = "macos"))]
1160 #[test]
1161 fn native_completion_streams_metadata_before_enumeration_finishes() {
1162 let dir = tempfile::tempdir().unwrap();
1163 for idx in 0..=ENTRY_CHUNK_SIZE {
1164 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1165 }
1166
1167 let (continue_tx, continue_rx) = std::sync::mpsc::sync_channel(0);
1168 let continue_rx = Arc::new(std::sync::Mutex::new(continue_rx));
1169 let seen = Arc::new(AtomicUsize::new(0));
1170 let seen_in_worker = Arc::clone(&seen);
1171 let mut entries = walk(dir.path(), 2, Order::Completion, move |entry| {
1172 if entry.depth == 1
1173 && seen_in_worker.fetch_add(1, AtomicOrdering::Relaxed) == ENTRY_CHUNK_SIZE
1174 {
1175 continue_rx
1176 .lock()
1177 .unwrap()
1178 .recv_timeout(std::time::Duration::from_secs(2))
1179 .expect("the first metadata batch should arrive before enumeration finishes");
1180 }
1181 true
1182 });
1183
1184 assert_eq!(
1185 entries.next().unwrap().unwrap().depth,
1186 0,
1187 "the root entry should be yielded first"
1188 );
1189 assert_eq!(
1190 entries.next().unwrap().unwrap().depth,
1191 1,
1192 "the first metadata batch should be yielded before enumeration resumes"
1193 );
1194 continue_tx.send(()).unwrap();
1195 entries.for_each(drop);
1196 assert_eq!(
1197 seen.load(AtomicOrdering::Relaxed),
1198 ENTRY_CHUNK_SIZE + 1,
1199 "all directory entries should be inspected"
1200 );
1201 }
1202}