commonware_parallel/lib.rs
1//! Parallelize fold operations with pluggable execution strategies.
2//!
3//! This crate provides the [`Strategy`] trait, which abstracts over sequential and parallel
4//! execution of fold operations. This allows algorithms to be written once and executed either
5//! sequentially or in parallel depending on the chosen strategy.
6//!
7//! # Overview
8//!
9//! The core abstraction is the [`Strategy`] trait, which provides several operations:
10//!
11//! **Core Operations:**
12//! - [`run`](Strategy::run): Chooses between serial and parallel operation bodies
13//! - [`run_batches`](Strategy::run_batches): Runs an operation over batches of its input
14//! - [`run_tiles`](Strategy::run_tiles): Runs an operation over tiles of several passes over
15//! the same input
16//! - [`fold`](Strategy::fold): Reduces a collection to a single value
17//! - [`try_fold`](Strategy::try_fold): Like `fold`, but stops applying the fold operation after
18//! failures
19//! - [`fold_init`](Strategy::fold_init): Like `fold`, but with per-partition initialization
20//! - [`sort_by`](Strategy::sort_by): Sorts a slice with a comparator
21//! - [`sort_unstable_by`](Strategy::sort_unstable_by): Like `sort_by`, but without preserving the
22//! order of equal elements
23//!
24//! **Convenience Methods:**
25//! - [`map_collect_vec`](Strategy::map_collect_vec): Maps elements and collects into a `Vec`
26//! - [`try_map_collect_vec`](Strategy::try_map_collect_vec): Maps fallible operations and
27//! collects into a `Result<Vec<_>, _>`
28//! - [`map_init_collect_vec`](Strategy::map_init_collect_vec): Like `map_collect_vec` with
29//! per-partition initialization
30//! - [`map_partition_collect_vec`](Strategy::map_partition_collect_vec): Maps elements, collecting
31//! successful results and tracking indices of filtered elements
32//!
33//! Two implementations are provided:
34//!
35//! - [`Sequential`]: Executes operations sequentially on the current thread (works in `no_std`)
36//! - [`Rayon`]: Adaptively executes collection operations serially or with a Commonware thread pool
37//! (requires `std`)
38//!
39//! # Features
40//!
41//! - `std` (default): Enables the [`Rayon`] strategy and its thread pool
42//! - `test-utils`: Enables `mocks`, strategies that pin or explore a test's execution shapes
43//!
44//! When the `std` feature is disabled, only [`Sequential`] is available, making this crate
45//! suitable for `no_std` environments.
46//!
47//! # Example
48//!
49//! The main benefit of this crate is writing algorithms that can switch between sequential
50//! and parallel execution:
51//!
52//! ```
53//! use commonware_parallel::{Strategy, Sequential};
54//!
55//! fn sum_of_squares(strategy: &impl Strategy, data: &[i64]) -> i64 {
56//! strategy.fold(
57//! data,
58//! || 0i64,
59//! |acc, &x| acc + x * x,
60//! |a, b| a + b,
61//! )
62//! }
63//!
64//! let strategy = Sequential;
65//! let data = vec![1, 2, 3, 4, 5];
66//! let result = sum_of_squares(&strategy, &data);
67//! assert_eq!(result, 55); // 1 + 4 + 9 + 16 + 25
68//! ```
69
70#![doc(
71 html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
72 html_favicon_url = "https://commonware.xyz/favicon.ico"
73)]
74#![cfg_attr(not(any(feature = "std", test)), no_std)]
75
76commonware_macros::stability_scope!(BETA {
77 use cfg_if::cfg_if;
78 use core::{cmp::Ordering, convert::Infallible, fmt, iter, num::NonZeroUsize, ops::Range};
79
80 cfg_if! {
81 if #[cfg(any(feature = "std", test))] {
82 use futures::{
83 channel::oneshot,
84 future::{self, Either},
85 FutureExt as _,
86 };
87 use self::rayon::{ThreadPoolBuildError, ThreadPoolBuilder};
88 use std::{
89 panic::{self, AssertUnwindSafe, Location},
90 pin::Pin,
91 sync::{
92 Arc,
93 atomic::{AtomicUsize, Ordering as AtomicOrdering},
94 },
95 task::Poll,
96 time::Instant,
97 };
98
99 mod policy;
100 pub mod rayon;
101 } else {
102 extern crate alloc;
103 use alloc::vec::Vec;
104 }
105 }
106
107 /// Batches supplied for one invocation of [`Strategy::run_batches`].
108 ///
109 /// Consume this value to prepare and execute the batches. Preparation can borrow input
110 /// slices or split mutable output buffers into disjoint slices for each batch. A
111 /// whole-input run supplies one batch that executes on the calling thread.
112 ///
113 /// Batches cannot outlive the operation that receives them:
114 ///
115 /// ```compile_fail
116 /// use commonware_parallel::{Sequential, Strategy};
117 /// use core::num::NonZeroUsize;
118 ///
119 /// let batches = Sequential.run_batches(8, NonZeroUsize::MIN, 1, |batches| batches);
120 /// ```
121 #[derive(Debug)]
122 pub struct Batches<'scope, S: Strategy> {
123 strategy: &'scope S,
124 ranges: Vec<Range<usize>>,
125 }
126
127 impl<'scope, S: Strategy> Batches<'scope, S> {
128 /// Returns the single batch `0..len`, executed on the calling thread.
129 fn whole(strategy: &'scope S, len: usize) -> Self {
130 Self {
131 strategy,
132 ranges: iter::once(0..len).collect(),
133 }
134 }
135
136 /// Splits `0..len` into `count` ordered batches whose lengths differ by at most one.
137 #[cfg(any(feature = "std", test))]
138 fn balanced(strategy: &'scope S, len: usize, count: usize) -> Self {
139 let per_batch = len / count;
140 let extra = len % count;
141 let ranges = (0..count)
142 .map(|batch| {
143 let start = batch * per_batch + batch.min(extra);
144 start..start + per_batch + usize::from(batch < extra)
145 })
146 .collect();
147 Self { strategy, ranges }
148 }
149
150 /// Returns whether the strategy supplied the whole input as one batch, which executes on
151 /// the calling thread.
152 ///
153 /// Such a run can process the input directly rather than preparing its single batch.
154 pub const fn is_whole(&self) -> bool {
155 self.ranges.len() == 1
156 }
157
158 /// Prepare and map batches, collecting results in batch order.
159 ///
160 /// `prepare` receives ordered ranges that partition the input and must return one item per
161 /// range in the same order. `map_op` may run those items in any order.
162 pub fn map_collect_vec<I, P, F, R>(self, prepare: P, map_op: F) -> Vec<R>
163 where
164 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
165 P: FnOnce(Vec<Range<usize>>) -> I,
166 F: Fn(I::Item) -> R + Send + Sync,
167 R: Send,
168 {
169 if self.is_whole() {
170 prepare(self.ranges).into_iter().map(map_op).collect()
171 } else {
172 self.strategy.map_collect_vec(prepare(self.ranges), map_op)
173 }
174 }
175
176 /// Like [`map_collect_vec`](Self::map_collect_vec), but for fallible mapping.
177 ///
178 /// After an error, remaining items may be skipped. If multiple items fail, any of
179 /// their errors may be returned. Successful results preserve batch order.
180 pub fn try_map_collect_vec<I, P, F, R, E>(
181 self,
182 prepare: P,
183 map_op: F,
184 ) -> Result<Vec<R>, E>
185 where
186 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
187 P: FnOnce(Vec<Range<usize>>) -> I,
188 F: Fn(I::Item) -> Result<R, E> + Send + Sync,
189 R: Send,
190 E: Send,
191 {
192 if self.is_whole() {
193 prepare(self.ranges).into_iter().map(map_op).collect()
194 } else {
195 self.strategy.try_map_collect_vec(prepare(self.ranges), map_op)
196 }
197 }
198 }
199
200 /// A grid that [`Strategy::run_tiles`] can cut, made of `rows` passes over the run's columns.
201 /// Finishing a tile costs a fixed `tile_cost` cells.
202 ///
203 /// A tile is one row over a contiguous range of columns, and a run pays `tile_cost` once per
204 /// tile, so every cut of a row adds a finish. Three rows over eight columns, as a serial run
205 /// and as two workers `A` and `B` might cut them (`[ ]` marks a tile):
206 ///
207 /// ```text
208 /// serial whole rows shares of cells
209 /// row 0 [A A A A A A A A] [A A A A A A A A] [A A A A A A A A]
210 /// row 1 [A A A A A A A A] [B B B B B B B B] [A A A A][B B B B]
211 /// row 2 [A A A A A A A A] [A A A A A A A A] [B B B B B B B B]
212 ///
213 /// 3 tiles on the 3 tiles: A claimed 4 tiles: 12 cells and
214 /// calling thread row 2 after row 0 2 finishes per worker
215 /// ```
216 ///
217 /// Here whole rows leave `A` 16 cells and two finishes against `B`'s eight and one, while
218 /// shares balance the cells at the price of a cut in row 1. Which cut finishes first depends on
219 /// `tile_cost`, and [`Tiles`] describes how a run chooses and how workers claim.
220 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
221 pub struct Grid {
222 /// Passes over the columns.
223 pub rows: usize,
224 /// The cost of finishing a tile, in cells.
225 pub tile_cost: NonZeroUsize,
226 }
227
228 impl Grid {
229 /// Estimates the cells on the critical path when `workers` share this grid over `len`
230 /// columns, and whether the workers take whole rows.
231 ///
232 /// Workers taking whole rows run `ceil(rows / workers)` rows each, and a row costs its
233 /// cells plus a tile's finish. Shares that cut rows balance the cells, but each finishes
234 /// about two extra tiles, one at a share boundary and one from a steal. Each also ends with
235 /// up to a tile's cost of cells that no steal evens out. The estimate considers cutting
236 /// only when half a row is at least the tile cost, and a tie goes to whole rows.
237 const fn critical_path(&self, len: usize, workers: usize) -> (usize, bool) {
238 let tile_cost = self.tile_cost.get();
239 let row = len.saturating_add(tile_cost);
240 let whole = self.rows.div_ceil(workers).saturating_mul(row);
241 if len < tile_cost.saturating_mul(2) {
242 return (whole, true);
243 }
244 let cut = (self.rows.saturating_mul(row) / workers)
245 .saturating_add(tile_cost.saturating_mul(3));
246 if cut < whole { (cut, false) } else { (whole, true) }
247 }
248
249 /// The grid among `grids` with the shortest estimated critical path when `workers`
250 /// share it over `len` columns, as its index, that estimate in cells, and whether the
251 /// workers take whole rows. Ties go to the earliest grid.
252 fn choose(grids: &[Self], len: usize, workers: usize) -> (usize, usize, bool) {
253 grids
254 .iter()
255 .enumerate()
256 .map(|(index, grid)| {
257 let (cells, whole) = grid.critical_path(len, workers);
258 (index, cells, whole)
259 })
260 .min_by_key(|&(_, cells, _)| cells)
261 .expect("run_tiles must receive at least one grid")
262 }
263 }
264
265 /// How a [`Tiles`] run cuts the grid.
266 #[derive(Debug)]
267 enum Plan {
268 /// Every row is one tile, processed on the calling thread.
269 Serial,
270 /// Workers claim whole rows from one queue when `whole` is set, and otherwise start on
271 /// equal shares of the cells and take halves of pending shares when they run out.
272 #[cfg(any(feature = "std", test))]
273 Shared {
274 tile_cost: usize,
275 workers: usize,
276 whole: bool,
277 },
278 }
279
280 /// Tiles supplied for one invocation of [`Strategy::run_tiles`].
281 ///
282 /// Each tile is one row over a contiguous range of columns, and the tiles cover every cell
283 /// exactly once. A serial run makes every row one tile on the calling thread.
284 ///
285 /// A parallel run estimates each grid's critical path over its workers both as whole rows
286 /// and as equal shares of cells, and cuts the grid in the way with the shortest estimate.
287 /// Workers claim whole rows from one queue, one row at a time unless the grid has more than
288 /// 65,535 rows, and whole rows may leave some workers a row short. Shares of cells cut rows
289 /// and cost each worker about two extra tiles and a tile's cost of unevenness.
290 ///
291 /// A worker that runs out of its share of cells takes the back half of the largest other
292 /// pending share. Taking it can cut a row, so it happens only while the half is longer than
293 /// `tile_cost`, where the extra tile pays for itself.
294 ///
295 /// When an operation does its work in `fill`, a worker that runs slowly or starts late holds
296 /// up the run by at most one claim of rows, or by one claim of cells and the halves of its
297 /// share too short to take. Those cells come to a few tile costs, or proportionally more in a
298 /// grid of more than 65,535 tile costs. Work an operation defers to `finish` stays with the
299 /// worker that holds the tile.
300 #[derive(Debug)]
301 pub struct Tiles<'scope, S: Strategy> {
302 #[cfg_attr(
303 not(any(feature = "std", test)),
304 expect(dead_code, reason = "only shared plans, which need std, dispatch work")
305 )]
306 strategy: &'scope S,
307 grid: usize,
308 rows: usize,
309 len: usize,
310 plan: Plan,
311 }
312
313 impl<'scope, S: Strategy> Tiles<'scope, S> {
314 /// Makes every row of the cheapest serial grid one tile, processed on the calling thread.
315 fn serial(strategy: &'scope S, grids: &[Grid], len: usize) -> Self {
316 let (grid, _, _) = Grid::choose(grids, len, 1);
317 Self {
318 strategy,
319 grid,
320 rows: grids[grid].rows,
321 len,
322 plan: Plan::Serial,
323 }
324 }
325
326 /// Shares the grid that `workers` workers finish soonest among them.
327 #[cfg(any(feature = "std", test))]
328 fn parallel(strategy: &'scope S, grids: &[Grid], len: usize, workers: usize) -> Self {
329 let (grid, _, whole) = Grid::choose(grids, len, workers);
330 let rows = grids[grid].rows;
331 assert!(rows.checked_mul(len).is_some(), "rows * len must fit in usize");
332 Self {
333 strategy,
334 grid,
335 rows,
336 len,
337 plan: Plan::Shared {
338 tile_cost: grids[grid].tile_cost.get(),
339 workers,
340 whole,
341 },
342 }
343 }
344
345 /// The index, among the grids supplied to the run, of the grid it cuts.
346 pub const fn grid(&self) -> usize {
347 self.grid
348 }
349
350 /// Fills every tile piece by piece with per-worker state, collecting one result per tile.
351 ///
352 /// `fill` receives a tile's row and the columns of each of its pieces in column order, and
353 /// no piece is empty. `finish` then ends the tile and returns its result. `init` creates
354 /// state that a worker reuses across its tiles, so `finish` must leave the state ready for
355 /// another tile.
356 ///
357 /// Results come back in no particular order. Where a parallel run cuts a row depends on
358 /// timing, so combine a row's results in a way that does not depend on its cuts.
359 pub fn fill_collect_vec<INIT, T, FILL, FINISH, R>(
360 self,
361 init: INIT,
362 fill: FILL,
363 finish: FINISH,
364 ) -> Vec<R>
365 where
366 INIT: Fn() -> T + Send + Sync,
367 T: Send,
368 FILL: Fn(&mut T, usize, Range<usize>) + Send + Sync,
369 FINISH: Fn(&mut T, usize) -> R + Send + Sync,
370 R: Send,
371 {
372 // An empty grid has no tiles, and `Shares` needs at least one cell to size its units.
373 if self.rows == 0 || self.len == 0 {
374 return Vec::new();
375 }
376 match self.plan {
377 // One state fills every row in order, each row one tile in a single piece.
378 Plan::Serial => {
379 let mut state = init();
380 (0..self.rows)
381 .map(|row| {
382 fill(&mut state, row, 0..self.len);
383 finish(&mut state, row)
384 })
385 .collect()
386 }
387
388 // Each worker is one task that claims rows from the queue or cells from its share,
389 // and an idle task can take the back half of another share. `Shares` bounds how
390 // long a late-starting task or a costlier share can hold up the run.
391 #[cfg(any(feature = "std", test))]
392 Plan::Shared {
393 tile_cost,
394 workers,
395 whole,
396 } => {
397 let shares = Shares::new(self.rows, self.len, tile_cost, workers, whole);
398 self.strategy
399 .map_init_collect_vec(0..shares.workers, init, |state, worker| {
400 shares.run(worker, self.len, state, &fill, &finish)
401 })
402 .into_iter()
403 .flatten()
404 .collect()
405 }
406 }
407 }
408 }
409
410 /// Bits for each end of a packed range of units in [`Shares`], few enough for any `usize`.
411 #[cfg(any(feature = "std", test))]
412 const HALF_BITS: u32 = 16;
413
414 /// The largest unit index a packed range can hold.
415 #[cfg(any(feature = "std", test))]
416 const MAX_UNITS: usize = (1 << HALF_BITS) - 1;
417
418 /// The unclaimed cells of a shared [`Tiles`] run.
419 ///
420 /// Whole rows queue in one share that every worker claims from, one unit of rows at a time, so
421 /// a worker that runs slowly or starts late holds up the run by at most one claim.
422 ///
423 /// Cells are divided into one share per worker. Each worker claims from the front of its own
424 /// share, one claim at a time, so its consecutive claims extend one tile within a row and the
425 /// rest stays available. A worker whose share is empty takes the back half of the largest
426 /// other share whose back half pays for a split, and stops once no share qualifies. A slow or
427 /// late worker therefore keeps at most a claim and the halves too short to take. A split can
428 /// cut a row and add a tile, so it happens only while the half is longer than a tile's cost.
429 #[cfg(any(feature = "std", test))]
430 struct Shares {
431 /// The unclaimed units of the one queue of whole rows, or of each worker's share of
432 /// cells, packed with [`pack`] so that claiming from the front and splitting off the back
433 /// are each one atomic update.
434 pending: Vec<Pending>,
435 /// Workers claiming from `pending`.
436 workers: usize,
437 /// Cells per unit, enough that every unit index fits in [`HALF_BITS`].
438 unit: usize,
439 /// Cells in the grid.
440 cells: usize,
441 /// Units in one claim.
442 claim: usize,
443 /// Cells a stolen half must exceed, or zero for the queue of whole rows.
444 split_cost: usize,
445 /// Whether any initial share of cells has a payable split. Claims only shrink shares, and
446 /// only a taken half refills one, so a run with no initial payable split never makes one.
447 /// The queue of whole rows is never split.
448 splittable: bool,
449 }
450
451 /// One worker's packed range of units, alone on its cache line so that neighboring workers'
452 /// claims do not contend.
453 #[cfg(any(feature = "std", test))]
454 #[repr(align(128))]
455 struct Pending(AtomicUsize);
456
457 #[cfg(any(feature = "std", test))]
458 impl Shares {
459 /// Queues the grid's whole rows when `whole` is set, and otherwise splits its cells into
460 /// equal shares, none smaller than a claim. Either way, at most `workers` workers take
461 /// part.
462 ///
463 /// Each unit of the queue holds whole rows, so a claim never cuts a row. Shares of cells
464 /// use units small enough to cut anywhere, claimed one tile's cost at a time.
465 fn new(rows: usize, len: usize, tile_cost: usize, workers: usize, whole: bool) -> Self {
466 // At most `MAX_UNITS` units cover the grid, so a unit may hold several whole rows.
467 let cells = rows * len;
468 let (unit, claim, split_cost) = if whole {
469 (len * rows.div_ceil(MAX_UNITS), 1, 0)
470 } else {
471 let unit = cells.div_ceil(MAX_UNITS);
472 (unit, tile_cost.div_ceil(unit), tile_cost)
473 };
474 let units = cells.div_ceil(unit);
475
476 // A nonempty grid holds at least one whole claim, since `Grid::critical_path` shares
477 // cells only when a row holds two tile costs. This keeps at least one worker and at
478 // most one per whole claim. Shares of cells differ by at most one unit, and the first
479 // `extra` take the larger size.
480 let workers = workers.min(units / claim);
481 let pending = if whole {
482 vec![Pending(AtomicUsize::new(pack(0, units)))]
483 } else {
484 let (per_share, extra) = (units / workers, units % workers);
485 (0..workers)
486 .map(|worker| {
487 let start = worker * per_share + worker.min(extra);
488 let end = start + per_share + usize::from(worker < extra);
489 Pending(AtomicUsize::new(pack(start, end)))
490 })
491 .collect()
492 };
493 let mut shares = Self {
494 pending,
495 workers,
496 unit,
497 cells,
498 claim,
499 split_cost,
500 splittable: false,
501 };
502 shares.splittable = !whole
503 && shares
504 .pending
505 .iter()
506 .any(|pending| shares.split(pending.0.load(AtomicOrdering::Relaxed)).is_some());
507 shares
508 }
509
510 /// The midpoint and actual cell count of a share whose back half pays for a split.
511 fn split(&self, word: usize) -> Option<(usize, usize)> {
512 // Each side of a split keeps at least one unit.
513 let (start, end) = unpack(word);
514 if end - start < 2 {
515 return None;
516 }
517
518 // The last unit can run past the grid, so the back half's actual cells decide
519 // whether the split pays, and the share's actual cells rank it among victims.
520 let middle = start + (end - start) / 2;
521 let stop = end.saturating_mul(self.unit).min(self.cells);
522 (stop - middle * self.unit > self.split_cost)
523 .then_some((middle, stop - start * self.unit))
524 }
525
526 /// Returns the next cells for `worker`, or `None` once no share is worth splitting.
527 fn claim(&self, worker: usize) -> Option<Range<usize>> {
528 // Whole rows queue in the one share.
529 let pending = &self.pending[worker.min(self.pending.len() - 1)].0;
530 let mut word = pending.load(AtomicOrdering::Relaxed);
531 loop {
532 let (start, end) = unpack(word);
533 if start == end {
534 // Refill from a steal, or stop once no share is worth splitting. Only this
535 // worker refills its own empty share, so a plain store cannot lose units.
536 word = self.steal(worker)?;
537 pending.store(word, AtomicOrdering::Relaxed);
538 continue;
539 }
540
541 // Take one claim from the front, clipped to the grid. Thieves only cut the back,
542 // so a failed exchange retries with the current range. Relaxed ordering suffices,
543 // since the word only divides units among workers and carries no other data.
544 let stop = start + self.claim.min(end - start);
545 match pending.compare_exchange_weak(
546 word,
547 pack(stop, end),
548 AtomicOrdering::Relaxed,
549 AtomicOrdering::Relaxed,
550 ) {
551 Ok(_) => {
552 return Some(start * self.unit..stop.saturating_mul(self.unit).min(self.cells));
553 }
554 Err(current) => word = current,
555 }
556 }
557 }
558
559 /// Takes the back half of the largest share other than the thief's own, returning it
560 /// packed.
561 fn steal(&self, thief: usize) -> Option<usize> {
562 // No share can ever pay for a split (see `splittable`).
563 if !self.splittable {
564 return None;
565 }
566 loop {
567 // Pick the largest other share whose back half pays for a split, if any.
568 // Halving the largest share keeps steals, and the tiles they add, few.
569 let (victim, word, middle, _) = (0..self.pending.len())
570 .filter(|&worker| worker != thief)
571 .filter_map(|worker| {
572 let word = self.pending[worker].0.load(AtomicOrdering::Relaxed);
573 self.split(word)
574 .map(|(middle, cells)| (worker, word, middle, cells))
575 })
576 .max_by_key(|&(_, _, _, cells)| cells)?;
577
578 // Keep the victim's front half if its range is still the one scanned.
579 // A claim or another steal in between sends the thief back to scanning.
580 let (start, end) = unpack(word);
581 if self.pending[victim]
582 .0
583 .compare_exchange(
584 word,
585 pack(start, middle),
586 AtomicOrdering::Relaxed,
587 AtomicOrdering::Relaxed,
588 )
589 .is_ok()
590 {
591 return Some(pack(middle, end));
592 }
593 }
594 }
595
596 /// Processes the cells `worker` claims, `len` to a row. A tile ends where its row ends or
597 /// where the worker's next cells do not continue it.
598 fn run<T, R>(
599 &self,
600 worker: usize,
601 len: usize,
602 state: &mut T,
603 fill: &impl Fn(&mut T, usize, Range<usize>),
604 finish: &impl Fn(&mut T, usize) -> R,
605 ) -> Vec<R> {
606 let mut results = Vec::new();
607
608 // The row of the unfinished tile and the column that would continue it.
609 let mut open = None;
610 while let Some(cells) = self.claim(worker) {
611 // Cut the claimed cells at row ends. A piece that does not continue the open tile
612 // (another row, or cells from a stolen half) finishes that tile first.
613 let mut start = cells.start;
614 while start < cells.end {
615 let (row, column) = (start / len, start % len);
616 let end = cells.end.min((row + 1) * len);
617 if open != Some((row, column))
618 && let Some((row, _)) = open
619 {
620 results.push(finish(state, row));
621 }
622 let columns = column..column + (end - start);
623 open = Some((row, columns.end));
624 fill(state, row, columns);
625 start = end;
626 }
627 }
628
629 // The last tile has no next piece to finish it.
630 if let Some((row, _)) = open {
631 results.push(finish(state, row));
632 }
633 results
634 }
635 }
636
637 /// Packs a range of units into one word.
638 #[cfg(any(feature = "std", test))]
639 const fn pack(start: usize, end: usize) -> usize {
640 start << HALF_BITS | end
641 }
642
643 /// Unpacks a range of units from one word.
644 #[cfg(any(feature = "std", test))]
645 const fn unpack(word: usize) -> (usize, usize) {
646 (word >> HALF_BITS, word & MAX_UNITS)
647 }
648
649 /// A strategy for executing fold operations.
650 ///
651 /// This trait abstracts over sequential and parallel execution, allowing algorithms
652 /// to be written generically and then executed with different strategies depending
653 /// on the use case (e.g., sequential for testing/debugging, parallel for production).
654 pub trait Strategy: Clone + Send + Sync + fmt::Debug + 'static {
655 /// Submit one CPU-bound job to this strategy, running it inline on the calling task when
656 /// it is measured cheaper than the round trip of offloading it to the pool.
657 ///
658 /// `len` groups calls at a call site into size classes for those measurements, so similar
659 /// `len` must mean comparable cost. An inline job runs to completion before `spawn`
660 /// returns, and jobs whose measured cost exceeds a small time budget offload.
661 ///
662 /// The returned future resolves when the job completes. Blocking on external
663 /// synchronization or I/O inside the job can occupy execution capacity until it returns.
664 /// When the polling thread itself belongs to the strategy's execution resources (e.g. a
665 /// runtime whose executor thread is registered as a pool worker), the job (and other
666 /// pending work) may be executed inline on that thread rather than waited on.
667 ///
668 /// If the job panics, the panic is propagated to the caller; it never aborts the process.
669 #[track_caller]
670 fn spawn<F, T>(
671 &self,
672 len: usize,
673 f: F,
674 ) -> impl core::future::Future<Output = T> + Send + 'static
675 where
676 F: FnOnce(Self) -> T + Send + 'static,
677 T: Send + 'static;
678
679 /// Runs either a serial or parallel body.
680 #[track_caller]
681 fn run<R, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> R
682 where
683 R: Send,
684 SEQ: FnOnce() -> R + Send,
685 PAR: FnOnce() -> R + Send;
686
687 /// Like [`run`](Self::run), but for fallible work.
688 ///
689 /// The strategy chooses and runs either the serial or parallel body, returning the
690 /// first error produced by the chosen body. Elapsed time is only recorded on success,
691 /// so abort-early error paths cannot poison the adaptive policy's estimates.
692 #[track_caller]
693 fn try_run<R, E, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> Result<R, E>
694 where
695 R: Send,
696 E: Send,
697 SEQ: FnOnce() -> Result<R, E> + Send,
698 PAR: FnOnce() -> Result<R, E> + Send;
699
700 /// Run an operation on strategy-provided batches.
701 ///
702 /// `run` is called once with batches covering `0..len`. The strategy either supplies one
703 /// batch that executes on the calling thread or splits the input into two or more batches
704 /// no shorter than `minimum_batch_len` that may execute in parallel. An input too short to
705 /// split always gets one batch.
706 ///
707 /// `multiplier` estimates work per input unit. Complete the operation, including
708 /// preparation and result assembly, inside `run`. Both execution shapes must produce
709 /// equivalent results. The default implementation runs the whole input.
710 ///
711 /// # Examples
712 ///
713 /// ```
714 /// use commonware_parallel::{Sequential, Strategy};
715 /// use core::num::NonZeroUsize;
716 ///
717 /// let values = [1u64, 2, 3, 4];
718 /// let total = Sequential.run_batches(values.len(), NonZeroUsize::MIN, 1, |batches| {
719 /// batches
720 /// .map_collect_vec(
721 /// |ranges| ranges.into_iter().map(|range| &values[range]).collect::<Vec<_>>(),
722 /// |batch| batch.iter().sum::<u64>(),
723 /// )
724 /// .into_iter()
725 /// .sum::<u64>()
726 /// });
727 /// assert_eq!(total, 10);
728 /// ```
729 #[track_caller]
730 fn run_batches<R, F>(
731 &self,
732 len: usize,
733 minimum_batch_len: NonZeroUsize,
734 multiplier: usize,
735 run: F,
736 ) -> R
737 where
738 R: Send,
739 F: for<'scope> FnOnce(Batches<'scope, Self>) -> R + Send,
740 {
741 match self.try_run_batches(len, minimum_batch_len, multiplier, |batches| {
742 Ok::<_, Infallible>(run(batches))
743 }) {
744 Ok(result) => result,
745 Err(e) => match e {},
746 }
747 }
748
749 /// Like [`run_batches`](Self::run_batches), but for fallible work.
750 ///
751 /// Adaptive strategies record elapsed time only when the complete operation succeeds.
752 #[track_caller]
753 fn try_run_batches<R, E, F>(
754 &self,
755 len: usize,
756 _minimum_batch_len: NonZeroUsize,
757 _multiplier: usize,
758 run: F,
759 ) -> Result<R, E>
760 where
761 R: Send,
762 E: Send,
763 F: for<'scope> FnOnce(Batches<'scope, Self>) -> Result<R, E> + Send,
764 {
765 run(Batches::whole(self, len))
766 }
767
768 /// Run an operation on strategy-provided tiles over `len` columns.
769 ///
770 /// `grids` are the ways the operation can cut its input. Each [`Grid`] has some number of
771 /// rows that pass over the columns and a fixed cost of finishing a tile. The strategy cuts
772 /// the grid it estimates its workers will finish first, and [`Tiles::grid`] names that
773 /// grid. `run` is called once with tiles that cover every `(row, column)` cell of that grid
774 /// exactly once, so a grid without cells has no tiles. Each tile is one row over a
775 /// contiguous range of columns, supplied in pieces and then finished once.
776 ///
777 /// This suits several independent passes over the same input where finishing a tile has a
778 /// fixed cost, such as reducing per-tile state, so cutting a pass is costly. Several grids
779 /// let an operation trade passes against that cost, as when a wider digit window makes
780 /// fewer passes with more buckets to reduce.
781 ///
782 /// A serial run cuts the grid with the fewest cells, counting each row's finish, and makes
783 /// every row one tile. A parallel run shares a grid among the strategy's workers as whole
784 /// rows or as shares of cells that cut rows, and [`Tiles`] describes how it chooses and how
785 /// workers claim.
786 ///
787 /// When rows differ in cost, order them so that consecutive rows mix cheap and expensive
788 /// ones. `rows * len` must not overflow `usize` for any grid, and `grids` must not be
789 /// empty.
790 ///
791 /// `multiplier` estimates work per cell. Complete the operation, including result assembly,
792 /// inside `run`. Both execution shapes must produce equivalent results. The default
793 /// implementation makes every row of the cheapest serial grid one tile on the calling
794 /// thread.
795 ///
796 /// # Panics
797 ///
798 /// Panics if `grids` is empty, or if a parallel run cuts a grid whose `rows * len`
799 /// overflows `usize`.
800 ///
801 /// # Examples
802 ///
803 /// ```
804 /// use commonware_parallel::{Grid, Sequential, Strategy};
805 /// use core::num::NonZeroUsize;
806 ///
807 /// let rows = [[1u64, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]];
808 /// let grid = Grid {
809 /// rows: rows.len(),
810 /// tile_cost: NonZeroUsize::MIN,
811 /// };
812 /// let sums = Sequential.run_tiles(4, &[grid], 1, |tiles| {
813 /// let partials = tiles.fill_collect_vec(
814 /// || 0u64,
815 /// |sum, row, columns| *sum += rows[row][columns].iter().sum::<u64>(),
816 /// |sum, row| (row, core::mem::take(sum)),
817 /// );
818 /// let mut sums = [0u64; 3];
819 /// for (row, partial) in partials {
820 /// sums[row] += partial;
821 /// }
822 /// sums
823 /// });
824 /// assert_eq!(sums, [10, 26, 42]);
825 /// ```
826 #[track_caller]
827 fn run_tiles<R, F>(
828 &self,
829 len: usize,
830 grids: &[Grid],
831 _multiplier: usize,
832 run: F,
833 ) -> R
834 where
835 R: Send,
836 F: for<'scope> FnOnce(Tiles<'scope, Self>) -> R + Send,
837 {
838 run(Tiles::serial(self, grids, len))
839 }
840
841 /// Reduces a collection to a single value with per-partition initialization.
842 ///
843 /// Similar to [`fold`](Self::fold), but provides a separate initialization value
844 /// that is created once per partition. This is useful when the fold operation
845 /// requires mutable state that should not be shared across partitions (e.g., a
846 /// scratch buffer, RNG, or expensive-to-clone resource).
847 ///
848 /// # Arguments
849 ///
850 /// - `iter`: The collection to fold over
851 /// - `init`: Creates the per-partition initialization value
852 /// - `identity`: Creates the identity value for the accumulator
853 /// - `fold_op`: Combines accumulator with init state and item: `(acc, &mut init, item) -> acc`
854 /// - `reduce_op`: Combines two accumulators: `(acc1, acc2) -> acc`
855 ///
856 /// # Examples
857 ///
858 /// ```
859 /// use commonware_parallel::{Strategy, Sequential};
860 ///
861 /// let strategy = Sequential;
862 /// let data = vec![1u32, 2, 3, 4, 5];
863 ///
864 /// // Use a scratch buffer to avoid allocations in the inner loop
865 /// let result: Vec<String> = strategy.fold_init(
866 /// &data,
867 /// || String::with_capacity(16), // Per-partition scratch buffer
868 /// Vec::new, // Identity for accumulator
869 /// |mut acc, buf, &n| {
870 /// buf.clear();
871 /// use std::fmt::Write;
872 /// write!(buf, "num:{}", n).unwrap();
873 /// acc.push(buf.clone());
874 /// acc
875 /// },
876 /// |mut a, b| { a.extend(b); a },
877 /// );
878 ///
879 /// assert_eq!(result, vec!["num:1", "num:2", "num:3", "num:4", "num:5"]);
880 /// ```
881 #[track_caller]
882 fn fold_init<I, INIT, T, R, ID, F, RD>(
883 &self,
884 iter: I,
885 init: INIT,
886 identity: ID,
887 fold_op: F,
888 reduce_op: RD,
889 ) -> R
890 where
891 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
892 INIT: Fn() -> T + Send + Sync,
893 T: Send,
894 R: Send,
895 ID: Fn() -> R + Send + Sync,
896 F: Fn(R, &mut T, I::Item) -> R + Send + Sync,
897 RD: Fn(R, R) -> R + Send + Sync;
898
899 /// Reduces a collection to a single value using fold and reduce operations.
900 ///
901 /// This method processes elements from the iterator, combining them into a single
902 /// result.
903 ///
904 /// # Arguments
905 ///
906 /// - `iter`: The collection to fold over
907 /// - `identity`: A closure that produces the identity value for the fold.
908 /// - `fold_op`: Combines an accumulator with a single item: `(acc, item) -> acc`
909 /// - `reduce_op`: Combines two accumulators: `(acc1, acc2) -> acc`.
910 ///
911 /// # Examples
912 ///
913 /// ## Sum of Elements
914 ///
915 /// ```
916 /// use commonware_parallel::{Strategy, Sequential};
917 ///
918 /// let strategy = Sequential;
919 /// let numbers = vec![1, 2, 3, 4, 5];
920 ///
921 /// let sum = strategy.fold(
922 /// &numbers,
923 /// || 0, // identity
924 /// |acc, &n| acc + n, // fold: add each number
925 /// |a, b| a + b, // reduce: combine partial sums
926 /// );
927 ///
928 /// assert_eq!(sum, 15);
929 /// ```
930 #[track_caller]
931 fn fold<I, R, ID, F, RD>(&self, iter: I, identity: ID, fold_op: F, reduce_op: RD) -> R
932 where
933 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
934 R: Send,
935 ID: Fn() -> R + Send + Sync,
936 F: Fn(R, I::Item) -> R + Send + Sync,
937 RD: Fn(R, R) -> R + Send + Sync,
938 {
939 self.fold_init(
940 iter,
941 || (),
942 identity,
943 |acc, _, item| fold_op(acc, item),
944 reduce_op,
945 )
946 }
947
948 /// Reduces a collection to a single value using a fallible fold operation.
949 ///
950 /// Similar to [`fold`](Self::fold), but `fold_op` may fail. Implementations may stop
951 /// applying `fold_op` after an error is observed. When more than one partition fails,
952 /// any error may be returned.
953 ///
954 /// Adaptive strategies must only record elapsed time when the fold succeeds, so
955 /// abort-early error paths cannot poison the policy's estimates.
956 ///
957 /// # Arguments
958 ///
959 /// - `iter`: The collection to fold over
960 /// - `identity`: A closure that produces the identity value for the fold.
961 /// - `fold_op`: Fallibly combines an accumulator with a single item: `(acc, item) -> Result<acc, E>`
962 /// - `reduce_op`: Combines two successful accumulators: `(acc1, acc2) -> acc`.
963 #[track_caller]
964 fn try_fold<I, R, E, ID, F, RD>(
965 &self,
966 iter: I,
967 identity: ID,
968 fold_op: F,
969 reduce_op: RD,
970 ) -> Result<R, E>
971 where
972 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
973 R: Send,
974 E: Send,
975 ID: Fn() -> R + Send + Sync,
976 F: Fn(R, I::Item) -> Result<R, E> + Send + Sync,
977 RD: Fn(R, R) -> R + Send + Sync;
978
979 /// Maps each element and collects results into a `Vec`.
980 ///
981 /// This is a convenience method that applies `map_op` to each element and
982 /// collects the results. For [`Sequential`], elements are processed in order.
983 /// For [`Rayon`], elements may be processed out of order but the final
984 /// vector preserves the original ordering.
985 ///
986 /// # Arguments
987 ///
988 /// - `iter`: The collection to map over
989 /// - `map_op`: The mapping function to apply to each element
990 ///
991 /// # Examples
992 ///
993 /// ```
994 /// use commonware_parallel::{Strategy, Sequential};
995 ///
996 /// let strategy = Sequential;
997 /// let data = vec![1, 2, 3, 4, 5];
998 ///
999 /// let squared: Vec<i32> = strategy.map_collect_vec(&data, |&x| x * x);
1000 /// assert_eq!(squared, vec![1, 4, 9, 16, 25]);
1001 /// ```
1002 #[track_caller]
1003 fn map_collect_vec<I, F, T>(&self, iter: I, map_op: F) -> Vec<T>
1004 where
1005 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1006 F: Fn(I::Item) -> T + Send + Sync,
1007 T: Send,
1008 {
1009 self.fold(
1010 iter,
1011 Vec::new,
1012 |mut acc, item| {
1013 acc.push(map_op(item));
1014 acc
1015 },
1016 |mut a, b| {
1017 a.extend(b);
1018 a
1019 },
1020 )
1021 }
1022
1023 /// Maps each element with a fallible operation and collects results into a `Vec`.
1024 ///
1025 /// This is a convenience method that applies `map_op` to each element and
1026 /// collects the results into a single `Result`. Output ordering on success
1027 /// matches [`map_collect_vec`](Self::map_collect_vec). Implementations may stop
1028 /// applying `map_op` after an error is observed. When more than one element
1029 /// fails, any error may be returned.
1030 ///
1031 /// # Arguments
1032 ///
1033 /// - `iter`: The collection to map over
1034 /// - `map_op`: The fallible mapping function to apply to each element
1035 ///
1036 /// # Examples
1037 ///
1038 /// ```
1039 /// use commonware_parallel::{Strategy, Sequential};
1040 ///
1041 /// let strategy = Sequential;
1042 /// let data = vec![1, 2, 3, 4, 5];
1043 ///
1044 /// let squared: Result<Vec<i32>, ()> = strategy.try_map_collect_vec(
1045 /// &data,
1046 /// |&x| Ok(x * x),
1047 /// );
1048 /// assert_eq!(squared, Ok(vec![1, 4, 9, 16, 25]));
1049 /// ```
1050 #[track_caller]
1051 fn try_map_collect_vec<I, F, T, E>(&self, iter: I, map_op: F) -> Result<Vec<T>, E>
1052 where
1053 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1054 F: Fn(I::Item) -> Result<T, E> + Send + Sync,
1055 T: Send,
1056 E: Send,
1057 {
1058 self.try_fold(
1059 iter,
1060 Vec::new,
1061 |mut acc, item| {
1062 acc.push(map_op(item)?);
1063 Ok(acc)
1064 },
1065 |mut a, b| {
1066 a.extend(b);
1067 a
1068 },
1069 )
1070 }
1071
1072 /// Maps each element with per-partition state and collects results into a `Vec`.
1073 ///
1074 /// Combines [`map_collect_vec`](Self::map_collect_vec) with per-partition
1075 /// initialization like [`fold_init`](Self::fold_init). Useful when the mapping
1076 /// operation requires mutable state that should not be shared across partitions.
1077 ///
1078 /// # Arguments
1079 ///
1080 /// - `iter`: The collection to map over
1081 /// - `init`: Creates the per-partition initialization value
1082 /// - `map_op`: The mapping function: `(&mut init, item) -> result`
1083 ///
1084 /// # Examples
1085 ///
1086 /// ```
1087 /// use commonware_parallel::{Strategy, Sequential};
1088 ///
1089 /// let strategy = Sequential;
1090 /// let data = vec![1, 2, 3, 4, 5];
1091 ///
1092 /// // Use a counter that tracks position within each partition
1093 /// let indexed: Vec<(usize, i32)> = strategy.map_init_collect_vec(
1094 /// &data,
1095 /// || 0usize, // Per-partition counter
1096 /// |counter, &x| {
1097 /// let idx = *counter;
1098 /// *counter += 1;
1099 /// (idx, x * 2)
1100 /// },
1101 /// );
1102 ///
1103 /// assert_eq!(indexed, vec![(0, 2), (1, 4), (2, 6), (3, 8), (4, 10)]);
1104 /// ```
1105 #[track_caller]
1106 fn map_init_collect_vec<I, INIT, T, F, R>(&self, iter: I, init: INIT, map_op: F) -> Vec<R>
1107 where
1108 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1109 INIT: Fn() -> T + Send + Sync,
1110 T: Send,
1111 F: Fn(&mut T, I::Item) -> R + Send + Sync,
1112 R: Send,
1113 {
1114 self.fold_init(
1115 iter,
1116 init,
1117 Vec::new,
1118 |mut acc, init_val, item| {
1119 acc.push(map_op(init_val, item));
1120 acc
1121 },
1122 |mut a, b| {
1123 a.extend(b);
1124 a
1125 },
1126 )
1127 }
1128
1129 /// Maps each element with per-partition state and a per-item work multiplier.
1130 #[track_caller]
1131 fn map_init_collect_vec_with_multiplier<I, INIT, T, F, R>(
1132 &self,
1133 iter: I,
1134 _multiplier: usize,
1135 init: INIT,
1136 map_op: F,
1137 ) -> Vec<R>
1138 where
1139 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1140 INIT: Fn() -> T + Send + Sync,
1141 T: Send,
1142 F: Fn(&mut T, I::Item) -> R + Send + Sync,
1143 R: Send,
1144 {
1145 self.map_init_collect_vec(iter, init, map_op)
1146 }
1147
1148 /// Maps each element with a per-item work multiplier.
1149 ///
1150 /// Convenience over
1151 /// [`map_init_collect_vec_with_multiplier`](Self::map_init_collect_vec_with_multiplier)
1152 /// for stateless map operations.
1153 #[track_caller]
1154 fn map_collect_vec_with_multiplier<I, F, R>(
1155 &self,
1156 iter: I,
1157 multiplier: usize,
1158 map_op: F,
1159 ) -> Vec<R>
1160 where
1161 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1162 F: Fn(I::Item) -> R + Send + Sync,
1163 R: Send,
1164 {
1165 self.map_init_collect_vec_with_multiplier(iter, multiplier, || (), |_, item| {
1166 map_op(item)
1167 })
1168 }
1169
1170 /// Maps each element, filtering out `None` results and tracking their keys.
1171 ///
1172 /// This is a convenience method that applies `map_op` to each element. The
1173 /// closure returns `(key, Option<value>)`. Elements where the option is `Some`
1174 /// have their values collected into the first vector. Elements where the option
1175 /// is `None` have their keys collected into the second vector.
1176 ///
1177 /// # Arguments
1178 ///
1179 /// - `iter`: The collection to map over
1180 /// - `map_op`: The mapping function returning `(K, Option<U>)`
1181 ///
1182 /// # Returns
1183 ///
1184 /// A tuple of `(results, filtered_keys)` where:
1185 /// - `results`: Values from successful mappings (where `map_op` returned `Some`)
1186 /// - `filtered_keys`: Keys where `map_op` returned `None`
1187 ///
1188 /// # Examples
1189 ///
1190 /// ```
1191 /// use commonware_parallel::{Strategy, Sequential};
1192 ///
1193 /// let strategy = Sequential;
1194 /// let data = vec![1, 2, 3, 4, 5];
1195 ///
1196 /// let (evens, odd_values): (Vec<i32>, Vec<i32>) = strategy.map_partition_collect_vec(
1197 /// data.iter(),
1198 /// |&x| (x, if x % 2 == 0 { Some(x * 10) } else { None }),
1199 /// );
1200 ///
1201 /// assert_eq!(evens, vec![20, 40]);
1202 /// assert_eq!(odd_values, vec![1, 3, 5]);
1203 /// ```
1204 #[track_caller]
1205 fn map_partition_collect_vec<I, F, K, U>(&self, iter: I, map_op: F) -> (Vec<U>, Vec<K>)
1206 where
1207 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1208 F: Fn(I::Item) -> (K, Option<U>) + Send + Sync,
1209 K: Send,
1210 U: Send,
1211 {
1212 self.fold(
1213 iter,
1214 || (Vec::new(), Vec::new()),
1215 |(mut results, mut filtered), item| {
1216 let (key, value) = map_op(item);
1217 match value {
1218 Some(v) => results.push(v),
1219 None => filtered.push(key),
1220 }
1221 (results, filtered)
1222 },
1223 |(mut r1, mut f1), (r2, f2)| {
1224 r1.extend(r2);
1225 f1.extend(f2);
1226 (r1, f1)
1227 },
1228 )
1229 }
1230
1231 /// Executes two closures, potentially in parallel, and returns both results.
1232 ///
1233 /// For [`Sequential`], this executes `a` then `b` on the current thread.
1234 /// For [`Rayon`], this executes `a` and `b` on its thread pool.
1235 ///
1236 /// # Arguments
1237 ///
1238 /// - `a`: First closure to execute
1239 /// - `b`: Second closure to execute
1240 ///
1241 /// # Examples
1242 ///
1243 /// ```
1244 /// use commonware_parallel::{Strategy, Sequential};
1245 ///
1246 /// let strategy = Sequential;
1247 ///
1248 /// let (sum, product) = strategy.join(
1249 /// || (1..=5).sum::<i32>(),
1250 /// || (1..=5).product::<i32>(),
1251 /// );
1252 ///
1253 /// assert_eq!(sum, 15);
1254 /// assert_eq!(product, 120);
1255 /// ```
1256 fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
1257 where
1258 A: FnOnce() -> RA + Send,
1259 B: FnOnce() -> RB + Send,
1260 RA: Send,
1261 RB: Send;
1262
1263 /// Sorts a slice with a comparator, preserving the order of equal elements.
1264 ///
1265 /// # Examples
1266 ///
1267 /// ```
1268 /// use commonware_parallel::{Strategy, Sequential};
1269 ///
1270 /// let strategy = Sequential;
1271 /// let mut data = vec![3, 1, 2];
1272 /// strategy.sort_by(&mut data, |a, b| a.cmp(b));
1273 /// assert_eq!(data, vec![1, 2, 3]);
1274 /// ```
1275 #[track_caller]
1276 fn sort_by<T, C>(&self, items: &mut [T], compare: C)
1277 where
1278 T: Send,
1279 C: Fn(&T, &T) -> Ordering + Send + Sync;
1280
1281 /// Sorts a slice with a comparator, without preserving the order of equal elements.
1282 ///
1283 /// Prefer it to [`sort_by`](Self::sort_by) when equal elements are indistinguishable or
1284 /// absent. Leaving their order unspecified lets a strategy choose a faster algorithm.
1285 ///
1286 /// # Examples
1287 ///
1288 /// ```
1289 /// use commonware_parallel::{Strategy, Sequential};
1290 ///
1291 /// let strategy = Sequential;
1292 /// let mut data = vec![3, 1, 2];
1293 /// strategy.sort_unstable_by(&mut data, |a, b| a.cmp(b));
1294 /// assert_eq!(data, vec![1, 2, 3]);
1295 /// ```
1296 #[track_caller]
1297 fn sort_unstable_by<T, C>(&self, items: &mut [T], compare: C)
1298 where
1299 T: Send,
1300 C: Fn(&T, &T) -> Ordering + Send + Sync,
1301 {
1302 self.sort_by(items, compare);
1303 }
1304 }
1305
1306 /// A sequential execution strategy.
1307 ///
1308 /// This strategy executes all operations on the current thread without any
1309 /// parallelism. It is useful for:
1310 ///
1311 /// - Debugging and testing (deterministic execution)
1312 /// - `no_std` environments where threading is unavailable
1313 /// - Small workloads where parallelism overhead exceeds benefits
1314 /// - Comparing sequential vs parallel performance
1315 ///
1316 /// # Examples
1317 ///
1318 /// ```
1319 /// use commonware_parallel::{Strategy, Sequential};
1320 ///
1321 /// let strategy = Sequential;
1322 /// let data = vec![1, 2, 3, 4, 5];
1323 ///
1324 /// let sum = strategy.fold(&data, || 0, |a, &b| a + b, |a, b| a + b);
1325 /// assert_eq!(sum, 15);
1326 /// ```
1327 #[derive(Default, Debug, Clone)]
1328 pub struct Sequential;
1329
1330 impl Strategy for Sequential {
1331 fn spawn<F, T>(
1332 &self,
1333 _len: usize,
1334 f: F,
1335 ) -> impl core::future::Future<Output = T> + Send + 'static
1336 where
1337 F: FnOnce(Self) -> T + Send + 'static,
1338 T: Send + 'static,
1339 {
1340 let result = f(self.clone());
1341 async move { result }
1342 }
1343
1344 fn run<R, SEQ, PAR>(&self, _len: usize, serial: SEQ, _parallel: PAR) -> R
1345 where
1346 R: Send,
1347 SEQ: FnOnce() -> R + Send,
1348 PAR: FnOnce() -> R + Send,
1349 {
1350 serial()
1351 }
1352
1353 fn try_run<R, E, SEQ, PAR>(&self, _len: usize, serial: SEQ, _parallel: PAR) -> Result<R, E>
1354 where
1355 R: Send,
1356 E: Send,
1357 SEQ: FnOnce() -> Result<R, E> + Send,
1358 PAR: FnOnce() -> Result<R, E> + Send,
1359 {
1360 serial()
1361 }
1362
1363 fn fold_init<I, INIT, T, R, ID, F, RD>(
1364 &self,
1365 iter: I,
1366 init: INIT,
1367 identity: ID,
1368 fold_op: F,
1369 _reduce_op: RD,
1370 ) -> R
1371 where
1372 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1373 INIT: Fn() -> T + Send + Sync,
1374 T: Send,
1375 R: Send,
1376 ID: Fn() -> R + Send + Sync,
1377 F: Fn(R, &mut T, I::Item) -> R + Send + Sync,
1378 RD: Fn(R, R) -> R + Send + Sync,
1379 {
1380 let mut init_val = init();
1381 iter.into_iter()
1382 .fold(identity(), |acc, item| fold_op(acc, &mut init_val, item))
1383 }
1384
1385 fn try_fold<I, R, E, ID, F, RD>(
1386 &self,
1387 iter: I,
1388 identity: ID,
1389 fold_op: F,
1390 _reduce_op: RD,
1391 ) -> Result<R, E>
1392 where
1393 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1394 R: Send,
1395 E: Send,
1396 ID: Fn() -> R + Send + Sync,
1397 F: Fn(R, I::Item) -> Result<R, E> + Send + Sync,
1398 RD: Fn(R, R) -> R + Send + Sync,
1399 {
1400 iter.into_iter().try_fold(identity(), fold_op)
1401 }
1402
1403 fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
1404 where
1405 A: FnOnce() -> RA + Send,
1406 B: FnOnce() -> RB + Send,
1407 RA: Send,
1408 RB: Send,
1409 {
1410 (a(), b())
1411 }
1412
1413 fn sort_by<T, C>(&self, items: &mut [T], compare: C)
1414 where
1415 T: Send,
1416 C: Fn(&T, &T) -> Ordering + Send + Sync,
1417 {
1418 items.sort_by(compare);
1419 }
1420
1421 fn sort_unstable_by<T, C>(&self, items: &mut [T], compare: C)
1422 where
1423 T: Send,
1424 C: Fn(&T, &T) -> Ordering + Send + Sync,
1425 {
1426 items.sort_unstable_by(compare);
1427 }
1428 }
1429});
1430commonware_macros::stability_scope!(BETA, cfg(any(feature = "std", test)) {
1431 /// A shared [`rayon::ThreadPool`].
1432 pub type ThreadPool = Arc<rayon::ThreadPool>;
1433
1434 /// A parallel execution strategy backed by a Commonware thread pool.
1435 ///
1436 /// This strategy adaptively executes collection operations serially or through its backing
1437 /// pool. It records wall-clock estimates by callsite, input-size and work-size buckets, and
1438 /// planning parallelism so small inputs can avoid thread-pool scheduling overhead without
1439 /// disabling parallel execution for larger inputs.
1440 ///
1441 /// Nested calls at the same callsite share estimates across parent paths, so use distinct
1442 /// callsites (propagating `#[track_caller]` through helpers as needed) to tune them separately.
1443 ///
1444 /// # Thread Pool Ownership
1445 ///
1446 /// `Rayon` shares its [`rayon::ThreadPool`] through an [`Arc`], so it can be cheaply cloned and
1447 /// shared across threads. Multiple strategies can share the same pool.
1448 ///
1449 /// # When to Use
1450 ///
1451 /// Use `Rayon` when:
1452 ///
1453 /// - Processing large collections where parallelism overhead is justified
1454 /// - The fold/reduce operations are CPU-bound
1455 /// - You want to utilize multiple cores
1456 ///
1457 /// Consider [`Sequential`] instead when:
1458 ///
1459 /// - The collection is small
1460 /// - Operations are I/O-bound rather than CPU-bound
1461 /// - Deterministic execution order is required for debugging
1462 ///
1463 /// # Examples
1464 ///
1465 /// ```rust
1466 /// use commonware_parallel::{Strategy, Rayon};
1467 /// use std::num::NonZeroUsize;
1468 ///
1469 /// let strategy = Rayon::new(NonZeroUsize::new(2).unwrap()).unwrap();
1470 ///
1471 /// let data: Vec<i64> = (0..1000).collect();
1472 /// let sum = strategy.fold(&data, || 0i64, |acc, &n| acc + n, |a, b| a + b);
1473 /// assert_eq!(sum, 499500);
1474 /// ```
1475 #[derive(Debug, Clone)]
1476 pub struct Rayon {
1477 // The pool that runs parallel work.
1478 thread_pool: ThreadPool,
1479 // The parallelism assumed for policy decisions and for planning batches and tiles.
1480 // Defaults to the pool's thread count.
1481 parallelism: usize,
1482 // `Some` enables adaptive serial-vs-parallel decisions. `None` runs the parallel body
1483 // whenever the parallelism exceeds one and allocates no policy state.
1484 policy: Option<policy::Policy>,
1485 }
1486
1487 impl Rayon {
1488 /// Creates a [`Rayon`] strategy with a [`ThreadPool`] that is configured with the given
1489 /// number of threads.
1490 pub fn new(num_threads: NonZeroUsize) -> Result<Self, ThreadPoolBuildError> {
1491 ThreadPoolBuilder::new()
1492 .num_threads(num_threads.get())
1493 .build()
1494 .map(|pool| Self::with_pool(Arc::new(pool)))
1495 }
1496
1497 /// Creates a new [`Rayon`] strategy with the given [`ThreadPool`].
1498 ///
1499 /// Build the pool with [`ThreadPoolBuilder`] and wrap it in an [`Arc`].
1500 pub fn with_pool(thread_pool: ThreadPool) -> Self {
1501 let parallelism = thread_pool.current_num_threads().max(1);
1502 Self {
1503 thread_pool,
1504 parallelism,
1505 policy: Some(policy::Policy::default()),
1506 }
1507 }
1508
1509 /// Overrides the parallelism assumed for planning decisions.
1510 ///
1511 /// This does not resize the backing pool. By default a strategy plans with the pool's
1512 /// thread count; override it when the strategy should expose a different parallelism
1513 /// (e.g. a runtime that executes strategy work inline on a single thread).
1514 pub const fn with_parallelism(mut self, parallelism: NonZeroUsize) -> Self {
1515 self.parallelism = parallelism.get();
1516 self
1517 }
1518
1519 /// Returns a copy of this strategy without a policy, for dispatching the batches or tiles
1520 /// of a run whose shape the policy has already chosen.
1521 fn fixed(&self) -> Self {
1522 Self {
1523 thread_pool: self.thread_pool.clone(),
1524 parallelism: self.parallelism,
1525 policy: None,
1526 }
1527 }
1528
1529 /// Runs [`Self::try_execute`] with an infallible `run`.
1530 #[track_caller]
1531 fn execute<R>(
1532 &self,
1533 len: usize,
1534 work: usize,
1535 run: impl FnOnce(policy::RunExecution) -> R,
1536 ) -> R {
1537 match self.try_execute(len, work, |execution| {
1538 Ok::<_, Infallible>(run(execution))
1539 }) {
1540 Ok(result) => result,
1541 Err(e) => match e {},
1542 }
1543 }
1544
1545 /// Runs `run` on the execution path the policy prefers for this call site's `len` items and
1546 /// estimated `work`.
1547 #[track_caller]
1548 fn try_execute<R, E>(
1549 &self,
1550 len: usize,
1551 work: usize,
1552 run: impl FnOnce(policy::RunExecution) -> Result<R, E>,
1553 ) -> Result<R, E> {
1554 let Some(policy) = &self.policy else {
1555 let execution = if self.parallelism <= 1 {
1556 policy::RunExecution::Serial
1557 } else {
1558 policy::RunExecution::Parallel
1559 };
1560 return run(execution);
1561 };
1562
1563 policy.try_run(Location::caller(), len, work, self.parallelism, run)
1564 }
1565 }
1566
1567 impl Strategy for Rayon {
1568 /// A pool worker that awaits the result runs queued jobs on its own stack while it waits,
1569 /// so blocking waits inside jobs must allow nesting. For example,
1570 /// `futures::executor::block_on` panics if a nested job calls it on a thread that is
1571 /// already inside it.
1572 #[track_caller]
1573 fn spawn<F, T>(
1574 &self,
1575 len: usize,
1576 f: F,
1577 ) -> impl core::future::Future<Output = T> + Send + 'static
1578 where
1579 F: FnOnce(Self) -> T + Send + 'static,
1580 T: Send + 'static,
1581 {
1582 let threads = self.thread_pool.current_num_threads();
1583 let caller = Location::caller();
1584
1585 // A single-worker pool cannot overlap a hand-off, so the job always runs inline,
1586 // untimed. A strategy without a policy always hands the job off. Otherwise the policy
1587 // weighs the measured job cost against the offload round trip.
1588 let ((execution, measure), policy) = if threads <= 1 {
1589 ((policy::SpawnExecution::Inline, false), None)
1590 } else {
1591 self.policy.as_ref().map_or(
1592 ((policy::SpawnExecution::Offload, false), None),
1593 |policy| (policy.choose_spawn(caller, len, threads), Some(policy)),
1594 )
1595 };
1596
1597 match execution {
1598 policy::SpawnExecution::Inline => {
1599 // Inline: run on the calling task and hand back a ready future.
1600 let start = measure.then(Instant::now);
1601 let result = f(self.clone());
1602 if let (Some(start), Some(policy)) = (start, policy) {
1603 policy.record_spawn_inline(caller, len, threads, start.elapsed());
1604 }
1605 Either::Left(future::ready(result))
1606 }
1607 policy::SpawnExecution::Offload => {
1608 // Offload: hand the job to the pool. The worker records the job wall (so
1609 // job estimates survive a dropped future), and the awaiting future records
1610 // the round-trip overhead when it observes the result.
1611 let spawn_start = measure.then(Instant::now);
1612 let (tx, mut rx) = oneshot::channel();
1613 let s = self.clone();
1614 let pool = self.thread_pool.clone();
1615 let recorder = if measure {
1616 policy.cloned().map(|policy| (policy, caller, len, threads))
1617 } else {
1618 None
1619 };
1620 let worker_recorder = recorder.clone();
1621 self.thread_pool.spawn(move || {
1622 let job_start = worker_recorder.is_some().then(Instant::now);
1623
1624 // The pool aborts on an uncaught panic in a spawned job, so catch the
1625 // panic and propagate it to the awaiting task.
1626 let result = panic::catch_unwind(AssertUnwindSafe(|| f(s)));
1627 let job = job_start.map(|start| start.elapsed());
1628 let ok = result.is_ok();
1629 let _ = tx.send((result, job));
1630
1631 // Record successful runs only, matching the inline arm: a panicked job's
1632 // wall time says nothing about the job size. Recording after the send
1633 // keeps the bookkeeping off the caller's wake path.
1634 if ok
1635 && let (Some((policy, caller, len, threads)), Some(job)) =
1636 (worker_recorder, job)
1637 {
1638 policy.record_spawn_job(caller, len, threads, job);
1639 }
1640 });
1641 Either::Right(async move {
1642 // Each poll checks membership because the future can move between threads.
1643 let payload = future::poll_fn(|cx| {
1644 if let Poll::Ready(payload) = rx.poll_unpin(cx) {
1645 return Poll::Ready(payload);
1646 }
1647 pool.block_on_worker(Pin::new(&mut rx))
1648 .map_or(Poll::Pending, Poll::Ready)
1649 })
1650 .await;
1651 let (result, job) = payload.unwrap_or_else(|_| {
1652 panic!("strategy job must not be dropped before completion")
1653 });
1654 match result {
1655 Ok(value) => {
1656 // The round trip is everything around the job itself: hand-off
1657 // setup, queueing, worker wake, result send, task wake, and this
1658 // poll. A late poll inflates the sample with overlap slack, which
1659 // only ever biases toward inline, and the policy's budget caps
1660 // what that bias can buy.
1661 if let (
1662 Some((policy, caller, len, threads)),
1663 Some(job),
1664 Some(start),
1665 ) = (recorder, job, spawn_start)
1666 {
1667 policy.record_spawn_overhead(
1668 caller,
1669 len,
1670 threads,
1671 start.elapsed().saturating_sub(job),
1672 );
1673 }
1674 value
1675 }
1676 Err(payload) => panic::resume_unwind(payload),
1677 }
1678 })
1679 }
1680 }
1681 }
1682
1683 #[track_caller]
1684 fn run<R, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> R
1685 where
1686 R: Send,
1687 SEQ: FnOnce() -> R + Send,
1688 PAR: FnOnce() -> R + Send,
1689 {
1690 self.execute(len, len, |execution| match execution {
1691 policy::RunExecution::Serial => serial(),
1692 policy::RunExecution::Parallel => parallel(),
1693 })
1694 }
1695
1696 #[track_caller]
1697 fn try_run<R, E, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> Result<R, E>
1698 where
1699 R: Send,
1700 E: Send,
1701 SEQ: FnOnce() -> Result<R, E> + Send,
1702 PAR: FnOnce() -> Result<R, E> + Send,
1703 {
1704 self.try_execute(len, len, |execution| match execution {
1705 policy::RunExecution::Serial => serial(),
1706 policy::RunExecution::Parallel => parallel(),
1707 })
1708 }
1709
1710 #[track_caller]
1711 fn try_run_batches<R, E, F>(
1712 &self,
1713 len: usize,
1714 minimum_batch_len: NonZeroUsize,
1715 multiplier: usize,
1716 run: F,
1717 ) -> Result<R, E>
1718 where
1719 R: Send,
1720 E: Send,
1721 F: for<'scope> FnOnce(Batches<'scope, Self>) -> Result<R, E> + Send,
1722 {
1723 let count = self.parallelism.min(len / minimum_batch_len.get());
1724 if count < 2 {
1725 return run(Batches::whole(self, len));
1726 }
1727 self.try_execute(len, len.saturating_mul(multiplier), |execution| match execution {
1728 policy::RunExecution::Serial => run(Batches::whole(self, len)),
1729 policy::RunExecution::Parallel => {
1730 let fixed = self.fixed();
1731 run(Batches::balanced(&fixed, len, count))
1732 }
1733 })
1734 }
1735
1736 #[track_caller]
1737 fn run_tiles<R, F>(
1738 &self,
1739 len: usize,
1740 grids: &[Grid],
1741 multiplier: usize,
1742 run: F,
1743 ) -> R
1744 where
1745 R: Send,
1746 F: for<'scope> FnOnce(Tiles<'scope, Self>) -> R + Send,
1747 {
1748 // The policy picks the shape from the cheapest serial grid's cells. A parallel run
1749 // dispatches its shares through a copy without a policy, since a policy would see only
1750 // one item per share and could run them serially.
1751 let (_, cells, _) = Grid::choose(grids, len, 1);
1752 let work = cells.saturating_mul(multiplier);
1753 self.execute(len, work, |execution| match execution {
1754 policy::RunExecution::Serial => run(Tiles::serial(self, grids, len)),
1755 policy::RunExecution::Parallel => {
1756 let fixed = self.fixed();
1757 run(Tiles::parallel(&fixed, grids, len, self.parallelism))
1758 }
1759 })
1760 }
1761
1762 #[track_caller]
1763 fn fold_init<I, INIT, T, R, ID, F, RD>(
1764 &self,
1765 iter: I,
1766 init: INIT,
1767 identity: ID,
1768 fold_op: F,
1769 reduce_op: RD,
1770 ) -> R
1771 where
1772 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1773 INIT: Fn() -> T + Send + Sync,
1774 T: Send,
1775 R: Send,
1776 ID: Fn() -> R + Send + Sync,
1777 F: Fn(R, &mut T, I::Item) -> R + Send + Sync,
1778 RD: Fn(R, R) -> R + Send + Sync,
1779 {
1780 let items: Vec<I::Item> = iter.into_iter().collect();
1781 self.execute(items.len(), items.len(), |execution| match execution {
1782 policy::RunExecution::Serial => {
1783 Sequential.fold_init(items, init, identity, fold_op, reduce_op)
1784 }
1785 policy::RunExecution::Parallel => self.thread_pool.install(|| {
1786 rayon::fold_init(items, &init, &identity, &fold_op, &reduce_op)
1787 }),
1788 })
1789 }
1790
1791 #[track_caller]
1792 fn map_collect_vec<I, F, T>(&self, iter: I, map_op: F) -> Vec<T>
1793 where
1794 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1795 F: Fn(I::Item) -> T + Send + Sync,
1796 T: Send,
1797 {
1798 let items: Vec<I::Item> = iter.into_iter().collect();
1799 self.execute(items.len(), items.len(), |execution| match execution {
1800 policy::RunExecution::Serial => Sequential.map_collect_vec(items, map_op),
1801 policy::RunExecution::Parallel => self
1802 .thread_pool
1803 .install(|| rayon::map_collect_vec(items, &map_op)),
1804 })
1805 }
1806
1807 #[track_caller]
1808 fn try_map_collect_vec<I, F, T, E>(&self, iter: I, map_op: F) -> Result<Vec<T>, E>
1809 where
1810 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1811 F: Fn(I::Item) -> Result<T, E> + Send + Sync,
1812 T: Send,
1813 E: Send,
1814 {
1815 let items: Vec<I::Item> = iter.into_iter().collect();
1816 self.try_execute(items.len(), items.len(), |execution| match execution {
1817 policy::RunExecution::Serial => Sequential.try_map_collect_vec(items, map_op),
1818 policy::RunExecution::Parallel => self
1819 .thread_pool
1820 .install(|| rayon::try_map_collect_vec(items, &map_op)),
1821 })
1822 }
1823
1824 #[track_caller]
1825 fn map_init_collect_vec<I, INIT, T, F, R>(&self, iter: I, init: INIT, map_op: F) -> Vec<R>
1826 where
1827 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1828 INIT: Fn() -> T + Send + Sync,
1829 T: Send,
1830 F: Fn(&mut T, I::Item) -> R + Send + Sync,
1831 R: Send,
1832 {
1833 let items: Vec<I::Item> = iter.into_iter().collect();
1834 self.execute(items.len(), items.len(), |execution| match execution {
1835 policy::RunExecution::Serial => Sequential.map_init_collect_vec(items, init, map_op),
1836 policy::RunExecution::Parallel => self
1837 .thread_pool
1838 .install(|| rayon::map_init_collect_vec(items, &init, &map_op)),
1839 })
1840 }
1841
1842 #[track_caller]
1843 fn map_init_collect_vec_with_multiplier<I, INIT, T, F, R>(
1844 &self,
1845 iter: I,
1846 multiplier: usize,
1847 init: INIT,
1848 map_op: F,
1849 ) -> Vec<R>
1850 where
1851 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1852 INIT: Fn() -> T + Send + Sync,
1853 T: Send,
1854 F: Fn(&mut T, I::Item) -> R + Send + Sync,
1855 R: Send,
1856 {
1857 let items: Vec<I::Item> = iter.into_iter().collect();
1858 self.execute(items.len(), items.len().saturating_mul(multiplier), |execution| match execution {
1859 policy::RunExecution::Serial => Sequential.map_init_collect_vec(items, init, map_op),
1860 policy::RunExecution::Parallel => self
1861 .thread_pool
1862 .install(|| rayon::map_init_collect_vec(items, &init, &map_op)),
1863 })
1864 }
1865
1866 #[track_caller]
1867 fn try_fold<I, R, E, ID, F, RD>(
1868 &self,
1869 iter: I,
1870 identity: ID,
1871 fold_op: F,
1872 reduce_op: RD,
1873 ) -> Result<R, E>
1874 where
1875 I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1876 R: Send,
1877 E: Send,
1878 ID: Fn() -> R + Send + Sync,
1879 F: Fn(R, I::Item) -> Result<R, E> + Send + Sync,
1880 RD: Fn(R, R) -> R + Send + Sync,
1881 {
1882 let items: Vec<I::Item> = iter.into_iter().collect();
1883 self.try_execute(items.len(), items.len(), |execution| match execution {
1884 policy::RunExecution::Serial => {
1885 Sequential.try_fold(items, identity, fold_op, reduce_op)
1886 }
1887 policy::RunExecution::Parallel => self.thread_pool.install(|| {
1888 rayon::try_fold(items, &identity, &fold_op, &reduce_op)
1889 }),
1890 })
1891 }
1892
1893 fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
1894 where
1895 A: FnOnce() -> RA + Send,
1896 B: FnOnce() -> RB + Send,
1897 RA: Send,
1898 RB: Send,
1899 {
1900 self.thread_pool.install(|| rayon::join(a, b))
1901 }
1902
1903 #[track_caller]
1904 fn sort_by<T, C>(&self, items: &mut [T], compare: C)
1905 where
1906 T: Send,
1907 C: Fn(&T, &T) -> Ordering + Send + Sync,
1908 {
1909 self.execute(items.len(), items.len(), |execution| match execution {
1910 policy::RunExecution::Serial => Sequential.sort_by(items, compare),
1911 policy::RunExecution::Parallel => {
1912 self.thread_pool.install(|| {
1913 rayon::par_mergesort(items, |a, b| compare(a, b) == Ordering::Less)
1914 })
1915 }
1916 });
1917 }
1918
1919 #[track_caller]
1920 fn sort_unstable_by<T, C>(&self, items: &mut [T], compare: C)
1921 where
1922 T: Send,
1923 C: Fn(&T, &T) -> Ordering + Send + Sync,
1924 {
1925 self.execute(items.len(), items.len(), |execution| match execution {
1926 policy::RunExecution::Serial => Sequential.sort_unstable_by(items, compare),
1927 policy::RunExecution::Parallel => {
1928 self.thread_pool.install(|| {
1929 rayon::par_quicksort(items, |a, b| compare(a, b) == Ordering::Less)
1930 })
1931 }
1932 });
1933 }
1934 }
1935});
1936commonware_macros::stability_scope!(ALPHA, cfg(any(feature = "test-utils", test)) {
1937 pub mod mocks;
1938});
1939
1940#[cfg(test)]
1941mod test {
1942 use crate::{
1943 Grid, MAX_UNITS, Rayon, Sequential, Strategy, mocks::Explorer, rayon::ThreadPoolBuilder,
1944 };
1945 use core::{num::NonZeroUsize, ops::Range};
1946 use futures::FutureExt;
1947 use proptest::prelude::*;
1948 use std::{
1949 rc::Rc,
1950 sync::{
1951 Arc,
1952 atomic::{AtomicUsize, Ordering},
1953 },
1954 time::{Duration, Instant},
1955 };
1956
1957 fn parallel_strategy() -> Rayon {
1958 Rayon::new(NonZeroUsize::new(4).unwrap()).unwrap()
1959 }
1960
1961 /// Call `spawn` with this helper so the policy entry is keyed by the helper's call
1962 /// site (both `track_caller` locations resolve to the same line).
1963 #[track_caller]
1964 fn spawn_flagged(
1965 strategy: &Rayon,
1966 panics: bool,
1967 ) -> (
1968 &'static std::panic::Location<'static>,
1969 impl core::future::Future<Output = usize> + Send + 'static,
1970 ) {
1971 (
1972 std::panic::Location::caller(),
1973 strategy.spawn(64, move |_| {
1974 if panics {
1975 panic!("job panic");
1976 }
1977 7
1978 }),
1979 )
1980 }
1981
1982 fn spawn_recorded(strategy: &Rayon, loc: &'static std::panic::Location<'static>) -> bool {
1983 let parallelism = strategy.parallelism;
1984 strategy
1985 .policy
1986 .as_ref()
1987 .is_some_and(|policy| policy.spawn_recorded(loc, 64, parallelism))
1988 }
1989
1990 /// A panicking offloaded job must not update the spawn policy: its wall time says
1991 /// nothing about the job size and would train the policy toward inlining.
1992 #[test]
1993 fn spawn_panic_records_nothing() {
1994 let strategy = parallel_strategy();
1995
1996 let (loc, job) = spawn_flagged(&strategy, true);
1997 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1998 futures::executor::block_on(job)
1999 }));
2000 assert!(result.is_err());
2001 assert!(!spawn_recorded(&strategy, loc));
2002
2003 let (loc, job) = spawn_flagged(&strategy, false);
2004 assert_eq!(futures::executor::block_on(job), 7);
2005 assert!(spawn_recorded(&strategy, loc));
2006 }
2007
2008 /// Spawn honors an inline decision after cheap job and hand-off samples are recorded.
2009 /// Caller tracking keys the recorded samples and spawn to the same policy entry.
2010 #[test]
2011 #[track_caller]
2012 fn spawn_inlines_a_sub_overhead_job() {
2013 let strategy = parallel_strategy();
2014 let policy = strategy.policy.as_ref().unwrap();
2015 let caller = std::panic::Location::caller();
2016 let threads = strategy.thread_pool.current_num_threads();
2017
2018 for _ in 0..2 {
2019 assert_eq!(
2020 policy.choose_spawn(caller, 64, threads),
2021 (crate::policy::SpawnExecution::Offload, true)
2022 );
2023 policy.record_spawn_job(caller, 64, threads, std::time::Duration::from_micros(2));
2024 policy.record_spawn_overhead(caller, 64, threads, std::time::Duration::from_micros(50));
2025 }
2026
2027 assert_eq!(
2028 futures::executor::block_on(strategy.spawn(64, |_| std::thread::current().id())),
2029 std::thread::current().id()
2030 );
2031 }
2032
2033 /// A job measured over the inline budget keeps offloading: the calling task is never blocked
2034 /// on a big job even when the hand-off looks expensive.
2035 #[test]
2036 fn spawn_keeps_offloading_big_jobs() {
2037 let strategy = parallel_strategy();
2038
2039 for _ in 0..20 {
2040 let on_pool = futures::executor::block_on(strategy.spawn(64, |_| {
2041 std::thread::sleep(std::time::Duration::from_millis(2));
2042 crate::rayon::current_thread_index().is_some()
2043 }));
2044 assert!(
2045 on_pool,
2046 "a job over the inline budget ran on the calling task"
2047 );
2048 }
2049 }
2050
2051 fn policy_len(strategy: &Rayon) -> usize {
2052 strategy.policy.as_ref().map_or(0, |policy| policy.len())
2053 }
2054
2055 /// A strategy that does not split runs the whole input once as the single range `0..len` on
2056 /// the calling thread.
2057 #[test]
2058 fn run_batches_preserves_whole_input_without_splitting() {
2059 fn check(strategy: &impl Strategy, len: usize, minimum: NonZeroUsize) {
2060 let owned = Box::new(len);
2061 let calls = AtomicUsize::new(0);
2062 let caller = std::thread::current().id();
2063 let (result, items) = strategy.run_batches(len, minimum, usize::MAX, |batches| {
2064 calls.fetch_add(1, Ordering::Relaxed);
2065 assert!(batches.is_whole());
2066 (
2067 *owned,
2068 batches.map_collect_vec(
2069 |ranges| ranges,
2070 |range| (range, std::thread::current().id()),
2071 ),
2072 )
2073 });
2074 assert_eq!(result, len);
2075 assert_eq!(items, [(0..len, caller)]);
2076 assert_eq!(calls.load(Ordering::Relaxed), 1);
2077 }
2078
2079 let one_worker = Rayon::new(NonZeroUsize::MIN).unwrap();
2080 let parallel = parallel_strategy();
2081 let minimum = NonZeroUsize::new(8).unwrap();
2082 for len in [0, 1, 7, 8, 15] {
2083 check(&Sequential, len, minimum);
2084 check(&one_worker, len, minimum);
2085 check(¶llel, len, minimum);
2086 check(¶llel.without_policy(), len, minimum);
2087 }
2088 check(&Sequential, usize::MAX, NonZeroUsize::MIN);
2089 check(&one_worker, usize::MAX, NonZeroUsize::MIN);
2090 check(¶llel, usize::MAX, NonZeroUsize::MAX);
2091 assert_eq!(policy_len(&one_worker), 0);
2092 assert_eq!(policy_len(¶llel), 0);
2093 }
2094
2095 /// A serial policy decision on a splittable extent runs the whole input as the single range
2096 /// `0..len` on the calling thread. Caller tracking keys the recorded samples and the run to
2097 /// the same policy entry.
2098 #[test]
2099 #[track_caller]
2100 fn run_batches_runs_serial_decision_on_calling_thread() {
2101 let strategy = parallel_strategy();
2102 let policy = strategy.policy.as_ref().unwrap();
2103 let caller = std::panic::Location::caller();
2104
2105 // Record samples that make serial the preferred path.
2106 policy.record_run(
2107 caller,
2108 16,
2109 16,
2110 4,
2111 crate::policy::RunExecution::Parallel,
2112 std::time::Duration::from_micros(100),
2113 );
2114 policy.record_run(
2115 caller,
2116 16,
2117 16,
2118 4,
2119 crate::policy::RunExecution::Serial,
2120 std::time::Duration::from_micros(95),
2121 );
2122
2123 // Run an extent that could form four batches.
2124 let thread = std::thread::current().id();
2125 let items = strategy.run_batches(16, NonZeroUsize::MIN, 1, |batches| {
2126 assert!(batches.is_whole());
2127 batches.map_collect_vec(
2128 |ranges| ranges,
2129 |range| (range, std::thread::current().id()),
2130 )
2131 });
2132 assert_eq!(items, [(0..16, thread)]);
2133 }
2134
2135 #[test]
2136 fn run_batches_supplies_ordered_balanced_ranges() {
2137 let strategy = Rayon::new(NonZeroUsize::MIN)
2138 .unwrap()
2139 .with_parallelism(NonZeroUsize::new(8).unwrap())
2140 .without_policy();
2141 for (len, minimum) in [
2142 (2, 1),
2143 (7, 1),
2144 (8, 1),
2145 (9, 1),
2146 (17, 1),
2147 (15, 7),
2148 (16, 8),
2149 (17, 8),
2150 (usize::MAX, 1),
2151 (usize::MAX, usize::MAX / 2),
2152 ] {
2153 let ranges = strategy.run_batches(
2154 len,
2155 NonZeroUsize::new(minimum).unwrap(),
2156 usize::MAX,
2157 |batches| {
2158 assert!(!batches.is_whole());
2159 batches.map_collect_vec(|ranges| ranges, |range| range)
2160 },
2161 );
2162 assert_eq!(ranges.len(), 8.min(len / minimum));
2163 assert_eq!(ranges.first().unwrap().start, 0);
2164 assert_eq!(ranges.last().unwrap().end, len);
2165 assert!(ranges.windows(2).all(|pair| pair[0].end == pair[1].start));
2166 assert!(ranges.iter().all(|range| range.len() >= minimum));
2167 let shortest = ranges.iter().map(|range| range.len()).min().unwrap();
2168 let longest = ranges.iter().map(|range| range.len()).max().unwrap();
2169 assert!(longest - shortest <= 1);
2170 }
2171 assert_eq!(policy_len(&strategy), 0);
2172 }
2173
2174 #[test]
2175 fn run_batches_prepares_borrowed_outputs_once() {
2176 let strategy = parallel_strategy().without_policy();
2177 let input: Vec<_> = (0..17).map(|value| value * 3).collect();
2178 let mut output = vec![0; input.len()];
2179 let preparations = AtomicUsize::new(0);
2180 let ranges = strategy.run_batches(input.len(), NonZeroUsize::MIN, 1, |batches| {
2181 let owned = Rc::new(42);
2182 batches.map_collect_vec(
2183 |ranges| {
2184 assert_eq!(*owned, 42);
2185 drop(owned);
2186 preparations.fetch_add(1, Ordering::Relaxed);
2187 let mut remaining = output.as_mut_slice();
2188 ranges
2189 .into_iter()
2190 .map(|range| {
2191 let (head, tail) =
2192 std::mem::take(&mut remaining).split_at_mut(range.len());
2193 remaining = tail;
2194 (range, head)
2195 })
2196 .collect::<Vec<_>>()
2197 },
2198 |(range, out)| {
2199 assert!(crate::rayon::current_thread_index().is_some());
2200 out.copy_from_slice(&input[range.clone()]);
2201 range
2202 },
2203 )
2204 });
2205 assert_eq!(preparations.load(Ordering::Relaxed), 1);
2206 assert_eq!(output, input);
2207 assert_eq!(ranges.first().unwrap().start, 0);
2208 assert_eq!(ranges.last().unwrap().end, input.len());
2209 }
2210
2211 #[track_caller]
2212 fn run_batches_flagged(
2213 strategy: &Rayon,
2214 fail_mapping: bool,
2215 fail_assembly: bool,
2216 ) -> (&'static std::panic::Location<'static>, Result<usize, ()>) {
2217 (
2218 std::panic::Location::caller(),
2219 strategy.try_run_batches(16, NonZeroUsize::MIN, usize::MAX, |batches| {
2220 let total = batches
2221 .try_map_collect_vec(
2222 |ranges| ranges,
2223 |range| {
2224 if fail_mapping {
2225 Err(())
2226 } else {
2227 Ok(range.len())
2228 }
2229 },
2230 )?
2231 .into_iter()
2232 .sum();
2233 if fail_assembly { Err(()) } else { Ok(total) }
2234 }),
2235 )
2236 }
2237
2238 #[test]
2239 fn run_batches_records_only_complete_success() {
2240 let strategy = parallel_strategy();
2241 let policy = strategy.policy.as_ref().unwrap();
2242
2243 let (mapping_loc, mapping) = run_batches_flagged(&strategy, true, false);
2244 assert_eq!(mapping, Err(()));
2245 assert_eq!(
2246 policy.get_entry(mapping_loc, 16, usize::MAX, 4),
2247 Some((None, None))
2248 );
2249
2250 let (assembly_loc, assembly) = run_batches_flagged(&strategy, false, true);
2251 assert_eq!(assembly, Err(()));
2252 assert_eq!(
2253 policy.get_entry(assembly_loc, 16, usize::MAX, 4),
2254 Some((None, None))
2255 );
2256
2257 let (success_loc, result) = run_batches_flagged(&strategy, false, false);
2258 assert_eq!(result, Ok(16));
2259 let (_, parallel) = policy.get_entry(success_loc, 16, usize::MAX, 4).unwrap();
2260 assert!(parallel.is_some());
2261 assert_eq!(policy_len(&strategy), 3);
2262 }
2263
2264 #[test]
2265 fn run_batches_keeps_one_policy_decision_and_fixed_execution() {
2266 let strategy = parallel_strategy();
2267 let run = |strategy: &Rayon| {
2268 strategy.run_batches(16, NonZeroUsize::MIN, 1, |batches| {
2269 batches.map_collect_vec(|ranges| ranges, |range| range.len())
2270 })
2271 };
2272 assert_eq!(run(&strategy).into_iter().sum::<usize>(), 16);
2273 assert_eq!(policy_len(&strategy), 1);
2274
2275 let fixed = strategy.without_policy();
2276 for _ in 0..3 {
2277 let on_pool = fixed.run_batches(16, NonZeroUsize::MIN, 1, |batches| {
2278 batches.map_collect_vec(
2279 |ranges| ranges,
2280 |_| crate::rayon::current_thread_index().is_some(),
2281 )
2282 });
2283 assert!(on_pool.into_iter().all(|on_pool| on_pool));
2284 }
2285 assert_eq!(policy_len(&strategy), 1);
2286 assert_eq!(policy_len(&fixed), 0);
2287 }
2288
2289 fn map_from_same_callsite(strategy: &Rayon, len: usize) {
2290 let _: Vec<_> = strategy.map_collect_vec(0..len, |x| x);
2291 }
2292
2293 fn map_init_with_multiplier_from_same_callsite(
2294 strategy: &Rayon,
2295 len: usize,
2296 multiplier: usize,
2297 ) {
2298 let _: Vec<_> =
2299 strategy.map_init_collect_vec_with_multiplier(0..len, multiplier, || (), |_, x| x);
2300 }
2301
2302 fn run_from_same_callsite(strategy: &Rayon, len: usize) {
2303 let _: usize = strategy.run(len, || 1, || 2);
2304 }
2305
2306 fn map_partition_from_same_callsite(strategy: &Rayon, len: usize) {
2307 let _: (Vec<_>, Vec<_>) = strategy.map_partition_collect_vec(0..len, |x| {
2308 if x % 2 == 0 { (x, Some(x)) } else { (x, None) }
2309 });
2310 }
2311
2312 #[test]
2313 fn adaptive_policy_is_scoped_to_rayon() {
2314 let strategy = parallel_strategy();
2315 let other = parallel_strategy();
2316
2317 let _: Vec<_> = strategy.map_collect_vec(0..16, |x| x);
2318
2319 assert_eq!(policy_len(&strategy), 1);
2320 assert_eq!(policy_len(&other), 0);
2321 }
2322
2323 /// A spawn awaited from a thread inside the pool must complete even when no other
2324 /// worker can run the job: the pool below registers this thread as a member and never
2325 /// starts its remaining worker, so only the spawn future's yield loop can execute the
2326 /// job (a single poll must suffice; there is no executor to re-poll a pending future).
2327 #[test]
2328 fn spawn_driven_inline_on_member_thread() {
2329 let pool = ThreadPoolBuilder::new()
2330 .num_threads(2)
2331 .use_current_thread()
2332 .unstarted_workers()
2333 .build()
2334 .unwrap();
2335 let strategy = Rayon::with_pool(Arc::new(pool));
2336
2337 let result = strategy
2338 .spawn(2, |strategy| strategy.map_collect_vec(0..2, |i| i + 1))
2339 .now_or_never()
2340 .expect("spawn should complete on first poll via the yield loop");
2341 assert_eq!(result, vec![1, 2]);
2342 }
2343
2344 #[test]
2345 fn spawn_future_moved_from_outside_to_worker_keeps_helping() {
2346 // Occupy one worker with a job whose result depends on work queued later.
2347 let strategy = Rayon::new(NonZeroUsize::new(2).unwrap()).unwrap();
2348 let (value_tx, value_rx) = std::sync::mpsc::channel();
2349 let (started_tx, started_rx) = std::sync::mpsc::channel();
2350 let waiting = strategy.spawn(1, move |_| {
2351 started_tx.send(()).unwrap();
2352 value_rx.recv_timeout(Duration::from_secs(10)).unwrap()
2353 });
2354 started_rx.recv_timeout(Duration::from_secs(10)).unwrap();
2355
2356 // Poll outside the pool, then transfer the pending future to its other worker.
2357 let mut waiting = Box::pin(waiting);
2358 let waker = futures::task::noop_waker();
2359 let mut cx = std::task::Context::from_waker(&waker);
2360 assert!(std::future::Future::poll(waiting.as_mut(), &mut cx).is_pending());
2361 let (polling_tx, polling_rx) = std::sync::mpsc::channel();
2362 let (done_tx, done_rx) = std::sync::mpsc::channel();
2363 let moved = strategy.spawn(1, move |_| {
2364 polling_tx.send(()).unwrap();
2365 done_tx.send(futures::executor::block_on(waiting)).unwrap();
2366 });
2367 polling_rx.recv_timeout(Duration::from_secs(10)).unwrap();
2368
2369 // Both workers are occupied, so the waiting worker must execute this dependency.
2370 // It only sends a value, so helping it cannot nest a blocking executor.
2371 let sender = value_tx.clone();
2372 let supply = strategy.spawn(1, move |_| {
2373 let _ = sender.send(42);
2374 });
2375 let result = done_rx.recv_timeout(Duration::from_secs(1)).ok();
2376
2377 // Release the blocked job and drain the pool even when the progress assertion fails.
2378 let _ = value_tx.send(0);
2379 futures::executor::block_on(moved);
2380 futures::executor::block_on(supply);
2381 assert_eq!(
2382 result,
2383 Some(42),
2384 "the moved future must run pool work while it waits"
2385 );
2386 }
2387
2388 #[test]
2389 fn with_parallelism_overrides_planning_parallelism() {
2390 let strategy = Rayon::new(NonZeroUsize::new(1).unwrap())
2391 .unwrap()
2392 .with_parallelism(NonZeroUsize::new(4).unwrap())
2393 .without_policy();
2394 assert_eq!(strategy.parallelism, 4);
2395 assert_eq!(strategy.run(2, || "serial", || "parallel"), "parallel");
2396 }
2397
2398 #[test]
2399 fn with_pool_plans_the_pool_thread_count() {
2400 let strategy = Rayon::new(NonZeroUsize::new(3).unwrap()).unwrap();
2401 assert_eq!(strategy.parallelism, 3);
2402 }
2403
2404 #[test]
2405 fn adaptive_policy_is_shared_by_clones() {
2406 let strategy = parallel_strategy();
2407 let clone = strategy.clone();
2408
2409 let _: Vec<_> = clone.map_collect_vec(0..16, |x| x);
2410
2411 assert_eq!(policy_len(&strategy), 1);
2412 assert_eq!(policy_len(&clone), 1);
2413 }
2414
2415 #[test]
2416 fn adaptive_policy_records_all_adaptive_operations() {
2417 let strategy = parallel_strategy();
2418
2419 let _: Vec<_> = strategy.fold_init(
2420 0..16,
2421 || (),
2422 Vec::new,
2423 |mut acc, _, x| {
2424 acc.push(x);
2425 acc
2426 },
2427 |mut a, b| {
2428 a.extend(b);
2429 a
2430 },
2431 );
2432 let _: i32 = strategy.fold(0..16, || 0, |acc, x| acc + x, |a, b| a + b);
2433 let _: Result<i32, ()> = strategy.try_fold(0..16, || 0, |acc, x| Ok(acc + x), |a, b| a + b);
2434 let _: Vec<_> = strategy.map_collect_vec(0..16, |x| x);
2435 let _: Result<Vec<_>, ()> = strategy.try_map_collect_vec(0..16, Ok);
2436 let _: Vec<_> = strategy.map_init_collect_vec(
2437 0..16,
2438 || AtomicUsize::new(0),
2439 |counter, x| {
2440 counter.fetch_add(1, Ordering::Relaxed);
2441 x
2442 },
2443 );
2444 let _: Vec<_> = strategy.map_init_collect_vec_with_multiplier(
2445 0..16,
2446 2,
2447 || AtomicUsize::new(0),
2448 |counter, x| {
2449 counter.fetch_add(1, Ordering::Relaxed);
2450 x
2451 },
2452 );
2453 let _: usize = strategy.run(16, || 1, || 2);
2454 let _: (Vec<_>, Vec<_>) = strategy.map_partition_collect_vec(0..16, |x| {
2455 if x % 2 == 0 { (x, Some(x)) } else { (x, None) }
2456 });
2457 let _: (i32, i32) = strategy.join(|| 1, || 2);
2458 let mut sortable = vec![3, 2, 1];
2459 strategy.sort_by(&mut sortable, |a, b| a.cmp(b));
2460 assert_eq!(sortable, vec![1, 2, 3]);
2461 strategy.sort_unstable_by(&mut sortable, |a, b| b.cmp(a));
2462 assert_eq!(sortable, vec![3, 2, 1]);
2463
2464 assert_eq!(policy_len(&strategy), 11);
2465 }
2466
2467 #[test]
2468 fn adaptive_policy_buckets_by_input_size() {
2469 let strategy = parallel_strategy();
2470
2471 map_from_same_callsite(&strategy, 1);
2472 map_from_same_callsite(&strategy, 2);
2473 map_from_same_callsite(&strategy, 3);
2474
2475 assert_eq!(policy_len(&strategy), 2);
2476 }
2477
2478 #[test]
2479 fn adaptive_policy_buckets_by_work_multiplier() {
2480 let strategy = parallel_strategy();
2481
2482 map_init_with_multiplier_from_same_callsite(&strategy, 16, 1);
2483 map_init_with_multiplier_from_same_callsite(&strategy, 16, 2);
2484 map_init_with_multiplier_from_same_callsite(&strategy, 16, 3);
2485
2486 assert_eq!(policy_len(&strategy), 2);
2487 }
2488
2489 #[test]
2490 fn adaptive_run_buckets_by_input_size() {
2491 let strategy = parallel_strategy();
2492
2493 run_from_same_callsite(&strategy, 1);
2494 run_from_same_callsite(&strategy, 2);
2495 run_from_same_callsite(&strategy, 3);
2496
2497 assert_eq!(policy_len(&strategy), 2);
2498 }
2499
2500 /// A strategy without a policy hands every job off on a multi-worker pool, so the job runs on
2501 /// the pool whatever an adaptive policy would decide for this call site.
2502 #[test]
2503 fn spawn_without_policy_always_hands_off() {
2504 let strategy = parallel_strategy().without_policy();
2505
2506 for _ in 0..10 {
2507 let on_pool = futures::executor::block_on(
2508 strategy.spawn(1, |_| crate::rayon::current_thread_index().is_some()),
2509 );
2510 assert!(
2511 on_pool,
2512 "spawn without a policy must run off the calling task"
2513 );
2514 }
2515 }
2516
2517 #[test]
2518 fn strategy_without_policy_records_nothing() {
2519 let strategy = parallel_strategy();
2520 let fixed = strategy.without_policy();
2521
2522 let _: usize = fixed.fold(0..4, || 0, |acc, x| acc + x, |a, b| a + b);
2523 assert_eq!(fixed.run(4, || 1, || 2), 2);
2524
2525 assert_eq!(policy_len(&strategy), 0);
2526 assert_eq!(policy_len(&fixed), 0);
2527 }
2528
2529 #[test]
2530 fn sequential_run_uses_serial_body() {
2531 assert_eq!(Sequential.run(4, || 1, || 2), 1);
2532 }
2533
2534 #[test]
2535 fn adaptive_policy_keys_default_methods_by_external_callsite() {
2536 let strategy = parallel_strategy();
2537
2538 // Default methods must attribute policy keys to their external callers.
2539 let _: i32 = strategy.fold(0..16, || 0, |acc, x| acc + x, |a, b| a + b);
2540 let _: i32 = strategy.fold(0..16, || 0, |acc, x| acc + x, |a, b| a + b);
2541 strategy.run_batches(16, NonZeroUsize::MIN, 1, |_| ());
2542 strategy.run_batches(16, NonZeroUsize::MIN, 1, |_| ());
2543
2544 assert_eq!(policy_len(&strategy), 4);
2545 }
2546
2547 #[test]
2548 fn adaptive_policy_keys_partition_map_by_external_callsite() {
2549 let strategy = parallel_strategy();
2550
2551 map_partition_from_same_callsite(&strategy, 16);
2552 let _: (Vec<_>, Vec<_>) = strategy.map_partition_collect_vec(0..16, |x| {
2553 if x % 2 == 0 { (x, Some(x)) } else { (x, None) }
2554 });
2555
2556 assert_eq!(policy_len(&strategy), 2);
2557 }
2558
2559 #[test]
2560 fn join_does_not_use_adaptive_policy() {
2561 let strategy = parallel_strategy();
2562
2563 let result = strategy.join(|| 1, || 2);
2564
2565 assert_eq!(result, (1, 2));
2566 assert_eq!(policy_len(&strategy), 0);
2567 }
2568
2569 #[test]
2570 fn sequential_spawn_runs_job() {
2571 let result = futures::executor::block_on(Sequential.spawn(1, |_| 7));
2572
2573 assert_eq!(result, 7);
2574 }
2575
2576 #[test]
2577 fn rayon_spawn_runs_job_on_pool() {
2578 let strategy = parallel_strategy();
2579
2580 let result = futures::executor::block_on(strategy.spawn(1, |_| {
2581 assert!(crate::rayon::current_thread_index().is_some());
2582 7
2583 }));
2584
2585 assert_eq!(result, 7);
2586
2587 // Spawn trains only the spawn-side policy: no run entries are created.
2588 assert_eq!(policy_len(&strategy), 0);
2589 }
2590
2591 #[test]
2592 fn rayon_spawn_runs_inline_on_current_thread_single_worker_pool() {
2593 let pool = ThreadPoolBuilder::new()
2594 .num_threads(1)
2595 .use_current_thread()
2596 .build()
2597 .unwrap();
2598 let strategy =
2599 Rayon::with_pool(Arc::new(pool)).with_parallelism(NonZeroUsize::new(4).unwrap());
2600
2601 assert_eq!(strategy.parallelism, 4);
2602
2603 let result = strategy.spawn(1, |_| 7).now_or_never();
2604
2605 assert_eq!(result, Some(7));
2606 assert_eq!(policy_len(&strategy), 0);
2607 }
2608
2609 #[test]
2610 #[should_panic(expected = "boom")]
2611 fn rayon_spawn_propagates_job_panic() {
2612 // A panic on a pool worker must surface at the await point, not abort the process.
2613 let strategy = parallel_strategy();
2614
2615 let _: () = futures::executor::block_on(strategy.spawn(1, |_| panic!("boom")));
2616 }
2617
2618 #[test]
2619 #[should_panic(expected = "boom")]
2620 fn sequential_spawn_propagates_job_panic() {
2621 let _: () = futures::executor::block_on(Sequential.spawn(1, |_| panic!("boom")));
2622 }
2623
2624 proptest! {
2625 #[test]
2626 fn parallel_fold_init_matches_sequential(data in prop::collection::vec(any::<i32>(), 0..500)) {
2627 let sequential = Sequential;
2628 let parallel = parallel_strategy();
2629
2630 let seq_result: Vec<i32> = sequential.fold_init(
2631 &data,
2632 || (),
2633 Vec::new,
2634 |mut acc, _, &x| { acc.push(x.wrapping_mul(2)); acc },
2635 |mut a, b| { a.extend(b); a },
2636 );
2637
2638 let par_result: Vec<i32> = parallel.fold_init(
2639 &data,
2640 || (),
2641 Vec::new,
2642 |mut acc, _, &x| { acc.push(x.wrapping_mul(2)); acc },
2643 |mut a, b| { a.extend(b); a },
2644 );
2645
2646 prop_assert_eq!(seq_result, par_result);
2647 }
2648
2649 #[test]
2650 fn fold_equals_fold_init(data in prop::collection::vec(any::<i32>(), 0..500)) {
2651 let s = Sequential;
2652
2653 let via_fold: Vec<i32> = s.fold(
2654 &data,
2655 Vec::new,
2656 |mut acc, &x| { acc.push(x); acc },
2657 |mut a, b| { a.extend(b); a },
2658 );
2659
2660 let via_fold_init: Vec<i32> = s.fold_init(
2661 &data,
2662 || (),
2663 Vec::new,
2664 |mut acc, _, &x| { acc.push(x); acc },
2665 |mut a, b| { a.extend(b); a },
2666 );
2667
2668 prop_assert_eq!(via_fold, via_fold_init);
2669 }
2670
2671 #[test]
2672 fn parallel_try_fold_matches_sequential(data in prop::collection::vec(any::<i32>(), 0..500)) {
2673 let sequential: Result<i32, ()> = Sequential.try_fold(
2674 &data,
2675 || 0i32,
2676 |acc, &x| Ok(acc.wrapping_add(x)),
2677 |a, b| a.wrapping_add(b),
2678 );
2679 let parallel: Result<i32, ()> = parallel_strategy().try_fold(
2680 &data,
2681 || 0i32,
2682 |acc, &x| Ok(acc.wrapping_add(x)),
2683 |a, b| a.wrapping_add(b),
2684 );
2685
2686 prop_assert_eq!(sequential, parallel);
2687 }
2688
2689 #[test]
2690 fn map_collect_vec_equals_fold(data in prop::collection::vec(any::<i32>(), 0..500)) {
2691 let s = Sequential;
2692 let map_op = |&x: &i32| x.wrapping_mul(3);
2693
2694 let via_map: Vec<i32> = s.map_collect_vec(&data, map_op);
2695
2696 let via_fold: Vec<i32> = s.fold(
2697 &data,
2698 Vec::new,
2699 |mut acc, item| { acc.push(map_op(item)); acc },
2700 |mut a, b| { a.extend(b); a },
2701 );
2702
2703 prop_assert_eq!(via_map, via_fold);
2704 }
2705
2706 #[test]
2707 fn try_map_collect_vec_collects_successes(data in prop::collection::vec(any::<i32>(), 0..500)) {
2708 let expected: Vec<i32> = data.iter().map(|x| x.wrapping_mul(5)).collect();
2709
2710 let sequential: Result<Vec<i32>, ()> =
2711 Sequential.try_map_collect_vec(&data, |&x| Ok(x.wrapping_mul(5)));
2712 prop_assert_eq!(sequential, Ok(expected.clone()));
2713
2714 let parallel: Result<Vec<i32>, ()> =
2715 parallel_strategy().try_map_collect_vec(&data, |&x| Ok(x.wrapping_mul(5)));
2716 prop_assert_eq!(parallel, Ok(expected));
2717 }
2718
2719 #[test]
2720 fn try_map_collect_vec_returns_first_error(data in prop::collection::vec(any::<i32>(), 0..500)) {
2721 let expected_error = data.iter().position(|x| x % 7 == 0);
2722 let result: Result<Vec<i32>, usize> =
2723 Sequential.try_map_collect_vec(data.iter().enumerate(), |(i, &x)| {
2724 if x % 7 == 0 {
2725 Err(i)
2726 } else {
2727 Ok(x)
2728 }
2729 });
2730
2731 match expected_error {
2732 Some(i) => prop_assert_eq!(result, Err(i)),
2733 None => prop_assert_eq!(result, Ok(data)),
2734 }
2735 }
2736
2737 #[test]
2738 fn map_init_collect_vec_equals_fold_init(data in prop::collection::vec(any::<i32>(), 0..500)) {
2739 let s = Sequential;
2740
2741 let via_map: Vec<i32> = s.map_init_collect_vec(
2742 &data,
2743 || 0i32,
2744 |counter, &x| { *counter += 1; x.wrapping_add(*counter) },
2745 );
2746
2747 let via_fold_init: Vec<i32> = s.fold_init(
2748 &data,
2749 || 0i32,
2750 Vec::new,
2751 |mut acc, counter, &x| {
2752 *counter += 1;
2753 acc.push(x.wrapping_add(*counter));
2754 acc
2755 },
2756 |mut a, b| { a.extend(b); a },
2757 );
2758
2759 prop_assert_eq!(via_map, via_fold_init);
2760 }
2761
2762 #[test]
2763 fn map_partition_collect_vec_returns_valid_results(data in prop::collection::vec(any::<i32>(), 0..500)) {
2764 let s = Sequential;
2765
2766 let map_op = |&x: &i32| {
2767 let value = if x % 2 == 0 { Some(x.wrapping_mul(2)) } else { None };
2768 (x, value)
2769 };
2770
2771 let (results, filtered) = s.map_partition_collect_vec(data.iter(), map_op);
2772
2773 // Verify results contains doubled even numbers
2774 let expected_results: Vec<i32> = data.iter().filter(|&&x| x % 2 == 0).map(|&x| x.wrapping_mul(2)).collect();
2775 prop_assert_eq!(results, expected_results);
2776
2777 // Verify filtered contains odd numbers
2778 let expected_filtered: Vec<i32> = data.iter().filter(|&&x| x % 2 != 0).copied().collect();
2779 prop_assert_eq!(filtered, expected_filtered);
2780 }
2781 }
2782
2783 #[test]
2784 fn try_map_collect_vec_sequential_short_circuits() {
2785 let calls = AtomicUsize::new(0);
2786 let result: Result<Vec<usize>, usize> = Sequential.try_map_collect_vec(0..10, |i| {
2787 calls.fetch_add(1, Ordering::Relaxed);
2788 if i == 3 { Err(i) } else { Ok(i) }
2789 });
2790
2791 assert_eq!(result, Err(3));
2792 assert_eq!(calls.load(Ordering::Relaxed), 4);
2793 }
2794
2795 fn fixed_strategy(parallelism: usize) -> Rayon {
2796 Rayon::new(NonZeroUsize::new(4).unwrap())
2797 .unwrap()
2798 .with_parallelism(NonZeroUsize::new(parallelism).unwrap())
2799 .without_policy()
2800 }
2801
2802 /// A tile as its row and the pieces `fill` received.
2803 type Tile = (usize, Vec<Range<usize>>);
2804
2805 /// The tiles a strategy supplies for the one grid.
2806 fn tiles_of(strategy: &impl Strategy, rows: usize, len: usize, tile_cost: usize) -> Vec<Tile> {
2807 let (grid, tiles) = tiles_of_grids(strategy, len, &[grid(rows, tile_cost)]);
2808 assert_eq!(grid, 0);
2809 tiles
2810 }
2811
2812 fn grid(rows: usize, tile_cost: usize) -> Grid {
2813 Grid {
2814 rows,
2815 tile_cost: NonZeroUsize::new(tile_cost).unwrap(),
2816 }
2817 }
2818
2819 /// The grid a strategy cuts among `grids` and its tiles.
2820 #[track_caller]
2821 fn tiles_of_grids(strategy: &impl Strategy, len: usize, grids: &[Grid]) -> (usize, Vec<Tile>) {
2822 strategy.run_tiles(len, grids, 1, |tiles| {
2823 let grid = tiles.grid();
2824
2825 // Record each piece with its row. Finishing a tile takes every recorded piece,
2826 // which must belong to that row, and leaves the state empty for the next tile.
2827 let tiles = tiles.fill_collect_vec(
2828 Vec::new,
2829 |pieces: &mut Vec<(usize, Range<usize>)>, row, columns| pieces.push((row, columns)),
2830 |pieces, row| {
2831 let pieces = core::mem::take(pieces);
2832 assert!(pieces.iter().all(|(piece_row, _)| *piece_row == row));
2833 (
2834 row,
2835 pieces.into_iter().map(|(_, columns)| columns).collect(),
2836 )
2837 },
2838 );
2839 (grid, tiles)
2840 })
2841 }
2842
2843 /// Asserts that `tiles` cover a `rows` by `len` grid exactly once, each tile with
2844 /// contiguous pieces in column order.
2845 fn assert_covers(tiles: &[Tile], rows: usize, len: usize) {
2846 // Validate piece geometry and mark each cell once; the final scan detects missing cells.
2847 let mut seen = vec![false; rows * len];
2848 for (row, pieces) in tiles {
2849 assert!(*row < rows && !pieces.is_empty());
2850 assert!(pieces.windows(2).all(|pair| pair[0].end == pair[1].start));
2851 for columns in pieces {
2852 assert!(!columns.is_empty() && columns.end <= len);
2853 for column in columns.clone() {
2854 assert!(!core::mem::replace(&mut seen[row * len + column], true));
2855 }
2856 }
2857 }
2858 assert!(seen.into_iter().all(|cell| cell));
2859 }
2860
2861 #[test]
2862 fn run_tiles_cover_every_cell_once() {
2863 // Empty grids, one cell, rows too short to cut (30 x 63 and 30 x 100 at tile cost 64),
2864 // and longer rows, the last two with more than `MAX_UNITS` cells and multi-cell units.
2865 let grids = [
2866 (0, 5, 1),
2867 (3, 0, 1),
2868 (1, 1, 1),
2869 (3, 10, 1),
2870 (5, 7, 1),
2871 (5, 7, 3),
2872 (30, 63, 64),
2873 (30, 100, 64),
2874 (30, 32_769, 2_304),
2875 (44, 2_001, 288),
2876 ];
2877
2878 // Each grid runs serially, adaptively, without a policy at 1 to 64 planned workers, and
2879 // under seeded explorers. Seeded explorers can also run the shares one after another on
2880 // the calling thread.
2881 for (rows, len, tile_cost) in grids {
2882 assert_covers(&tiles_of(&Sequential, rows, len, tile_cost), rows, len);
2883 assert_covers(
2884 &tiles_of(¶llel_strategy(), rows, len, tile_cost),
2885 rows,
2886 len,
2887 );
2888 for parallelism in [1, 2, 8, 16, 32, 64] {
2889 let strategy = fixed_strategy(parallelism);
2890 assert_covers(&tiles_of(&strategy, rows, len, tile_cost), rows, len);
2891 }
2892 for seed in 0..16 {
2893 let strategy = Explorer::seeded(&fixed_strategy(16), seed);
2894 assert_covers(&tiles_of(&strategy, rows, len, tile_cost), rows, len);
2895 }
2896 }
2897
2898 // More workers than pool threads, with tiles cheap enough to cut finely, keep claims and
2899 // steals racing.
2900 let strategy = fixed_strategy(16);
2901 for _ in 0..200 {
2902 assert_covers(&tiles_of(&strategy, 7, 300, 2), 7, 300);
2903 }
2904 }
2905
2906 /// A queue of more than `MAX_UNITS` rows holds several rows per unit, so every unit index
2907 /// still fits the packed range and every row is still one tile.
2908 #[test]
2909 fn run_tiles_group_rows_beyond_the_unit_limit() {
2910 // Three columns at tile cost four keep every grid on whole rows.
2911 let strategy = fixed_strategy(4);
2912 for rows in [MAX_UNITS, MAX_UNITS + 1, 2 * MAX_UNITS + 1] {
2913 let tiles = tiles_of(&strategy, rows, 3, 4);
2914 assert_eq!(tiles.len(), rows);
2915 assert!(
2916 tiles.iter().all(
2917 |(_, pieces)| matches!(pieces.as_slice(), [columns] if *columns == (0..3))
2918 )
2919 );
2920 assert_covers(&tiles, rows, 3);
2921 }
2922 }
2923
2924 #[test]
2925 fn run_tiles_serial_runs_make_every_row_one_tile() {
2926 // A single-worker run without a policy is serial too, and a grid without columns has no
2927 // tiles.
2928 let whole: Vec<_> = (0..3)
2929 .map(|row| (row, core::iter::once(0..10).collect()))
2930 .collect();
2931 assert_eq!(tiles_of(&Sequential, 3, 10, 1), whole);
2932 assert_eq!(tiles_of(&fixed_strategy(1), 3, 10, 1), whole);
2933 assert!(tiles_of(&Sequential, 3, 0, 1).is_empty());
2934 }
2935
2936 #[test]
2937 fn run_tiles_bound_workers_by_the_grid() {
2938 // A planning parallelism far beyond the grid allocates and dispatches only the workers the
2939 // grid can occupy.
2940 let strategy = Rayon::new(NonZeroUsize::new(4).unwrap())
2941 .unwrap()
2942 .with_parallelism(NonZeroUsize::MAX)
2943 .without_policy();
2944 for (rows, len, tile_cost) in [(1, 1, 1), (3, 10, 1), (32, 1, 100), (30, 100, 64)] {
2945 assert_covers(&tiles_of(&strategy, rows, len, tile_cost), rows, len);
2946 }
2947 }
2948
2949 /// A run cuts the grid it estimates finishes first for its workers. A serial run cuts the one
2950 /// with the fewest cells, counting each row's finish. A shared run cuts the one whose shares
2951 /// finish soonest, so it may prefer rows that divide evenly among the workers to fewer cells.
2952 #[test]
2953 fn run_tiles_cut_the_grid_that_finishes_first() {
2954 // Twenty-six rows finishing at 1024 cells cost fewer cells than twenty-four finishing at
2955 // 2048 (156,624 against 169,152), so a serial run cuts the first grid. Eight workers get
2956 // three whole rows each of the second (21,144 cells) but would cut rows of the first
2957 // (19,578 plus three tile costs each, 22,650), so a shared run cuts the second as whole
2958 // rows.
2959 let len = 5_000;
2960 let grids = [grid(26, 1024), grid(24, 2048)];
2961 let (chosen, tiles) = tiles_of_grids(&Sequential, len, &grids);
2962 assert_eq!(chosen, 0);
2963 assert_covers(&tiles, 26, len);
2964 assert_eq!(tiles.len(), 26);
2965
2966 let (chosen, tiles) = tiles_of_grids(&fixed_strategy(8), len, &grids);
2967 assert_eq!(chosen, 1);
2968 assert_covers(&tiles, 24, len);
2969 assert_eq!(tiles.len(), 24, "whole rows must never be cut");
2970 assert!(
2971 tiles
2972 .iter()
2973 .all(|(_, pieces)| matches!(pieces[..], [ref whole] if *whole == (0..len)))
2974 );
2975
2976 // The order of equally cheap grids breaks the tie.
2977 let (chosen, _) = tiles_of_grids(&Sequential, len, &[grid(24, 2048), grid(24, 2048)]);
2978 assert_eq!(chosen, 0);
2979 }
2980
2981 /// Long rows that divide evenly among the workers are shared whole, since cutting them
2982 /// would only add tiles. With one row more, the shares cut rows to balance the cells.
2983 #[test]
2984 fn run_tiles_share_whole_rows_when_cutting_only_adds_tiles() {
2985 let strategy = fixed_strategy(4);
2986 let (_, tiles) = tiles_of_grids(&strategy, 1_000, &[grid(8, 10)]);
2987 assert_covers(&tiles, 8, 1_000);
2988 assert_eq!(tiles.len(), 8);
2989
2990 // Nine rows over four workers put three share boundaries inside rows, and each cuts one.
2991 let (_, tiles) = tiles_of_grids(&strategy, 1_000, &[grid(9, 10)]);
2992 assert_covers(&tiles, 9, 1_000);
2993 assert!(tiles.len() >= 12, "{} tiles", tiles.len());
2994
2995 // With three rows of fifty cells over two workers, cutting at 90 cells plus three tile
2996 // costs ties with two whole rows at 120 cells, and the tie keeps the rows whole.
2997 let (_, tiles) = tiles_of_grids(&fixed_strategy(2), 50, &[grid(3, 10)]);
2998 assert_covers(&tiles, 3, 50);
2999 assert_eq!(tiles.len(), 3);
3000 }
3001
3002 #[test]
3003 fn run_tiles_spread_short_rows_across_workers() {
3004 // Every row is shorter than a tile's cost, so no row is cut, but rows still go to separate
3005 // workers: the worker filling the first row waits until another has filled a row.
3006 let rows = 32;
3007 let others = AtomicUsize::new(0);
3008 let tiles = fixed_strategy(4).run_tiles(1, &[grid(rows, 100)], 1, |tiles| {
3009 tiles.fill_collect_vec(
3010 Vec::new,
3011 |pieces: &mut Vec<Range<usize>>, row, columns| {
3012 if row == 0 {
3013 let deadline = Instant::now() + Duration::from_secs(30);
3014 while others.load(Ordering::Acquire) == 0 {
3015 assert!(Instant::now() < deadline, "another worker must fill a row");
3016 std::thread::yield_now();
3017 }
3018 } else {
3019 others.fetch_add(1, Ordering::Release);
3020 }
3021 pieces.push(columns);
3022 },
3023 |pieces, row| (row, core::mem::take(pieces)),
3024 )
3025 });
3026 assert_covers(&tiles, rows, 1);
3027 assert_eq!(tiles.len(), rows);
3028 }
3029
3030 #[test]
3031 fn run_tiles_cut_the_share_of_a_stalled_worker() {
3032 // Four equal shares of one row. The worker holding the first share stalls in its first
3033 // piece until the others have filled every cell except those it may keep for itself: that
3034 // piece, plus at most two tile costs that are not worth taking.
3035 let (len, tile_cost) = (4_000, 10);
3036 let filled = AtomicUsize::new(0);
3037 let tiles = fixed_strategy(4).run_tiles(len, &[grid(1, tile_cost)], 1, |tiles| {
3038 tiles.fill_collect_vec(
3039 Vec::new,
3040 |pieces: &mut Vec<Range<usize>>, _, columns| {
3041 if columns.start == 0 {
3042 let deadline = Instant::now() + Duration::from_secs(30);
3043 while filled.load(Ordering::Acquire) < len - 3 * tile_cost {
3044 assert!(Instant::now() < deadline, "the stalled share must be cut");
3045 std::thread::yield_now();
3046 }
3047 }
3048 filled.fetch_add(columns.len(), Ordering::Release);
3049 pieces.push(columns);
3050 },
3051 |pieces, row| (row, core::mem::take(pieces)),
3052 )
3053 });
3054 assert_covers(&tiles, 1, len);
3055
3056 // The tile holding the stalled piece kept at most those three tile costs.
3057 let first: usize = tiles
3058 .iter()
3059 .find(|(_, pieces)| pieces[0].start == 0)
3060 .map(|(_, pieces)| pieces.iter().map(|columns| columns.len()).sum())
3061 .unwrap();
3062 assert!(first <= 3 * tile_cost, "first tile kept {first} cells");
3063 }
3064
3065 #[test]
3066 fn run_tiles_take_a_stalled_workers_last_row() {
3067 // Two workers over eight whole rows. The worker filling the first row stalls until every
3068 // other row is filled. Only the other worker can fill them, by claiming the rest of the
3069 // queue through the last row.
3070 let rows = 8;
3071 let others = AtomicUsize::new(0);
3072 let tiles = fixed_strategy(2).run_tiles(1, &[grid(rows, 100)], 1, |tiles| {
3073 tiles.fill_collect_vec(
3074 Vec::new,
3075 |pieces: &mut Vec<Range<usize>>, row, columns| {
3076 if row == 0 {
3077 let deadline = Instant::now() + Duration::from_secs(30);
3078 while others.load(Ordering::Acquire) < rows - 1 {
3079 assert!(
3080 Instant::now() < deadline,
3081 "another worker must fill every other row"
3082 );
3083 std::thread::yield_now();
3084 }
3085 } else {
3086 others.fetch_add(1, Ordering::Release);
3087 }
3088 pieces.push(columns);
3089 },
3090 |pieces, row| (row, core::mem::take(pieces)),
3091 )
3092 });
3093 assert_covers(&tiles, rows, 1);
3094 assert_eq!(tiles.len(), rows);
3095 }
3096
3097 /// Fills one row of `len` cells as four shares and asserts that no worker steals.
3098 /// Workers on the last three shares pause at the claims starting at `wait_for` until
3099 /// the first share's tile finishes. The first worker pauses at the claim starting at
3100 /// `drain` until all three have paused, then empties its share. The tails left pending
3101 /// must be too small to pay for a split, so the run ends with one tile per share.
3102 fn assert_no_cheap_steal(len: usize, tile_cost: usize, wait_for: [usize; 3], drain: usize) {
3103 let ready = AtomicUsize::new(0);
3104 let release = AtomicUsize::new(0);
3105 let tiles = fixed_strategy(4).run_tiles(len, &[grid(1, tile_cost)], 1, |tiles| {
3106 tiles.fill_collect_vec(
3107 Vec::new,
3108 |pieces: &mut Vec<Range<usize>>, _, columns| {
3109 // Three workers leave their pending tails available until the first
3110 // worker finishes its original share and checks whether to steal.
3111 let deadline = Instant::now() + Duration::from_secs(30);
3112 if wait_for.contains(&columns.start) {
3113 ready.fetch_add(1, Ordering::Release);
3114 while release.load(Ordering::Acquire) == 0 {
3115 assert!(Instant::now() < deadline, "the first share must finish");
3116 std::thread::yield_now();
3117 }
3118 } else if columns.start == drain {
3119 while ready.load(Ordering::Acquire) != wait_for.len() {
3120 assert!(Instant::now() < deadline, "the other shares must pause");
3121 std::thread::yield_now();
3122 }
3123 }
3124 pieces.push(columns);
3125 },
3126 |pieces, row| {
3127 if pieces[0].start == 0 {
3128 release.store(1, Ordering::Release);
3129 }
3130 (row, core::mem::take(pieces))
3131 },
3132 )
3133 });
3134 assert_covers(&tiles, 1, len);
3135 assert_eq!(tiles.len(), 4, "len={len} tile_cost={tile_cost}");
3136 }
3137
3138 /// Shares of three claims each: a paused share's pending back half is exactly one tile cost.
3139 #[test]
3140 fn run_tiles_do_not_steal_equal_cost_tails() {
3141 assert_no_cheap_steal(120, 10, [30, 60, 90], 0);
3142 }
3143
3144 /// Two-cell units at a tile cost of three cells: the last share's pending back half
3145 /// spans two units but only three cells before the grid ends.
3146 #[test]
3147 fn run_tiles_do_not_steal_clipped_tails_at_cost() {
3148 assert_no_cheap_steal(65_537, 3, [32_762, 49_146, 65_526], 16_384);
3149 }
3150
3151 #[test]
3152 fn run_tiles_serial_runs_reuse_worker_state() {
3153 // A serial run creates its state once and reuses it for every tile.
3154 let inits = AtomicUsize::new(0);
3155 let tiles = Sequential.run_tiles(1_000, &[grid(7, 16)], 1, |tiles| {
3156 tiles.fill_collect_vec(
3157 || {
3158 inits.fetch_add(1, Ordering::Relaxed);
3159 },
3160 |_, _, _| {},
3161 |_, row| row,
3162 )
3163 });
3164 assert_eq!(inits.load(Ordering::Relaxed), 1);
3165 assert_eq!(tiles.len(), 7);
3166 }
3167
3168 #[test]
3169 fn try_map_collect_vec_parallel_returns_an_error() {
3170 let result: Result<Vec<usize>, usize> = parallel_strategy()
3171 .try_map_collect_vec(0..128, |i| if i == 17 || i == 42 { Err(i) } else { Ok(i) });
3172
3173 assert!(matches!(result, Err(17 | 42)));
3174 }
3175}