1use crate::FileIdentity;
2use crate::runtime::ParallelRuntime;
3use crate::walker::{ErrorPolicy, WalkEntry, WalkError, WalkOperation, WalkOptions};
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex, mpsc};
6
7mod collect;
8pub(crate) mod dynamic;
9mod ordered_pull;
10mod pull;
11mod visit;
12mod visit_worker;
13
14use collect::{DirectoryTask, collect_lane, collect_serial, collect_shallow, expand_frontier};
15pub use pull::ParallelWalkIter;
16pub use visit::{ParallelVisitReport, WalkControl, WalkEvent};
17
18#[derive(Debug)]
20pub struct ParallelWalkReport {
21 pub entries: Vec<WalkEntry>,
22 pub errors: Vec<WalkError>,
23}
24
25pub struct ParallelWalker {
32 pub(super) root: PathBuf,
33 pub(super) options: WalkOptions,
34 pub(super) parallelism: usize,
35 pub(super) skip_stdout: Option<FileIdentity>,
36 pub(super) runtime: ParallelRuntime,
37}
38
39impl ParallelWalker {
40 #[must_use]
41 pub fn new(root: impl Into<PathBuf>) -> Self {
42 Self {
43 root: root.into(),
44 options: WalkOptions::default(),
45 parallelism: 0,
46 skip_stdout: None,
47 runtime: ParallelRuntime::global(),
48 }
49 }
50
51 #[must_use]
52 pub const fn options(mut self, options: WalkOptions) -> Self {
53 self.options = options;
54 self
55 }
56
57 #[must_use]
59 pub const fn with_parallelism(mut self, parallelism: usize) -> Self {
60 self.parallelism = parallelism;
61 self
62 }
63
64 #[must_use]
66 pub fn runtime(mut self, runtime: ParallelRuntime) -> Self {
67 self.runtime = runtime;
68 self
69 }
70
71 #[must_use]
77 pub fn skip_stdout(mut self, enabled: bool) -> Self {
78 self.skip_stdout = enabled.then(crate::stdout::identity).flatten();
79 self
80 }
81
82 pub fn walk(mut self) -> Result<ParallelWalkReport, WalkError> {
94 self.options = self.options.normalized();
95 let skip_stdout = self.skip_stdout;
96 if self.runtime.is_worker_thread() {
97 return collect_serial(&self.root, self.options)
98 .map(|report| without_stdout(report, skip_stdout));
99 }
100 if self.options.follow_links {
101 return self
102 .walk_dynamic()
103 .map(|report| without_stdout(report, skip_stdout));
104 }
105 let mut shallow = collect_shallow(&self.root, self.options)?;
106 if self.options.error_policy == ErrorPolicy::Abort && !shallow.errors.is_empty() {
107 return Err(shallow.errors.into_iter().next().expect("error exists"));
108 }
109 if !self.options.same_file_system && shallow.tasks.len() < 2 {
110 let target = requested_workers(&self.runtime, self.parallelism, self.options.max_open)
111 .min(FRONTIER_TARGET_TASKS);
112 shallow = expand_frontier(shallow, self.options, target);
113 }
114 if shallow.tasks.is_empty() {
115 return Ok(without_stdout(
116 ParallelWalkReport {
117 entries: shallow.entries,
118 errors: shallow.errors,
119 },
120 skip_stdout,
121 ));
122 }
123 self.walk_lanes(shallow)
124 .map(|report| without_stdout(report, skip_stdout))
125 }
126
127 fn walk_dynamic(self) -> Result<ParallelWalkReport, WalkError> {
128 let report = Arc::new(Mutex::new(ParallelWalkReport {
129 entries: Vec::new(),
130 errors: Vec::new(),
131 }));
132 let visitor_report = Arc::clone(&report);
133 dynamic::stream_batched(
134 &self.root,
135 self.options,
136 self.parallelism,
137 &self.runtime,
138 &crate::CancellationToken::new(),
139 move |entries, errors| {
140 let mut report = visitor_report
141 .lock()
142 .expect("parallel walk report is not poisoned");
143 report.entries.extend(entries);
144 report.errors.extend(errors.iter().map(copy_walk_error));
145 true
146 },
147 )?;
148 let mut report = Arc::try_unwrap(report)
149 .expect("parallel walk visitor released report")
150 .into_inner()
151 .expect("parallel walk report is not poisoned");
152 report
153 .entries
154 .sort_unstable_by(|left, right| left.path().cmp(right.path()));
155 report.errors.sort_unstable_by(|left, right| {
156 left.path()
157 .cmp(right.path())
158 .then_with(|| left.depth().cmp(&right.depth()))
159 });
160 Ok(report)
161 }
162
163 fn walk_lanes(self, shallow: collect::ShallowWalk) -> Result<ParallelWalkReport, WalkError> {
164 let mut entries = shallow.entries;
165 let mut errors = shallow.errors;
166 let task_count = shallow.tasks.len();
167 let worker_count = parallel_worker_count(
168 &self.runtime,
169 self.parallelism,
170 self.options.max_open,
171 task_count,
172 );
173 let mut lanes = (0..worker_count)
174 .map(|_| Vec::<DirectoryTask>::new())
175 .collect::<Vec<_>>();
176 for (index, task) in shallow.tasks.into_iter().enumerate() {
177 lanes[index % worker_count].push(task);
178 }
179 let (sender, receiver) = mpsc::channel();
180 for (index, lane) in lanes.into_iter().enumerate() {
181 let sender = sender.clone();
182 let root = Arc::clone(&shallow.root);
183 let options = self.options;
184 self.runtime
185 .try_execute(move || {
186 let report = collect_lane(lane, options, &root);
187 let _ = sender.send((index, report));
188 })
189 .map_err(|source| schedule_error(&self.root, source))?;
190 }
191 drop(sender);
192 let mut completed = (0..worker_count).map(|_| None).collect::<Vec<_>>();
193 for (index, report) in receiver {
194 completed[index] = Some(report);
195 }
196 let mut lanes = completed
197 .into_iter()
198 .map(|lane| {
199 let lane = lane.expect("every parallel lane reports completion");
200 (
201 lane.report.entries.into_iter(),
202 lane.report.errors.into_iter(),
203 lane.segments,
204 )
205 })
206 .collect::<Vec<_>>();
207 for task_index in 0..task_count {
208 let (lane_entries, lane_errors, segments) = &mut lanes[task_index % worker_count];
209 let segment = segments
210 .pop_front()
211 .expect("every directory task has an output segment");
212 entries.extend(lane_entries.by_ref().take(segment.entries));
213 errors.extend(lane_errors.by_ref().take(segment.errors));
214 }
215 if self.options.error_policy == ErrorPolicy::Abort && !errors.is_empty() {
216 return Err(errors.remove(0));
217 }
218 Ok(ParallelWalkReport { entries, errors })
219 }
220}
221
222fn parallel_worker_count(
223 runtime: &ParallelRuntime,
224 parallelism: usize,
225 max_open: usize,
226 tasks: usize,
227) -> usize {
228 requested_workers(runtime, parallelism, max_open)
229 .min(tasks)
230 .max(1)
231}
232
233fn requested_workers(runtime: &ParallelRuntime, parallelism: usize, max_open: usize) -> usize {
234 let available = runtime.parallelism();
235 if parallelism == 0 {
236 available.min(default_traversal_workers())
237 } else {
238 parallelism.min(available)
239 }
240 .min(max_open.max(1))
241 .max(1)
242}
243
244fn schedule_error(root: &std::path::Path, source: std::io::Error) -> WalkError {
245 WalkError::new(root, 0, WalkOperation::ScheduleWorker, source)
246}
247
248const fn default_traversal_workers() -> usize {
249 if cfg!(windows) { 16 } else { 8 }
250}
251
252const FRONTIER_TARGET_TASKS: usize = 4;
253
254fn copy_walk_error(error: &WalkError) -> WalkError {
255 WalkError::new(
256 error.path().to_path_buf(),
257 error.depth(),
258 error.operation(),
259 std::io::Error::new(error.io_error().kind(), error.io_error().to_string()),
260 )
261}
262
263pub(super) fn matches_stdout(entry: &WalkEntry, identity: Option<FileIdentity>) -> bool {
264 identity.is_some_and(|identity| {
265 entry.is_file() && crate::stdout::path_matches(entry.path(), identity).unwrap_or(false)
266 })
267}
268
269fn without_stdout(
270 mut report: ParallelWalkReport,
271 identity: Option<FileIdentity>,
272) -> ParallelWalkReport {
273 if identity.is_some() {
274 report
275 .entries
276 .retain(|entry| !matches_stdout(entry, identity));
277 }
278 report
279}