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