moirai_iter/execution/
parallel.rs1use super::base::ExecutionBase;
4use super::hybrid::owned_chunks;
5use crate::base::SendPtr;
6use std::fmt::Debug;
7
8#[derive(Clone)]
14pub struct ParallelContext {
15 chunk_size: usize,
16}
17
18impl Default for ParallelContext {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl Debug for ParallelContext {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("ParallelContext")
27 .field("chunk_size", &self.chunk_size)
28 .finish()
29 }
30}
31
32impl ParallelContext {
33 pub fn new() -> Self {
35 Self { chunk_size: 1000 }
36 }
37
38 pub fn with_chunk_size(chunk_size: usize) -> Self {
40 Self { chunk_size }
41 }
42}
43
44impl ParallelContext {
45 pub fn execute_iter<T, F, R>(
47 &self,
48 items: Vec<T>,
49 func: F,
50 ) -> Result<Vec<R>, Box<dyn std::error::Error + Send + Sync>>
51 where
52 T: Send + 'static,
53 F: Fn(T) -> R + Send + Sync + 'static,
54 R: Send + 'static,
55 {
56 if items.is_empty() {
57 return Ok(Vec::new());
58 }
59
60 let chunk_size = self.chunk_size.max(1);
61
62 if items.len() <= chunk_size {
63 return Ok(items.into_iter().map(func).collect());
64 }
65
66 let item_count = items.len();
67 let chunks = owned_chunks(items, chunk_size);
68 let num_chunks = chunks.len();
69
70 let mut chunks: Vec<Option<Vec<T>>> = chunks.into_iter().map(Some).collect();
76 let mut chunk_results: Vec<Option<Vec<R>>> = (0..num_chunks).map(|_| None).collect();
77 let chunks_ptr = SendPtr(chunks.as_mut_ptr());
78 let results_ptr = SendPtr(chunk_results.as_mut_ptr());
79
80 let map_chunk = |idx: usize| unsafe {
84 let chunk = (*chunks_ptr.as_ptr().add(idx))
85 .take()
86 .expect("invariant: each chunk is claimed by exactly one index");
87 let mapped: Vec<R> = chunk.into_iter().map(&func).collect();
88 *results_ptr.as_ptr().add(idx) = Some(mapped);
89 };
90
91 let run_on_global = moirai_executor::global()
92 .for_each_indexed::<moirai_executor::schedule::SyncTask, _>(num_chunks, &map_chunk);
93
94 if crate::base::sequential_fallback_permitted(&run_on_global) {
95 (0..num_chunks).for_each(map_chunk);
96 }
97
98 let mut results = Vec::with_capacity(item_count);
99 for chunk in chunk_results {
100 results.extend(chunk.expect("invariant: every chunk index produced a result"));
101 }
102
103 Ok(results)
104 }
105
106 pub fn execute<F, R>(&self, func: F) -> Result<R, Box<dyn std::error::Error + Send + Sync>>
108 where
109 F: FnOnce() -> R + Send,
110 R: Send,
111 {
112 Ok(func())
114 }
115}
116
117impl ExecutionBase for ParallelContext {
118 fn context_type(&self) -> &'static str {
119 "Parallel"
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::test_support::panic_message;
127
128 const CHUNK: usize = 8;
129 const ITEMS: usize = CHUNK * 5;
130
131 #[test]
132 fn execute_iter_returns_every_item_in_input_order() {
133 let context = ParallelContext::with_chunk_size(CHUNK);
134 let items: Vec<usize> = (0..ITEMS).collect();
135
136 let doubled = context
137 .execute_iter(items.clone(), |item| item * 2)
138 .expect("chunked execution must succeed");
139
140 assert_eq!(
141 doubled,
142 items.iter().map(|item| item * 2).collect::<Vec<_>>(),
143 "results must follow input order, not completion order"
144 );
145 }
146
147 #[test]
148 fn execute_iter_propagates_a_chunk_panic_instead_of_truncating() {
149 let context = ParallelContext::with_chunk_size(CHUNK);
153 let items: Vec<usize> = (0..ITEMS).collect();
154
155 let previous_hook = std::panic::take_hook();
156 std::panic::set_hook(Box::new(|_| {}));
157 let outcome = std::panic::catch_unwind(move || {
158 context.execute_iter(items, |item| {
159 assert_ne!(item, ITEMS - 1, "chunk panic");
160 item
161 })
162 });
163 std::panic::set_hook(previous_hook);
164
165 let payload = outcome
166 .expect_err("a panicking chunk must reach the caller rather than shorten the result");
167 let message = panic_message(&*payload);
171 assert!(
172 message.contains("indexed fan-out failed after partial execution"),
173 "the fan-out must report partial execution, got {message:?}"
174 );
175 }
176}