1#[cfg(not(target_os = "emscripten"))]
26use rayon::prelude::*;
27use std::sync::atomic::{AtomicBool, Ordering};
28use std::sync::Arc;
29
30pub fn run_parallel<T, R, F>(input: Vec<T>, num_partitions: usize, worker: F) -> Vec<R>
34where
35 T: Send,
36 R: Send,
37 F: Fn(Vec<T>) -> Vec<R> + Sync + Send,
38{
39 let parts = if num_partitions == 0 {
40 1
41 } else {
42 num_partitions
43 };
44 let chunk_size = input.len().div_ceil(parts).max(1);
45 let chunks: Vec<Vec<T>> = input
46 .into_iter()
47 .fold(Vec::with_capacity(parts), |mut acc, x| {
48 if acc
49 .last()
50 .map(|c: &Vec<T>| c.len() >= chunk_size)
51 .unwrap_or(true)
52 {
53 acc.push(Vec::with_capacity(chunk_size));
54 }
55 if let Some(chunk) = acc.last_mut() {
56 chunk.push(x);
57 }
58 acc
59 });
60 #[cfg(not(target_os = "emscripten"))]
61 {
62 chunks
63 .into_par_iter()
64 .map(worker)
65 .reduce(Vec::new, |mut a, b| {
66 a.extend(b);
67 a
68 })
69 }
70 #[cfg(target_os = "emscripten")]
71 {
72 chunks.into_iter().map(worker).fold(Vec::new(), |mut a, b| {
73 a.extend(b);
74 a
75 })
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn run_parallel_preserves_total_elements() {
85 let v: Vec<i32> = (0..100).collect();
86 let out = run_parallel(v.clone(), 4, |chunk| {
87 chunk.into_iter().map(|x| x * 2).collect()
88 });
89 assert_eq!(out.len(), v.len());
90 assert_eq!(
91 out.iter().sum::<i32>(),
92 v.iter().map(|x| x * 2).sum::<i32>()
93 );
94 }
95
96 #[test]
97 fn run_parallel_with_zero_partitions_is_safe() {
98 let out = run_parallel(vec![1, 2, 3], 0, |c| c);
99 assert_eq!(out, vec![1, 2, 3]);
100 }
101
102 #[test]
103 fn parallel_executor_returns_results_in_branch_order() {
104 let par = ParallelExecutor::new(4);
105 let workers: Vec<Box<dyn Fn() -> i32 + Send + Sync>> =
106 vec![Box::new(|| 1), Box::new(|| 2), Box::new(|| 3)];
107 let out = par.execute_branches(&workers);
108 assert_eq!(out, vec![1, 2, 3]);
109 }
110
111 #[test]
112 fn parallel_executor_disabled_falls_back_to_sequential() {
113 let par = ParallelExecutor::new(0);
114 assert!(!par.enabled());
115 let workers: Vec<Box<dyn Fn() -> i32 + Send + Sync>> =
116 vec![Box::new(|| 10), Box::new(|| 20)];
117 let out = par.execute_branches(&workers);
118 assert_eq!(out, vec![10, 20]);
119 }
120
121 #[test]
122 fn parallel_executor_below_threshold_skips_pool() {
123 let par = ParallelExecutor::new(4);
124 let workers: Vec<Box<dyn Fn() -> i32 + Send + Sync>> = vec![Box::new(|| 99)];
125 let out = par.execute_branches(&workers);
126 assert_eq!(out, vec![99]);
127 }
128}
129
130pub const DEFAULT_PARALLEL_WORKERS: usize = 4;
137
138pub const MIN_PARALLEL_BRANCHES: usize = 2;
141
142#[derive(Debug, Clone)]
149pub struct ParallelExecutor {
150 max_workers: usize,
151 shutdown: Arc<AtomicBool>,
152}
153
154impl ParallelExecutor {
155 #[must_use]
159 pub fn new(max_workers: usize) -> Self {
160 Self {
161 max_workers,
162 shutdown: Arc::new(AtomicBool::new(false)),
163 }
164 }
165
166 #[must_use]
168 pub fn enabled(&self) -> bool {
169 self.max_workers > 0 && !self.shutdown.load(Ordering::Acquire)
170 }
171
172 pub fn shutdown(&self) {
176 self.shutdown.store(true, Ordering::Release);
177 }
178
179 pub fn execute_branches<R, F>(&self, workers: &[F]) -> Vec<R>
184 where
185 R: Send,
186 F: Fn() -> R + Sync + Send,
187 {
188 if !self.enabled() || workers.len() < MIN_PARALLEL_BRANCHES {
189 return workers.iter().map(|w| w()).collect();
190 }
191 #[cfg(not(target_os = "emscripten"))]
192 {
193 workers.par_iter().map(|w| w()).collect()
194 }
195 #[cfg(target_os = "emscripten")]
196 {
197 workers.iter().map(|w| w()).collect()
198 }
199 }
200}
201
202impl Default for ParallelExecutor {
203 fn default() -> Self {
204 Self::new(DEFAULT_PARALLEL_WORKERS)
205 }
206}