Skip to main content

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(&parallel, len, minimum);
2086            check(&parallel.without_policy(), len, minimum);
2087        }
2088        check(&Sequential, usize::MAX, NonZeroUsize::MIN);
2089        check(&one_worker, usize::MAX, NonZeroUsize::MIN);
2090        check(&parallel, usize::MAX, NonZeroUsize::MAX);
2091        assert_eq!(policy_len(&one_worker), 0);
2092        assert_eq!(policy_len(&parallel), 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(&parallel_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}