1use std::{
9 env, io,
10 num::NonZeroUsize,
11 sync::{Arc, OnceLock},
12 thread,
13};
14
15use crate::{
16 scope_bounded::scope_bounded,
17 util::{Chunk, good_chunk_size, into_chunks},
18};
19
20pub fn available_parallelism() -> Result<NonZeroUsize, Arc<io::Error>> {
29 static CACHED: OnceLock<Result<NonZeroUsize, Arc<io::Error>>> = OnceLock::new();
30 CACHED
31 .get_or_init(|| {
32 if let Ok(raw) = env::var("TURBO_TASKS_AVAILABLE_PARALLELISM") {
33 Ok(raw.parse::<NonZeroUsize>().unwrap_or_else(|err| {
34 panic!("Invalid TURBO_TASKS_AVAILABLE_PARALLELISM={raw:?}: {err}")
35 }))
36 } else {
37 thread::available_parallelism().map_err(Arc::new)
38 }
39 })
40 .clone()
41}
42
43struct Chunked {
44 chunk_size: usize,
45 chunk_count: usize,
46}
47
48fn get_chunked(len: usize) -> Option<Chunked> {
49 if len <= 1 {
50 return None;
51 }
52 let chunk_size = good_chunk_size(len);
53 let chunk_count = len.div_ceil(chunk_size);
54 if chunk_count <= 1 {
55 return None;
56 }
57 Some(Chunked {
58 chunk_size,
59 chunk_count,
60 })
61}
62
63pub fn for_each<'l, T, F>(items: &'l [T], f: F)
64where
65 T: Sync,
66 F: Fn(&'l T) + Send + Sync,
67{
68 let Some(Chunked {
69 chunk_size,
70 chunk_count,
71 }) = get_chunked(items.len())
72 else {
73 for item in items {
74 f(item);
75 }
76 return;
77 };
78 let f = &f;
79 let _results = scope_bounded(chunk_count, |scope| {
80 for chunk in items.chunks(chunk_size) {
81 scope.spawn(move || {
82 for item in chunk {
83 f(item);
84 }
85 })
86 }
87 });
88}
89
90pub fn for_each_chunk<'l, T, F>(items: &'l [T], f: F)
94where
95 T: Sync,
96 F: Fn(&'l [T]) + Send + Sync,
97{
98 if items.is_empty() {
99 return;
100 }
101 let Some(Chunked {
102 chunk_size,
103 chunk_count,
104 }) = get_chunked(items.len())
105 else {
106 f(items);
107 return;
108 };
109 let f = &f;
110 let _results = scope_bounded(chunk_count, |scope| {
111 for chunk in items.chunks(chunk_size) {
112 scope.spawn(move || f(chunk))
113 }
114 });
115}
116
117pub fn for_each_owned<T>(items: Vec<T>, f: impl Fn(T) + Send + Sync)
118where
119 T: Send + Sync,
120{
121 let Some(Chunked {
122 chunk_size,
123 chunk_count,
124 }) = get_chunked(items.len())
125 else {
126 for item in items {
127 f(item);
128 }
129 return;
130 };
131 let f = &f;
132 let _results = scope_bounded(chunk_count, |scope| {
133 for chunk in into_chunks(items, chunk_size) {
134 scope.spawn(move || {
135 for item in chunk {
137 f(item);
138 }
139 })
140 }
141 });
142}
143
144pub fn try_for_each<'l, T, E>(
145 items: &'l [T],
146 f: impl (Fn(&'l T) -> Result<(), E>) + Send + Sync,
147) -> Result<(), E>
148where
149 T: Sync,
150 E: Send + 'static,
151{
152 let Some(Chunked {
153 chunk_size,
154 chunk_count,
155 }) = get_chunked(items.len())
156 else {
157 for item in items {
158 f(item)?;
159 }
160 return Ok(());
161 };
162 let f = &f;
163 scope_bounded(chunk_count, |scope| {
164 for chunk in items.chunks(chunk_size) {
165 scope.spawn(move || {
166 for item in chunk {
167 f(item)?;
168 }
169 Ok(())
170 })
171 }
172 })
173 .collect::<Result<(), E>>()
174}
175
176pub fn try_for_each_mut<'l, T, E>(
177 items: &'l mut [T],
178 f: impl (Fn(&'l mut T) -> Result<(), E>) + Send + Sync,
179) -> Result<(), E>
180where
181 T: Send + Sync,
182 E: Send + 'static,
183{
184 let Some(Chunked {
185 chunk_size,
186 chunk_count,
187 }) = get_chunked(items.len())
188 else {
189 for item in items {
190 f(item)?;
191 }
192 return Ok(());
193 };
194 let f = &f;
195 scope_bounded(chunk_count, |scope| {
196 for chunk in items.chunks_mut(chunk_size) {
197 scope.spawn(move || {
198 for item in chunk {
199 f(item)?;
200 }
201 Ok(())
202 })
203 }
204 })
205 .collect::<Result<(), E>>()
206}
207
208pub fn try_for_each_owned<T, E>(
209 items: Vec<T>,
210 f: impl (Fn(T) -> Result<(), E>) + Send + Sync,
211) -> Result<(), E>
212where
213 T: Send + Sync,
214 E: Send + 'static,
215{
216 let Some(Chunked {
217 chunk_size,
218 chunk_count,
219 }) = get_chunked(items.len())
220 else {
221 for item in items {
222 f(item)?;
223 }
224 return Ok(());
225 };
226 let f = &f;
227 scope_bounded(chunk_count, |scope| {
228 for chunk in into_chunks(items, chunk_size) {
229 scope.spawn(move || {
230 for item in chunk {
231 f(item)?;
232 }
233 Ok(())
234 })
235 }
236 })
237 .collect::<Result<(), E>>()
238}
239
240pub fn map_collect<'l, Item, PerItemResult, Result>(
241 items: &'l [Item],
242 f: impl Fn(&'l Item) -> PerItemResult + Send + Sync,
243) -> Result
244where
245 Item: Sync,
246 PerItemResult: Send + Sync + 'l,
247 Result: FromIterator<PerItemResult>,
248{
249 let Some(Chunked {
250 chunk_size,
251 chunk_count,
252 }) = get_chunked(items.len())
253 else {
254 return Result::from_iter(items.iter().map(f));
255 };
256 let f = &f;
257 scope_bounded(chunk_count, |scope| {
258 for chunk in items.chunks(chunk_size) {
259 scope.spawn(move || chunk.iter().map(f).collect::<Vec<_>>())
260 }
261 })
262 .flatten()
263 .collect()
264}
265
266pub fn map_collect_owned<'l, Item, PerItemResult, Result>(
267 items: Vec<Item>,
268 f: impl Fn(Item) -> PerItemResult + Send + Sync,
269) -> Result
270where
271 Item: Send + Sync,
272 PerItemResult: Send + Sync + 'l,
273 Result: FromIterator<PerItemResult>,
274{
275 let Some(Chunked {
276 chunk_size,
277 chunk_count,
278 }) = get_chunked(items.len())
279 else {
280 return Result::from_iter(items.into_iter().map(f));
281 };
282 let f = &f;
283 scope_bounded(chunk_count, |scope| {
284 for chunk in into_chunks(items, chunk_size) {
285 scope.spawn(move || chunk.map(f).collect::<Vec<_>>())
286 }
287 })
288 .flatten()
289 .collect()
290}
291
292pub fn map_collect_chunked_owned<'l, Item, PerItemResult, Result>(
293 items: Vec<Item>,
294 f: impl Fn(Chunk<Item>) -> PerItemResult + Send + Sync,
295) -> Result
296where
297 Item: Send + Sync,
298 PerItemResult: Send + Sync + 'l,
299 Result: FromIterator<PerItemResult>,
300{
301 let Some(Chunked {
302 chunk_size,
303 chunk_count,
304 }) = get_chunked(items.len())
305 else {
306 let len = items.len();
307 return Result::from_iter(into_chunks(items, len).map(f));
308 };
309 let f = &f;
310 scope_bounded(chunk_count, |scope| {
311 for chunk in into_chunks(items, chunk_size) {
312 scope.spawn(move || f(chunk))
313 }
314 })
315 .collect()
316}
317
318#[cfg(test)]
319mod tests {
320 use std::{
321 panic::{AssertUnwindSafe, catch_unwind},
322 sync::atomic::{AtomicI32, Ordering},
323 };
324
325 use super::*;
326
327 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
328 async fn test_parallel_for_each() {
329 let input = vec![1, 2, 3, 4, 5];
330 let sum = AtomicI32::new(0);
331 for_each(&input, |&x| {
332 sum.fetch_add(x, Ordering::SeqCst);
333 });
334 assert_eq!(sum.load(Ordering::SeqCst), 15);
335 }
336
337 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
338 async fn test_parallel_try_for_each() {
339 let input = vec![1, 2, 3, 4, 5];
340 let result = try_for_each(&input, |&x| {
341 if x % 2 == 0 {
342 Ok(())
343 } else {
344 Err(format!("Odd number {x} encountered"))
345 }
346 });
347 assert!(result.is_err());
348 assert_eq!(result.unwrap_err(), "Odd number 1 encountered");
349 }
350
351 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
352 async fn test_parallel_try_for_each_mut() {
353 let item_count = available_parallelism().map_or(16, |count| count.get() * 4);
357 let max_value = i32::try_from(item_count).unwrap();
358 let mut input: Vec<_> = (1..=max_value).collect();
359 let result = try_for_each_mut(&mut input, |x| {
360 *x += 10;
361 if *x % 2 == 0 {
362 Ok(())
363 } else {
364 Err(format!("Odd number {} encountered", *x))
365 }
366 });
367 assert!(result.is_err());
368 assert_eq!(result.unwrap_err(), "Odd number 11 encountered");
369 assert_eq!(input, (11..=max_value + 10).collect::<Vec<_>>());
370 }
371
372 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
373 async fn test_parallel_for_each_owned() {
374 let input = vec![1, 2, 3, 4, 5];
375 let sum = AtomicI32::new(0);
376 for_each_owned(input, |x| {
377 sum.fetch_add(x, Ordering::SeqCst);
378 });
379 assert_eq!(sum.load(Ordering::SeqCst), 15);
380 }
381
382 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
383 async fn test_parallel_map_collect() {
384 let input = vec![1, 2, 3, 4, 5];
385 let result: Vec<_> = map_collect(&input, |&x| x * 2);
386 assert_eq!(result, vec![2, 4, 6, 8, 10]);
387 }
388
389 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
390 async fn test_parallel_map_collect_owned() {
391 let input = vec![1, 2, 3, 4, 5];
392 let result: Vec<_> = map_collect_owned(input, |x| x * 2);
393 assert_eq!(result, vec![2, 4, 6, 8, 10]);
394 }
395
396 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
397 async fn test_parallel_map_collect_owned_many() {
398 let input = vec![1; 1000];
399 let result: Vec<_> = map_collect_owned(input, |x| x * 2);
400 assert_eq!(result, vec![2; 1000]);
401 }
402
403 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
404 #[cfg_attr(
406 target_family = "wasm",
407 ignore = "no unwinding on wasm: std is built panic=abort, so catch_unwind cannot catch"
408 )]
409 async fn test_panic_in_scope() {
410 let result = catch_unwind(AssertUnwindSafe(|| {
411 let mut input = vec![1; 1000];
412 input[744] = 2;
413 for_each(&input, |x| {
414 if *x == 2 {
415 panic!("Intentional panic");
416 }
417 });
418 panic!("Should not get here")
419 }));
420 assert!(result.is_err());
421 assert_eq!(
422 result.unwrap_err().downcast_ref::<&str>(),
423 Some(&"Intentional panic")
424 );
425 }
426}