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//! - [`fold`](Strategy::fold): Reduces a collection to a single value
13//! - [`try_fold`](Strategy::try_fold): Like `fold`, but stops applying the fold operation after
14//!   failures
15//! - [`fold_init`](Strategy::fold_init): Like `fold`, but with per-partition initialization
16//! - [`sort_by`](Strategy::sort_by): Sorts a slice with a comparator
17//!
18//! **Convenience Methods:**
19//! - [`map_collect_vec`](Strategy::map_collect_vec): Maps elements and collects into a `Vec`
20//! - [`try_map_collect_vec`](Strategy::try_map_collect_vec): Maps fallible operations and
21//!   collects into a `Result<Vec<_>, _>`
22//! - [`map_init_collect_vec`](Strategy::map_init_collect_vec): Like `map_collect_vec` with
23//!   per-partition initialization
24//! - [`map_partition_collect_vec`](Strategy::map_partition_collect_vec): Maps elements, collecting
25//!   successful results and tracking indices of filtered elements
26//!
27//! Two implementations are provided:
28//!
29//! - [`Sequential`]: Executes operations sequentially on the current thread (works in `no_std`)
30//! - [`Rayon`]: Adaptively executes collection operations serially or with a [`rayon`] thread pool
31//!   (requires `std`)
32//!
33//! # Features
34//!
35//! - `std` (default): Enables the [`Rayon`] strategy backed by rayon
36//!
37//! When the `std` feature is disabled, only [`Sequential`] is available, making this crate
38//! suitable for `no_std` environments.
39//!
40//! # Example
41//!
42//! The main benefit of this crate is writing algorithms that can switch between sequential
43//! and parallel execution:
44//!
45//! ```
46//! use commonware_parallel::{Strategy, Sequential};
47//!
48//! fn sum_of_squares(strategy: &impl Strategy, data: &[i64]) -> i64 {
49//!     strategy.fold(
50//!         data,
51//!         || 0i64,
52//!         |acc, &x| acc + x * x,
53//!         |a, b| a + b,
54//!     )
55//! }
56//!
57//! let strategy = Sequential;
58//! let data = vec![1, 2, 3, 4, 5];
59//! let result = sum_of_squares(&strategy, &data);
60//! assert_eq!(result, 55); // 1 + 4 + 9 + 16 + 25
61//! ```
62
63#![doc(
64    html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
65    html_favicon_url = "https://commonware.xyz/favicon.ico"
66)]
67#![cfg_attr(not(any(feature = "std", test)), no_std)]
68
69commonware_macros::stability_scope!(BETA {
70    use cfg_if::cfg_if;
71    use core::{cmp::Ordering, fmt};
72
73    cfg_if! {
74        if #[cfg(any(feature = "std", test))] {
75            use core::{convert::Infallible, num::NonZeroUsize};
76            use futures::{
77                channel::oneshot,
78                future::{self, Either},
79            };
80            use rayon::{
81                ThreadPool as RThreadPool, ThreadPoolBuildError, ThreadPoolBuilder, Yield,
82                iter::{IntoParallelIterator, ParallelIterator},
83                slice::ParallelSliceMut,
84            };
85            use std::{
86                panic::{self, AssertUnwindSafe, Location},
87                sync::Arc,
88                time::Instant,
89            };
90
91            mod policy;
92        } else {
93            extern crate alloc;
94            use alloc::vec::Vec;
95        }
96    }
97
98    /// A strategy wrapper for manually partitioned work.
99    ///
100    /// Built via [`Strategy::manual`], this disables adaptive policy decisions (including spawn
101    /// placement) for operations that callers have already split into partitions, and carries
102    /// the parallelism used to plan those partitions.
103    #[derive(Clone, Debug)]
104    pub struct Manual<S> {
105        strategy: S,
106        parallelism: usize,
107    }
108
109    impl<S> Manual<S> {
110        /// Returns the parallelism to use for manually partitioned work.
111        pub const fn parallelism(&self) -> usize {
112            self.parallelism
113        }
114    }
115
116    /// A strategy for executing fold operations.
117    ///
118    /// This trait abstracts over sequential and parallel execution, allowing algorithms
119    /// to be written generically and then executed with different strategies depending
120    /// on the use case (e.g., sequential for testing/debugging, parallel for production).
121    pub trait Strategy: Clone + Send + Sync + fmt::Debug + 'static {
122        /// Returns a strategy wrapper for manually partitioned work.
123        fn manual(&self) -> Manual<Self>
124        where
125            Self: Sized;
126
127        /// Submit one CPU-bound job to this strategy, running it inline on the calling task when
128        /// it is measured cheaper than the round trip of offloading it to the pool.
129        ///
130        /// `len` groups calls at a call site into size classes for those measurements, so similar
131        /// `len` must mean comparable cost. An inline job runs to completion before `spawn`
132        /// returns, and jobs whose measured cost exceeds a small time budget offload. To force a
133        /// hand-off on a multi-worker pool, submit through [`manual`](Self::manual).
134        ///
135        /// The returned future resolves when the job completes. Blocking on external
136        /// synchronization or I/O inside the job can occupy execution capacity until it returns.
137        /// When the polling thread itself belongs to the strategy's execution resources (e.g. a
138        /// runtime whose executor thread is registered as a pool worker), the job (and other
139        /// pending work) may be executed inline on that thread rather than waited on.
140        ///
141        /// If the job panics, the panic is propagated to the caller; it never aborts the process.
142        #[track_caller]
143        fn spawn<F, T>(
144            &self,
145            len: usize,
146            f: F,
147        ) -> impl core::future::Future<Output = T> + Send + 'static
148        where
149            F: FnOnce(Self) -> T + Send + 'static,
150            T: Send + 'static;
151
152        /// Runs either a serial or parallel body.
153        #[track_caller]
154        fn run<R, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> R
155        where
156            R: Send,
157            SEQ: FnOnce() -> R + Send,
158            PAR: FnOnce() -> R + Send;
159
160        /// Like [`run`](Self::run), but for fallible work.
161        ///
162        /// The strategy chooses and runs either the serial or parallel body, returning the
163        /// first error produced by the chosen body. Elapsed time is only recorded on success,
164        /// so abort-early error paths cannot poison the adaptive policy's estimates.
165        #[track_caller]
166        fn try_run<R, E, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> Result<R, E>
167        where
168            R: Send,
169            E: Send,
170            SEQ: FnOnce() -> Result<R, E> + Send,
171            PAR: FnOnce() -> Result<R, E> + Send;
172
173        /// Reduces a collection to a single value with per-partition initialization.
174        ///
175        /// Similar to [`fold`](Self::fold), but provides a separate initialization value
176        /// that is created once per partition. This is useful when the fold operation
177        /// requires mutable state that should not be shared across partitions (e.g., a
178        /// scratch buffer, RNG, or expensive-to-clone resource).
179        ///
180        /// # Arguments
181        ///
182        /// - `iter`: The collection to fold over
183        /// - `init`: Creates the per-partition initialization value
184        /// - `identity`: Creates the identity value for the accumulator
185        /// - `fold_op`: Combines accumulator with init state and item: `(acc, &mut init, item) -> acc`
186        /// - `reduce_op`: Combines two accumulators: `(acc1, acc2) -> acc`
187        ///
188        /// # Examples
189        ///
190        /// ```
191        /// use commonware_parallel::{Strategy, Sequential};
192        ///
193        /// let strategy = Sequential;
194        /// let data = vec![1u32, 2, 3, 4, 5];
195        ///
196        /// // Use a scratch buffer to avoid allocations in the inner loop
197        /// let result: Vec<String> = strategy.fold_init(
198        ///     &data,
199        ///     || String::with_capacity(16),  // Per-partition scratch buffer
200        ///     Vec::new,                       // Identity for accumulator
201        ///     |mut acc, buf, &n| {
202        ///         buf.clear();
203        ///         use std::fmt::Write;
204        ///         write!(buf, "num:{}", n).unwrap();
205        ///         acc.push(buf.clone());
206        ///         acc
207        ///     },
208        ///     |mut a, b| { a.extend(b); a },
209        /// );
210        ///
211        /// assert_eq!(result, vec!["num:1", "num:2", "num:3", "num:4", "num:5"]);
212        /// ```
213        #[track_caller]
214        fn fold_init<I, INIT, T, R, ID, F, RD>(
215            &self,
216            iter: I,
217            init: INIT,
218            identity: ID,
219            fold_op: F,
220            reduce_op: RD,
221        ) -> R
222        where
223            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
224            INIT: Fn() -> T + Send + Sync,
225            T: Send,
226            R: Send,
227            ID: Fn() -> R + Send + Sync,
228            F: Fn(R, &mut T, I::Item) -> R + Send + Sync,
229            RD: Fn(R, R) -> R + Send + Sync;
230
231        /// Reduces a collection to a single value using fold and reduce operations.
232        ///
233        /// This method processes elements from the iterator, combining them into a single
234        /// result.
235        ///
236        /// # Arguments
237        ///
238        /// - `iter`: The collection to fold over
239        /// - `identity`: A closure that produces the identity value for the fold.
240        /// - `fold_op`: Combines an accumulator with a single item: `(acc, item) -> acc`
241        /// - `reduce_op`: Combines two accumulators: `(acc1, acc2) -> acc`.
242        ///
243        /// # Examples
244        ///
245        /// ## Sum of Elements
246        ///
247        /// ```
248        /// use commonware_parallel::{Strategy, Sequential};
249        ///
250        /// let strategy = Sequential;
251        /// let numbers = vec![1, 2, 3, 4, 5];
252        ///
253        /// let sum = strategy.fold(
254        ///     &numbers,
255        ///     || 0,                    // identity
256        ///     |acc, &n| acc + n,       // fold: add each number
257        ///     |a, b| a + b,            // reduce: combine partial sums
258        /// );
259        ///
260        /// assert_eq!(sum, 15);
261        /// ```
262        #[track_caller]
263        fn fold<I, R, ID, F, RD>(&self, iter: I, identity: ID, fold_op: F, reduce_op: RD) -> R
264        where
265            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
266            R: Send,
267            ID: Fn() -> R + Send + Sync,
268            F: Fn(R, I::Item) -> R + Send + Sync,
269            RD: Fn(R, R) -> R + Send + Sync,
270        {
271            self.fold_init(
272                iter,
273                || (),
274                identity,
275                |acc, _, item| fold_op(acc, item),
276                reduce_op,
277            )
278        }
279
280        /// Reduces a collection to a single value using a fallible fold operation.
281        ///
282        /// Similar to [`fold`](Self::fold), but `fold_op` may fail. Implementations may stop
283        /// applying `fold_op` after an error is observed. When more than one partition fails,
284        /// any error may be returned.
285        ///
286        /// Adaptive strategies must only record elapsed time when the fold succeeds, so
287        /// abort-early error paths cannot poison the policy's estimates.
288        ///
289        /// # Arguments
290        ///
291        /// - `iter`: The collection to fold over
292        /// - `identity`: A closure that produces the identity value for the fold.
293        /// - `fold_op`: Fallibly combines an accumulator with a single item: `(acc, item) -> Result<acc, E>`
294        /// - `reduce_op`: Combines two successful accumulators: `(acc1, acc2) -> acc`.
295        #[track_caller]
296        fn try_fold<I, R, E, ID, F, RD>(
297            &self,
298            iter: I,
299            identity: ID,
300            fold_op: F,
301            reduce_op: RD,
302        ) -> Result<R, E>
303        where
304            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
305            R: Send,
306            E: Send,
307            ID: Fn() -> R + Send + Sync,
308            F: Fn(R, I::Item) -> Result<R, E> + Send + Sync,
309            RD: Fn(R, R) -> R + Send + Sync;
310
311        /// Maps each element and collects results into a `Vec`.
312        ///
313        /// This is a convenience method that applies `map_op` to each element and
314        /// collects the results. For [`Sequential`], elements are processed in order.
315        /// For [`Rayon`], elements may be processed out of order but the final
316        /// vector preserves the original ordering.
317        ///
318        /// # Arguments
319        ///
320        /// - `iter`: The collection to map over
321        /// - `map_op`: The mapping function to apply to each element
322        ///
323        /// # Examples
324        ///
325        /// ```
326        /// use commonware_parallel::{Strategy, Sequential};
327        ///
328        /// let strategy = Sequential;
329        /// let data = vec![1, 2, 3, 4, 5];
330        ///
331        /// let squared: Vec<i32> = strategy.map_collect_vec(&data, |&x| x * x);
332        /// assert_eq!(squared, vec![1, 4, 9, 16, 25]);
333        /// ```
334        #[track_caller]
335        fn map_collect_vec<I, F, T>(&self, iter: I, map_op: F) -> Vec<T>
336        where
337            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
338            F: Fn(I::Item) -> T + Send + Sync,
339            T: Send,
340        {
341            self.fold(
342                iter,
343                Vec::new,
344                |mut acc, item| {
345                    acc.push(map_op(item));
346                    acc
347                },
348                |mut a, b| {
349                    a.extend(b);
350                    a
351                },
352            )
353        }
354
355        /// Maps each element with a fallible operation and collects results into a `Vec`.
356        ///
357        /// This is a convenience method that applies `map_op` to each element and
358        /// collects the results into a single `Result`. Output ordering on success
359        /// matches [`map_collect_vec`](Self::map_collect_vec). Implementations may stop
360        /// applying `map_op` after an error is observed. When more than one element
361        /// fails, any error may be returned.
362        ///
363        /// # Arguments
364        ///
365        /// - `iter`: The collection to map over
366        /// - `map_op`: The fallible mapping function to apply to each element
367        ///
368        /// # Examples
369        ///
370        /// ```
371        /// use commonware_parallel::{Strategy, Sequential};
372        ///
373        /// let strategy = Sequential;
374        /// let data = vec![1, 2, 3, 4, 5];
375        ///
376        /// let squared: Result<Vec<i32>, ()> = strategy.try_map_collect_vec(
377        ///     &data,
378        ///     |&x| Ok(x * x),
379        /// );
380        /// assert_eq!(squared, Ok(vec![1, 4, 9, 16, 25]));
381        /// ```
382        #[track_caller]
383        fn try_map_collect_vec<I, F, T, E>(&self, iter: I, map_op: F) -> Result<Vec<T>, E>
384        where
385            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
386            F: Fn(I::Item) -> Result<T, E> + Send + Sync,
387            T: Send,
388            E: Send,
389        {
390            self.try_fold(
391                iter,
392                Vec::new,
393                |mut acc, item| {
394                    acc.push(map_op(item)?);
395                    Ok(acc)
396                },
397                |mut a, b| {
398                    a.extend(b);
399                    a
400                },
401            )
402        }
403
404        /// Maps each element with per-partition state and collects results into a `Vec`.
405        ///
406        /// Combines [`map_collect_vec`](Self::map_collect_vec) with per-partition
407        /// initialization like [`fold_init`](Self::fold_init). Useful when the mapping
408        /// operation requires mutable state that should not be shared across partitions.
409        ///
410        /// # Arguments
411        ///
412        /// - `iter`: The collection to map over
413        /// - `init`: Creates the per-partition initialization value
414        /// - `map_op`: The mapping function: `(&mut init, item) -> result`
415        ///
416        /// # Examples
417        ///
418        /// ```
419        /// use commonware_parallel::{Strategy, Sequential};
420        ///
421        /// let strategy = Sequential;
422        /// let data = vec![1, 2, 3, 4, 5];
423        ///
424        /// // Use a counter that tracks position within each partition
425        /// let indexed: Vec<(usize, i32)> = strategy.map_init_collect_vec(
426        ///     &data,
427        ///     || 0usize, // Per-partition counter
428        ///     |counter, &x| {
429        ///         let idx = *counter;
430        ///         *counter += 1;
431        ///         (idx, x * 2)
432        ///     },
433        /// );
434        ///
435        /// assert_eq!(indexed, vec![(0, 2), (1, 4), (2, 6), (3, 8), (4, 10)]);
436        /// ```
437        #[track_caller]
438        fn map_init_collect_vec<I, INIT, T, F, R>(&self, iter: I, init: INIT, map_op: F) -> Vec<R>
439        where
440            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
441            INIT: Fn() -> T + Send + Sync,
442            T: Send,
443            F: Fn(&mut T, I::Item) -> R + Send + Sync,
444            R: Send,
445        {
446            self.fold_init(
447                iter,
448                init,
449                Vec::new,
450                |mut acc, init_val, item| {
451                    acc.push(map_op(init_val, item));
452                    acc
453                },
454                |mut a, b| {
455                    a.extend(b);
456                    a
457                },
458            )
459        }
460
461        /// Maps each element with per-partition state and a per-item work multiplier.
462        #[track_caller]
463        fn map_init_collect_vec_with_multiplier<I, INIT, T, F, R>(
464            &self,
465            iter: I,
466            _multiplier: usize,
467            init: INIT,
468            map_op: F,
469        ) -> Vec<R>
470        where
471            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
472            INIT: Fn() -> T + Send + Sync,
473            T: Send,
474            F: Fn(&mut T, I::Item) -> R + Send + Sync,
475            R: Send,
476        {
477            self.map_init_collect_vec(iter, init, map_op)
478        }
479
480        /// Maps each element with a per-item work multiplier.
481        ///
482        /// Convenience over
483        /// [`map_init_collect_vec_with_multiplier`](Self::map_init_collect_vec_with_multiplier)
484        /// for stateless map operations.
485        #[track_caller]
486        fn map_collect_vec_with_multiplier<I, F, R>(
487            &self,
488            iter: I,
489            multiplier: usize,
490            map_op: F,
491        ) -> Vec<R>
492        where
493            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
494            F: Fn(I::Item) -> R + Send + Sync,
495            R: Send,
496        {
497            self.map_init_collect_vec_with_multiplier(iter, multiplier, || (), |_, item| {
498                map_op(item)
499            })
500        }
501
502        /// Maps each element, filtering out `None` results and tracking their keys.
503        ///
504        /// This is a convenience method that applies `map_op` to each element. The
505        /// closure returns `(key, Option<value>)`. Elements where the option is `Some`
506        /// have their values collected into the first vector. Elements where the option
507        /// is `None` have their keys collected into the second vector.
508        ///
509        /// # Arguments
510        ///
511        /// - `iter`: The collection to map over
512        /// - `map_op`: The mapping function returning `(K, Option<U>)`
513        ///
514        /// # Returns
515        ///
516        /// A tuple of `(results, filtered_keys)` where:
517        /// - `results`: Values from successful mappings (where `map_op` returned `Some`)
518        /// - `filtered_keys`: Keys where `map_op` returned `None`
519        ///
520        /// # Examples
521        ///
522        /// ```
523        /// use commonware_parallel::{Strategy, Sequential};
524        ///
525        /// let strategy = Sequential;
526        /// let data = vec![1, 2, 3, 4, 5];
527        ///
528        /// let (evens, odd_values): (Vec<i32>, Vec<i32>) = strategy.map_partition_collect_vec(
529        ///     data.iter(),
530        ///     |&x| (x, if x % 2 == 0 { Some(x * 10) } else { None }),
531        /// );
532        ///
533        /// assert_eq!(evens, vec![20, 40]);
534        /// assert_eq!(odd_values, vec![1, 3, 5]);
535        /// ```
536        #[track_caller]
537        fn map_partition_collect_vec<I, F, K, U>(&self, iter: I, map_op: F) -> (Vec<U>, Vec<K>)
538        where
539            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
540            F: Fn(I::Item) -> (K, Option<U>) + Send + Sync,
541            K: Send,
542            U: Send,
543        {
544            self.fold(
545                iter,
546                || (Vec::new(), Vec::new()),
547                |(mut results, mut filtered), item| {
548                    let (key, value) = map_op(item);
549                    match value {
550                        Some(v) => results.push(v),
551                        None => filtered.push(key),
552                    }
553                    (results, filtered)
554                },
555                |(mut r1, mut f1), (r2, f2)| {
556                    r1.extend(r2);
557                    f1.extend(f2);
558                    (r1, f1)
559                },
560            )
561        }
562
563        /// Executes two closures, potentially in parallel, and returns both results.
564        ///
565        /// For [`Sequential`], this executes `a` then `b` on the current thread.
566        /// For [`Rayon`], this executes `a` and `b` using `rayon::join`.
567        ///
568        /// # Arguments
569        ///
570        /// - `a`: First closure to execute
571        /// - `b`: Second closure to execute
572        ///
573        /// # Examples
574        ///
575        /// ```
576        /// use commonware_parallel::{Strategy, Sequential};
577        ///
578        /// let strategy = Sequential;
579        ///
580        /// let (sum, product) = strategy.join(
581        ///     || (1..=5).sum::<i32>(),
582        ///     || (1..=5).product::<i32>(),
583        /// );
584        ///
585        /// assert_eq!(sum, 15);
586        /// assert_eq!(product, 120);
587        /// ```
588        fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
589        where
590            A: FnOnce() -> RA + Send,
591            B: FnOnce() -> RB + Send,
592            RA: Send,
593            RB: Send;
594
595        /// Sorts a slice with a comparator, preserving the order of equal elements.
596        ///
597        /// # Examples
598        ///
599        /// ```
600        /// use commonware_parallel::{Strategy, Sequential};
601        ///
602        /// let strategy = Sequential;
603        /// let mut data = vec![3, 1, 2];
604        /// strategy.sort_by(&mut data, |a, b| a.cmp(b));
605        /// assert_eq!(data, vec![1, 2, 3]);
606        /// ```
607        #[track_caller]
608        fn sort_by<T, C>(&self, items: &mut [T], compare: C)
609        where
610            T: Send,
611            C: Fn(&T, &T) -> Ordering + Send + Sync;
612    }
613
614    impl<S: Strategy> Strategy for Manual<S> {
615        fn manual(&self) -> Manual<Self> {
616            Manual {
617                strategy: self.clone(),
618                parallelism: self.parallelism,
619            }
620        }
621
622        #[track_caller]
623        fn spawn<F, T>(
624            &self,
625            len: usize,
626            f: F,
627        ) -> impl core::future::Future<Output = T> + Send + 'static
628        where
629            F: FnOnce(Self) -> T + Send + 'static,
630            T: Send + 'static,
631        {
632            let s = self.clone();
633            self.strategy.spawn(len, |_| f(s))
634        }
635
636        #[track_caller]
637        fn run<R, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> R
638        where
639            R: Send,
640            SEQ: FnOnce() -> R + Send,
641            PAR: FnOnce() -> R + Send,
642        {
643            self.strategy.run(len, serial, parallel)
644        }
645
646        #[track_caller]
647        fn try_run<R, E, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> Result<R, E>
648        where
649            R: Send,
650            E: Send,
651            SEQ: FnOnce() -> Result<R, E> + Send,
652            PAR: FnOnce() -> Result<R, E> + Send,
653        {
654            self.strategy.try_run(len, serial, parallel)
655        }
656
657        #[track_caller]
658        fn fold_init<I, INIT, T, R, ID, F, RD>(
659            &self,
660            iter: I,
661            init: INIT,
662            identity: ID,
663            fold_op: F,
664            reduce_op: RD,
665        ) -> R
666        where
667            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
668            INIT: Fn() -> T + Send + Sync,
669            T: Send,
670            R: Send,
671            ID: Fn() -> R + Send + Sync,
672            F: Fn(R, &mut T, I::Item) -> R + Send + Sync,
673            RD: Fn(R, R) -> R + Send + Sync,
674        {
675            self.strategy
676                .fold_init(iter, init, identity, fold_op, reduce_op)
677        }
678
679        #[track_caller]
680        fn try_fold<I, R, E, ID, F, RD>(
681            &self,
682            iter: I,
683            identity: ID,
684            fold_op: F,
685            reduce_op: RD,
686        ) -> Result<R, E>
687        where
688            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
689            R: Send,
690            E: Send,
691            ID: Fn() -> R + Send + Sync,
692            F: Fn(R, I::Item) -> Result<R, E> + Send + Sync,
693            RD: Fn(R, R) -> R + Send + Sync,
694        {
695            self.strategy.try_fold(iter, identity, fold_op, reduce_op)
696        }
697
698        #[track_caller]
699        fn map_collect_vec<I, F, T>(&self, iter: I, map_op: F) -> Vec<T>
700        where
701            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
702            F: Fn(I::Item) -> T + Send + Sync,
703            T: Send,
704        {
705            self.strategy.map_collect_vec(iter, map_op)
706        }
707
708        #[track_caller]
709        fn try_map_collect_vec<I, F, T, E>(&self, iter: I, map_op: F) -> Result<Vec<T>, E>
710        where
711            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
712            F: Fn(I::Item) -> Result<T, E> + Send + Sync,
713            T: Send,
714            E: Send,
715        {
716            self.strategy.try_map_collect_vec(iter, map_op)
717        }
718
719        #[track_caller]
720        fn map_init_collect_vec<I, INIT, T, F, R>(&self, iter: I, init: INIT, map_op: F) -> Vec<R>
721        where
722            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
723            INIT: Fn() -> T + Send + Sync,
724            T: Send,
725            F: Fn(&mut T, I::Item) -> R + Send + Sync,
726            R: Send,
727        {
728            self.strategy.map_init_collect_vec(iter, init, map_op)
729        }
730
731        #[track_caller]
732        fn map_init_collect_vec_with_multiplier<I, INIT, T, F, R>(
733            &self,
734            iter: I,
735            multiplier: usize,
736            init: INIT,
737            map_op: F,
738        ) -> Vec<R>
739        where
740            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
741            INIT: Fn() -> T + Send + Sync,
742            T: Send,
743            F: Fn(&mut T, I::Item) -> R + Send + Sync,
744            R: Send,
745        {
746            self.strategy
747                .map_init_collect_vec_with_multiplier(iter, multiplier, init, map_op)
748        }
749
750        #[track_caller]
751        fn map_partition_collect_vec<I, F, K, U>(&self, iter: I, map_op: F) -> (Vec<U>, Vec<K>)
752        where
753            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
754            F: Fn(I::Item) -> (K, Option<U>) + Send + Sync,
755            K: Send,
756            U: Send,
757        {
758            self.strategy.map_partition_collect_vec(iter, map_op)
759        }
760
761        fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
762        where
763            A: FnOnce() -> RA + Send,
764            B: FnOnce() -> RB + Send,
765            RA: Send,
766            RB: Send,
767        {
768            self.strategy.join(a, b)
769        }
770
771        #[track_caller]
772        fn sort_by<T, C>(&self, items: &mut [T], compare: C)
773        where
774            T: Send,
775            C: Fn(&T, &T) -> Ordering + Send + Sync,
776        {
777            self.strategy.sort_by(items, compare)
778        }
779    }
780
781    /// A sequential execution strategy.
782    ///
783    /// This strategy executes all operations on the current thread without any
784    /// parallelism. It is useful for:
785    ///
786    /// - Debugging and testing (deterministic execution)
787    /// - `no_std` environments where threading is unavailable
788    /// - Small workloads where parallelism overhead exceeds benefits
789    /// - Comparing sequential vs parallel performance
790    ///
791    /// # Examples
792    ///
793    /// ```
794    /// use commonware_parallel::{Strategy, Sequential};
795    ///
796    /// let strategy = Sequential;
797    /// let data = vec![1, 2, 3, 4, 5];
798    ///
799    /// let sum = strategy.fold(&data, || 0, |a, &b| a + b, |a, b| a + b);
800    /// assert_eq!(sum, 15);
801    /// ```
802    #[derive(Default, Debug, Clone)]
803    pub struct Sequential;
804
805    impl Strategy for Sequential {
806        fn manual(&self) -> Manual<Self> {
807            Manual {
808                strategy: Self,
809                parallelism: 1,
810            }
811        }
812
813        fn spawn<F, T>(
814            &self,
815            _len: usize,
816            f: F,
817        ) -> impl core::future::Future<Output = T> + Send + 'static
818        where
819            F: FnOnce(Self) -> T + Send + 'static,
820            T: Send + 'static,
821        {
822            let result = f(self.clone());
823            async move { result }
824        }
825
826        fn run<R, SEQ, PAR>(&self, _len: usize, serial: SEQ, _parallel: PAR) -> R
827        where
828            R: Send,
829            SEQ: FnOnce() -> R + Send,
830            PAR: FnOnce() -> R + Send,
831        {
832            serial()
833        }
834
835        fn try_run<R, E, SEQ, PAR>(&self, _len: usize, serial: SEQ, _parallel: PAR) -> Result<R, E>
836        where
837            R: Send,
838            E: Send,
839            SEQ: FnOnce() -> Result<R, E> + Send,
840            PAR: FnOnce() -> Result<R, E> + Send,
841        {
842            serial()
843        }
844
845        fn fold_init<I, INIT, T, R, ID, F, RD>(
846            &self,
847            iter: I,
848            init: INIT,
849            identity: ID,
850            fold_op: F,
851            _reduce_op: RD,
852        ) -> R
853        where
854            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
855            INIT: Fn() -> T + Send + Sync,
856            T: Send,
857            R: Send,
858            ID: Fn() -> R + Send + Sync,
859            F: Fn(R, &mut T, I::Item) -> R + Send + Sync,
860            RD: Fn(R, R) -> R + Send + Sync,
861        {
862            let mut init_val = init();
863            iter.into_iter()
864                .fold(identity(), |acc, item| fold_op(acc, &mut init_val, item))
865        }
866
867        fn try_fold<I, R, E, ID, F, RD>(
868            &self,
869            iter: I,
870            identity: ID,
871            fold_op: F,
872            _reduce_op: RD,
873        ) -> Result<R, E>
874        where
875            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
876            R: Send,
877            E: Send,
878            ID: Fn() -> R + Send + Sync,
879            F: Fn(R, I::Item) -> Result<R, E> + Send + Sync,
880            RD: Fn(R, R) -> R + Send + Sync,
881        {
882            iter.into_iter().try_fold(identity(), fold_op)
883        }
884
885        fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
886        where
887            A: FnOnce() -> RA + Send,
888            B: FnOnce() -> RB + Send,
889            RA: Send,
890            RB: Send,
891        {
892            (a(), b())
893        }
894
895        fn sort_by<T, C>(&self, items: &mut [T], compare: C)
896        where
897            T: Send,
898            C: Fn(&T, &T) -> Ordering + Send + Sync,
899        {
900            items.sort_by(compare);
901        }
902    }
903});
904commonware_macros::stability_scope!(BETA, cfg(any(feature = "std", test)) {
905    /// A clone-able wrapper around a [rayon]-compatible thread pool.
906    pub type ThreadPool = Arc<RThreadPool>;
907
908    /// A parallel execution strategy backed by a rayon thread pool.
909    ///
910    /// This strategy adaptively executes collection operations serially or through its backing
911    /// pool. It records wall-clock estimates by callsite, input-size and work-size buckets, and
912    /// planning parallelism so small inputs can avoid rayon scheduling overhead without disabling
913    /// parallel execution for larger inputs.
914    ///
915    /// # Thread Pool Ownership
916    ///
917    /// `Rayon` holds an [`Arc<ThreadPool>`], so it can be cheaply cloned and shared
918    /// across threads. Multiple [`Rayon`] instances can share the same underlying
919    /// thread pool.
920    ///
921    /// # When to Use
922    ///
923    /// Use `Rayon` when:
924    ///
925    /// - Processing large collections where parallelism overhead is justified
926    /// - The fold/reduce operations are CPU-bound
927    /// - You want to utilize multiple cores
928    ///
929    /// Consider [`Sequential`] instead when:
930    ///
931    /// - The collection is small
932    /// - Operations are I/O-bound rather than CPU-bound
933    /// - Deterministic execution order is required for debugging
934    ///
935    /// # Examples
936    ///
937    /// ```rust
938    /// use commonware_parallel::{Strategy, Rayon};
939    /// use std::num::NonZeroUsize;
940    ///
941    /// let strategy = Rayon::new(NonZeroUsize::new(2).unwrap()).unwrap();
942    ///
943    /// let data: Vec<i64> = (0..1000).collect();
944    /// let sum = strategy.fold(&data, || 0i64, |acc, &n| acc + n, |a, b| a + b);
945    /// assert_eq!(sum, 499500);
946    /// ```
947    #[derive(Debug, Clone)]
948    pub struct Rayon {
949        thread_pool: ThreadPool,
950        // The parallelism assumed for policy decisions and manual partitioning. Defaults to the
951        // pool's thread count.
952        parallelism: usize,
953        // `Some` enables adaptive serial-vs-parallel decisions; `None` (used by `manual`) runs the
954        // parallel body whenever the parallelism exceeds one and allocates no policy state.
955        policy: Option<policy::Policy>,
956    }
957
958    impl Rayon {
959        /// Creates a [`Rayon`] strategy with a [`ThreadPool`] that is configured with the given
960        /// number of threads.
961        pub fn new(num_threads: NonZeroUsize) -> Result<Self, ThreadPoolBuildError> {
962            ThreadPoolBuilder::new()
963                .num_threads(num_threads.get())
964                .build()
965                .map(|pool| Self::with_pool(Arc::new(pool)))
966        }
967
968        /// Creates a new [`Rayon`] strategy with the given [`ThreadPool`].
969        pub fn with_pool(thread_pool: ThreadPool) -> Self {
970            let parallelism = thread_pool.current_num_threads().max(1);
971            Self {
972                thread_pool,
973                parallelism,
974                policy: Some(policy::Policy::default()),
975            }
976        }
977
978        /// Overrides the parallelism assumed for planning decisions.
979        ///
980        /// This does not resize the backing pool. By default a strategy plans with the pool's
981        /// thread count; override it when the strategy should expose a different parallelism
982        /// (e.g. a runtime that executes strategy work inline on a single thread).
983        pub const fn with_parallelism(mut self, parallelism: NonZeroUsize) -> Self {
984            self.parallelism = parallelism.get();
985            self
986        }
987
988        #[track_caller]
989        fn execute<R>(
990            &self,
991            len: usize,
992            multiplier: usize,
993            run: impl FnOnce(policy::RunExecution) -> R,
994        ) -> R {
995            match self.try_execute(len, multiplier, |execution| {
996                Ok::<_, Infallible>(run(execution))
997            }) {
998                Ok(result) => result,
999                Err(e) => match e {},
1000            }
1001        }
1002
1003        #[track_caller]
1004        fn try_execute<R, E>(
1005            &self,
1006            len: usize,
1007            multiplier: usize,
1008            run: impl FnOnce(policy::RunExecution) -> Result<R, E>,
1009        ) -> Result<R, E> {
1010            let Some(policy) = &self.policy else {
1011                let execution = if self.parallelism <= 1 {
1012                    policy::RunExecution::Serial
1013                } else {
1014                    policy::RunExecution::Parallel
1015                };
1016                return run(execution);
1017            };
1018
1019            let work = len.saturating_mul(multiplier);
1020            policy.try_run(Location::caller(), len, work, self.parallelism, run)
1021        }
1022    }
1023
1024    impl Strategy for Rayon {
1025        fn manual(&self) -> Manual<Self> {
1026            Manual {
1027                strategy: Self {
1028                    thread_pool: self.thread_pool.clone(),
1029                    parallelism: self.parallelism,
1030                    policy: None,
1031                },
1032                parallelism: self.parallelism,
1033            }
1034        }
1035
1036        #[track_caller]
1037        fn spawn<F, T>(
1038            &self,
1039            len: usize,
1040            f: F,
1041        ) -> impl core::future::Future<Output = T> + Send + 'static
1042        where
1043            F: FnOnce(Self) -> T + Send + 'static,
1044            T: Send + 'static,
1045        {
1046            let threads = self.thread_pool.current_num_threads();
1047            let caller = Location::caller();
1048
1049            // A single-worker pool cannot overlap a hand-off, so the job always runs inline,
1050            // untimed. A manual strategy has no policy and keeps spawn's unconditional
1051            // hand-off. Otherwise the policy weighs the measured job cost against the offload
1052            // round trip.
1053            let ((execution, measure), policy) = if threads <= 1 {
1054                ((policy::SpawnExecution::Inline, false), None)
1055            } else {
1056                self.policy.as_ref().map_or(
1057                    ((policy::SpawnExecution::Offload, false), None),
1058                    |policy| (policy.choose_spawn(caller, len, threads), Some(policy)),
1059                )
1060            };
1061
1062            match execution {
1063                policy::SpawnExecution::Inline => {
1064                    // Inline: run on the calling task and hand back a ready future.
1065                    let start = measure.then(Instant::now);
1066                    let result = f(self.clone());
1067                    if let (Some(start), Some(policy)) = (start, policy) {
1068                        policy.record_spawn_inline(caller, len, threads, start.elapsed());
1069                    }
1070                    Either::Left(future::ready(result))
1071                }
1072                policy::SpawnExecution::Offload => {
1073                    // Offload: hand the job to the pool. The worker records the job wall (so
1074                    // job estimates survive a dropped future), and the awaiting future records
1075                    // the round-trip overhead when it observes the result.
1076                    let spawn_start = measure.then(Instant::now);
1077                    let (tx, mut rx) = oneshot::channel();
1078                    let s = self.clone();
1079                    let pool = self.thread_pool.clone();
1080                    let recorder = if measure {
1081                        policy.cloned().map(|policy| (policy, caller, len, threads))
1082                    } else {
1083                        None
1084                    };
1085                    let worker_recorder = recorder.clone();
1086                    self.thread_pool.spawn(move || {
1087                        let job_start = worker_recorder.is_some().then(Instant::now);
1088
1089                        // Catch the panic so a panicking job propagates to the awaiting task
1090                        // rather than aborting the process (rayon aborts on an uncaught panic in
1091                        // a spawned job).
1092                        let result = panic::catch_unwind(AssertUnwindSafe(|| f(s)));
1093                        let job = job_start.map(|start| start.elapsed());
1094                        let ok = result.is_ok();
1095                        let _ = tx.send((result, job));
1096
1097                        // Record successful runs only, matching the inline arm: a panicked job's
1098                        // wall time says nothing about the job size. Recording after the send
1099                        // keeps the bookkeeping off the caller's wake path.
1100                        if ok
1101                            && let (Some((policy, caller, len, threads)), Some(job)) =
1102                                (worker_recorder, job)
1103                        {
1104                            policy.record_spawn_job(caller, len, threads, job);
1105                        }
1106                    });
1107                    Either::Right(async move {
1108                        // When the polling thread is itself a member of the pool, waiting on the
1109                        // channel could park the only worker able to run the job. Execute pending
1110                        // pool work inline until the job completes or another worker takes over.
1111                        // `yield_now` returns `None` when this thread is not a pool member, so
1112                        // external callers fall through to the channel immediately.
1113                        let (result, job) = loop {
1114                            if let Ok(Some(payload)) = rx.try_recv() {
1115                                break payload;
1116                            }
1117                            if !matches!(pool.yield_now(), Some(Yield::Executed)) {
1118                                break rx.await.unwrap_or_else(|_| {
1119                                    panic!("strategy job dropped before completion")
1120                                });
1121                            }
1122                        };
1123                        match result {
1124                            Ok(value) => {
1125                                // The round trip is everything around the job itself: hand-off
1126                                // setup, queueing, worker wake, result send, task wake, and this
1127                                // poll. A late poll inflates the sample with overlap slack, which
1128                                // only ever biases toward inline, and the policy's budget caps
1129                                // what that bias can buy.
1130                                if let (
1131                                    Some((policy, caller, len, threads)),
1132                                    Some(job),
1133                                    Some(start),
1134                                ) = (recorder, job, spawn_start)
1135                                {
1136                                    policy.record_spawn_overhead(
1137                                        caller,
1138                                        len,
1139                                        threads,
1140                                        start.elapsed().saturating_sub(job),
1141                                    );
1142                                }
1143                                value
1144                            }
1145                            Err(payload) => panic::resume_unwind(payload),
1146                        }
1147                    })
1148                }
1149            }
1150        }
1151
1152        #[track_caller]
1153        fn run<R, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> R
1154        where
1155            R: Send,
1156            SEQ: FnOnce() -> R + Send,
1157            PAR: FnOnce() -> R + Send,
1158        {
1159            self.execute(len, 1, |execution| match execution {
1160                policy::RunExecution::Serial => serial(),
1161                policy::RunExecution::Parallel => parallel(),
1162            })
1163        }
1164
1165        #[track_caller]
1166        fn try_run<R, E, SEQ, PAR>(&self, len: usize, serial: SEQ, parallel: PAR) -> Result<R, E>
1167        where
1168            R: Send,
1169            E: Send,
1170            SEQ: FnOnce() -> Result<R, E> + Send,
1171            PAR: FnOnce() -> Result<R, E> + Send,
1172        {
1173            self.try_execute(len, 1, |execution| match execution {
1174                policy::RunExecution::Serial => serial(),
1175                policy::RunExecution::Parallel => parallel(),
1176            })
1177        }
1178
1179        #[track_caller]
1180        fn fold_init<I, INIT, T, R, ID, F, RD>(
1181            &self,
1182            iter: I,
1183            init: INIT,
1184            identity: ID,
1185            fold_op: F,
1186            reduce_op: RD,
1187        ) -> R
1188        where
1189            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1190            INIT: Fn() -> T + Send + Sync,
1191            T: Send,
1192            R: Send,
1193            ID: Fn() -> R + Send + Sync,
1194            F: Fn(R, &mut T, I::Item) -> R + Send + Sync,
1195            RD: Fn(R, R) -> R + Send + Sync,
1196        {
1197            let items: Vec<I::Item> = iter.into_iter().collect();
1198            self.execute(items.len(), 1, |execution| match execution {
1199                policy::RunExecution::Serial => {
1200                    Sequential.fold_init(items, init, identity, fold_op, reduce_op)
1201                }
1202                policy::RunExecution::Parallel => self.thread_pool.install(|| {
1203                    items
1204                        .into_par_iter()
1205                        .fold(
1206                            || (init(), identity()),
1207                            |(mut init_val, acc), item| {
1208                                let new_acc = fold_op(acc, &mut init_val, item);
1209                                (init_val, new_acc)
1210                            },
1211                        )
1212                        .map(|(_, acc)| acc)
1213                        .reduce(&identity, reduce_op)
1214                }),
1215            })
1216        }
1217
1218        #[track_caller]
1219        fn map_collect_vec<I, F, T>(&self, iter: I, map_op: F) -> Vec<T>
1220        where
1221            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1222            F: Fn(I::Item) -> T + Send + Sync,
1223            T: Send,
1224        {
1225            let items: Vec<I::Item> = iter.into_iter().collect();
1226            self.execute(items.len(), 1, |execution| match execution {
1227                policy::RunExecution::Serial => Sequential.map_collect_vec(items, map_op),
1228                policy::RunExecution::Parallel => self
1229                    .thread_pool
1230                    .install(|| items.into_par_iter().map(map_op).collect()),
1231            })
1232        }
1233
1234        #[track_caller]
1235        fn try_map_collect_vec<I, F, T, E>(&self, iter: I, map_op: F) -> Result<Vec<T>, E>
1236        where
1237            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1238            F: Fn(I::Item) -> Result<T, E> + Send + Sync,
1239            T: Send,
1240            E: Send,
1241        {
1242            let items: Vec<I::Item> = iter.into_iter().collect();
1243            self.try_execute(items.len(), 1, |execution| match execution {
1244                policy::RunExecution::Serial => Sequential.try_map_collect_vec(items, map_op),
1245                policy::RunExecution::Parallel => self
1246                    .thread_pool
1247                    .install(|| items.into_par_iter().map(map_op).collect()),
1248            })
1249        }
1250
1251        #[track_caller]
1252        fn map_init_collect_vec<I, INIT, T, F, R>(&self, iter: I, init: INIT, map_op: F) -> Vec<R>
1253        where
1254            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1255            INIT: Fn() -> T + Send + Sync,
1256            T: Send,
1257            F: Fn(&mut T, I::Item) -> R + Send + Sync,
1258            R: Send,
1259        {
1260            let items: Vec<I::Item> = iter.into_iter().collect();
1261            self.execute(items.len(), 1, |execution| match execution {
1262                policy::RunExecution::Serial => Sequential.map_init_collect_vec(items, init, map_op),
1263                policy::RunExecution::Parallel => self
1264                    .thread_pool
1265                    .install(|| items.into_par_iter().map_init(init, map_op).collect()),
1266            })
1267        }
1268
1269        #[track_caller]
1270        fn map_init_collect_vec_with_multiplier<I, INIT, T, F, R>(
1271            &self,
1272            iter: I,
1273            multiplier: usize,
1274            init: INIT,
1275            map_op: F,
1276        ) -> Vec<R>
1277        where
1278            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1279            INIT: Fn() -> T + Send + Sync,
1280            T: Send,
1281            F: Fn(&mut T, I::Item) -> R + Send + Sync,
1282            R: Send,
1283        {
1284            let items: Vec<I::Item> = iter.into_iter().collect();
1285            self.execute(items.len(), multiplier, |execution| match execution {
1286                policy::RunExecution::Serial => Sequential.map_init_collect_vec(items, init, map_op),
1287                policy::RunExecution::Parallel => self
1288                    .thread_pool
1289                    .install(|| items.into_par_iter().map_init(init, map_op).collect()),
1290            })
1291        }
1292
1293        #[track_caller]
1294        fn try_fold<I, R, E, ID, F, RD>(
1295            &self,
1296            iter: I,
1297            identity: ID,
1298            fold_op: F,
1299            reduce_op: RD,
1300        ) -> Result<R, E>
1301        where
1302            I: IntoIterator<IntoIter: Send, Item: Send> + Send,
1303            R: Send,
1304            E: Send,
1305            ID: Fn() -> R + Send + Sync,
1306            F: Fn(R, I::Item) -> Result<R, E> + Send + Sync,
1307            RD: Fn(R, R) -> R + Send + Sync,
1308        {
1309            let items: Vec<I::Item> = iter.into_iter().collect();
1310            self.try_execute(items.len(), 1, |execution| match execution {
1311                policy::RunExecution::Serial => {
1312                    Sequential.try_fold(items, identity, fold_op, reduce_op)
1313                }
1314                policy::RunExecution::Parallel => self.thread_pool.install(|| {
1315                    items
1316                        .into_par_iter()
1317                        .try_fold(&identity, &fold_op)
1318                        .try_reduce(&identity, |a, b| Ok(reduce_op(a, b)))
1319                }),
1320            })
1321        }
1322
1323        fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
1324        where
1325            A: FnOnce() -> RA + Send,
1326            B: FnOnce() -> RB + Send,
1327            RA: Send,
1328            RB: Send,
1329        {
1330            self.thread_pool.install(|| rayon::join(a, b))
1331        }
1332
1333        #[track_caller]
1334        fn sort_by<T, C>(&self, items: &mut [T], compare: C)
1335        where
1336            T: Send,
1337            C: Fn(&T, &T) -> Ordering + Send + Sync,
1338        {
1339            self.execute(items.len(), 1, |execution| match execution {
1340                policy::RunExecution::Serial => Sequential.sort_by(items, compare),
1341                policy::RunExecution::Parallel => {
1342                    self.thread_pool.install(|| items.par_sort_by(compare))
1343                }
1344            });
1345        }
1346    }
1347});
1348commonware_macros::stability_scope!(ALPHA, cfg(any(feature = "test-utils", test)) {
1349    pub mod mocks;
1350});
1351
1352#[cfg(test)]
1353mod test {
1354    use crate::{Rayon, Sequential, Strategy};
1355    use core::num::NonZeroUsize;
1356    use futures::FutureExt;
1357    use proptest::prelude::*;
1358    use rayon::ThreadPoolBuilder;
1359    use std::sync::{
1360        Arc,
1361        atomic::{AtomicUsize, Ordering},
1362    };
1363
1364    fn parallel_strategy() -> Rayon {
1365        Rayon::new(NonZeroUsize::new(4).unwrap()).unwrap()
1366    }
1367
1368    /// Call `spawn` with this helper so the policy entry is keyed by the helper's call
1369    /// site (both `track_caller` locations resolve to the same line).
1370    #[track_caller]
1371    fn spawn_flagged(
1372        strategy: &Rayon,
1373        panics: bool,
1374    ) -> (
1375        &'static std::panic::Location<'static>,
1376        impl core::future::Future<Output = usize> + Send + 'static,
1377    ) {
1378        (
1379            std::panic::Location::caller(),
1380            strategy.spawn(64, move |_| {
1381                if panics {
1382                    panic!("job panic");
1383                }
1384                7
1385            }),
1386        )
1387    }
1388
1389    fn spawn_recorded(strategy: &Rayon, loc: &'static std::panic::Location<'static>) -> bool {
1390        let parallelism = strategy.manual().parallelism();
1391        strategy
1392            .policy
1393            .as_ref()
1394            .is_some_and(|policy| policy.spawn_recorded(loc, 64, parallelism))
1395    }
1396
1397    /// A panicking offloaded job must not update the spawn policy: its wall time says
1398    /// nothing about the job size and would train the policy toward inlining.
1399    #[test]
1400    fn spawn_panic_records_nothing() {
1401        let strategy = parallel_strategy();
1402
1403        let (loc, job) = spawn_flagged(&strategy, true);
1404        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1405            futures::executor::block_on(job)
1406        }));
1407        assert!(result.is_err());
1408        assert!(!spawn_recorded(&strategy, loc));
1409
1410        let (loc, job) = spawn_flagged(&strategy, false);
1411        assert_eq!(futures::executor::block_on(job), 7);
1412        assert!(spawn_recorded(&strategy, loc));
1413    }
1414
1415    /// Once the seed and boundary runs measure a trivial job cheaper than the pool hand-off,
1416    /// spawn places it inline on the calling (non-pool) thread.
1417    #[test]
1418    fn spawn_converges_inline_for_tiny_jobs() {
1419        let strategy = parallel_strategy();
1420
1421        for _ in 0..100 {
1422            let on_pool = futures::executor::block_on(
1423                strategy.spawn(64, |_| rayon::current_thread_index().is_some()),
1424            );
1425            if !on_pool {
1426                return;
1427            }
1428        }
1429        panic!("a trivial job never converged to inline placement");
1430    }
1431
1432    /// A job measured over the inline budget keeps offloading: the calling task is never blocked
1433    /// on a big job even when the hand-off looks expensive.
1434    #[test]
1435    fn spawn_keeps_offloading_big_jobs() {
1436        let strategy = parallel_strategy();
1437
1438        for _ in 0..20 {
1439            let on_pool = futures::executor::block_on(strategy.spawn(64, |_| {
1440                std::thread::sleep(std::time::Duration::from_millis(2));
1441                rayon::current_thread_index().is_some()
1442            }));
1443            assert!(
1444                on_pool,
1445                "a job over the inline budget ran on the calling task"
1446            );
1447        }
1448    }
1449
1450    fn policy_len(strategy: &Rayon) -> usize {
1451        strategy.policy.as_ref().map_or(0, |policy| policy.len())
1452    }
1453
1454    fn map_from_same_callsite(strategy: &Rayon, len: usize) {
1455        let _: Vec<_> = strategy.map_collect_vec(0..len, |x| x);
1456    }
1457
1458    fn map_init_with_multiplier_from_same_callsite(
1459        strategy: &Rayon,
1460        len: usize,
1461        multiplier: usize,
1462    ) {
1463        let _: Vec<_> =
1464            strategy.map_init_collect_vec_with_multiplier(0..len, multiplier, || (), |_, x| x);
1465    }
1466
1467    fn run_from_same_callsite(strategy: &Rayon, len: usize) {
1468        let _: usize = strategy.run(len, || 1, || 2);
1469    }
1470
1471    fn map_partition_from_same_callsite(strategy: &Rayon, len: usize) {
1472        let _: (Vec<_>, Vec<_>) = strategy.map_partition_collect_vec(0..len, |x| {
1473            if x % 2 == 0 { (x, Some(x)) } else { (x, None) }
1474        });
1475    }
1476
1477    #[test]
1478    fn adaptive_policy_is_scoped_to_rayon() {
1479        let strategy = parallel_strategy();
1480        let other = parallel_strategy();
1481
1482        let _: Vec<_> = strategy.map_collect_vec(0..16, |x| x);
1483
1484        assert_eq!(policy_len(&strategy), 1);
1485        assert_eq!(policy_len(&other), 0);
1486    }
1487
1488    /// A spawn awaited from a thread inside the pool must complete even when no other
1489    /// worker can run the job: the pool below registers this thread as a member and never
1490    /// starts its remaining worker, so only the spawn future's yield loop can execute the
1491    /// job (a single poll must suffice; there is no executor to re-poll a pending future).
1492    #[test]
1493    fn spawn_driven_inline_on_member_thread() {
1494        let pool = ThreadPoolBuilder::new()
1495            .num_threads(2)
1496            .use_current_thread()
1497            .spawn_handler(|_| Ok(()))
1498            .build()
1499            .unwrap();
1500        let strategy = Rayon::with_pool(Arc::new(pool));
1501
1502        let result = strategy
1503            .spawn(2, |strategy| strategy.map_collect_vec(0..2, |i| i + 1))
1504            .now_or_never()
1505            .expect("spawn should complete on first poll via the yield loop");
1506        assert_eq!(result, vec![1, 2]);
1507    }
1508
1509    #[test]
1510    fn with_parallelism_overrides_planning_parallelism() {
1511        let strategy = Rayon::new(NonZeroUsize::new(1).unwrap())
1512            .unwrap()
1513            .with_parallelism(NonZeroUsize::new(4).unwrap());
1514        let strategy = strategy.manual();
1515        assert_eq!(strategy.parallelism(), 4);
1516        assert_eq!(strategy.run(2, || "serial", || "parallel"), "parallel");
1517    }
1518
1519    #[test]
1520    fn adaptive_policy_is_shared_by_clones() {
1521        let strategy = parallel_strategy();
1522        let clone = strategy.clone();
1523
1524        let _: Vec<_> = clone.map_collect_vec(0..16, |x| x);
1525
1526        assert_eq!(policy_len(&strategy), 1);
1527        assert_eq!(policy_len(&clone), 1);
1528    }
1529
1530    #[test]
1531    fn adaptive_policy_records_all_adaptive_operations() {
1532        let strategy = parallel_strategy();
1533
1534        let _: Vec<_> = strategy.fold_init(
1535            0..16,
1536            || (),
1537            Vec::new,
1538            |mut acc, _, x| {
1539                acc.push(x);
1540                acc
1541            },
1542            |mut a, b| {
1543                a.extend(b);
1544                a
1545            },
1546        );
1547        let _: i32 = strategy.fold(0..16, || 0, |acc, x| acc + x, |a, b| a + b);
1548        let _: Result<i32, ()> = strategy.try_fold(0..16, || 0, |acc, x| Ok(acc + x), |a, b| a + b);
1549        let _: Vec<_> = strategy.map_collect_vec(0..16, |x| x);
1550        let _: Result<Vec<_>, ()> = strategy.try_map_collect_vec(0..16, Ok);
1551        let _: Vec<_> = strategy.map_init_collect_vec(
1552            0..16,
1553            || AtomicUsize::new(0),
1554            |counter, x| {
1555                counter.fetch_add(1, Ordering::Relaxed);
1556                x
1557            },
1558        );
1559        let _: Vec<_> = strategy.map_init_collect_vec_with_multiplier(
1560            0..16,
1561            2,
1562            || AtomicUsize::new(0),
1563            |counter, x| {
1564                counter.fetch_add(1, Ordering::Relaxed);
1565                x
1566            },
1567        );
1568        let _: usize = strategy.run(16, || 1, || 2);
1569        let _: (Vec<_>, Vec<_>) = strategy.map_partition_collect_vec(0..16, |x| {
1570            if x % 2 == 0 { (x, Some(x)) } else { (x, None) }
1571        });
1572        let _: (i32, i32) = strategy.join(|| 1, || 2);
1573        let mut sortable = vec![3, 2, 1];
1574        strategy.sort_by(&mut sortable, |a, b| a.cmp(b));
1575
1576        assert_eq!(sortable, vec![1, 2, 3]);
1577        assert_eq!(policy_len(&strategy), 10);
1578    }
1579
1580    #[test]
1581    fn adaptive_policy_buckets_by_input_size() {
1582        let strategy = parallel_strategy();
1583
1584        map_from_same_callsite(&strategy, 1);
1585        map_from_same_callsite(&strategy, 2);
1586        map_from_same_callsite(&strategy, 3);
1587
1588        assert_eq!(policy_len(&strategy), 2);
1589    }
1590
1591    #[test]
1592    fn adaptive_policy_buckets_by_work_multiplier() {
1593        let strategy = parallel_strategy();
1594
1595        map_init_with_multiplier_from_same_callsite(&strategy, 16, 1);
1596        map_init_with_multiplier_from_same_callsite(&strategy, 16, 2);
1597        map_init_with_multiplier_from_same_callsite(&strategy, 16, 3);
1598
1599        assert_eq!(policy_len(&strategy), 2);
1600    }
1601
1602    #[test]
1603    fn adaptive_run_buckets_by_input_size() {
1604        let strategy = parallel_strategy();
1605
1606        run_from_same_callsite(&strategy, 1);
1607        run_from_same_callsite(&strategy, 2);
1608        run_from_same_callsite(&strategy, 3);
1609
1610        assert_eq!(policy_len(&strategy), 2);
1611    }
1612
1613    /// `manual()` forces the hand-off on a multi-worker pool: the job runs on the pool no matter
1614    /// what the adaptive policy would have decided for this call site.
1615    #[test]
1616    fn manual_spawn_always_hands_off() {
1617        let strategy = parallel_strategy();
1618        let manual = strategy.manual();
1619
1620        for _ in 0..10 {
1621            let on_pool = futures::executor::block_on(
1622                manual.spawn(1, |_| rayon::current_thread_index().is_some()),
1623            );
1624            assert!(on_pool, "manual spawn ran on the calling task");
1625        }
1626    }
1627
1628    #[test]
1629    fn manual_strategy_does_not_use_adaptive_policy() {
1630        let strategy = parallel_strategy();
1631        let manual = strategy.manual();
1632
1633        let _: usize = manual.fold(0..4, || 0, |acc, x| acc + x, |a, b| a + b);
1634        assert_eq!(manual.run(4, || 1, || 2), 2);
1635
1636        assert_eq!(policy_len(&strategy), 0);
1637        assert_eq!(policy_len(&manual.strategy), 0);
1638    }
1639
1640    #[test]
1641    fn sequential_run_uses_serial_body() {
1642        assert_eq!(Sequential.run(4, || 1, || 2), 1);
1643    }
1644
1645    #[test]
1646    fn adaptive_policy_keys_default_methods_by_external_callsite() {
1647        let strategy = parallel_strategy();
1648
1649        // `fold` uses the trait's default body (no `Rayon` override), so this also guards that
1650        // `#[track_caller]` still attributes the policy key to the caller's line rather than the
1651        // default method body: two calls from distinct callsites must yield two distinct entries.
1652        let _: i32 = strategy.fold(0..16, || 0, |acc, x| acc + x, |a, b| a + b);
1653        let _: i32 = strategy.fold(0..16, || 0, |acc, x| acc + x, |a, b| a + b);
1654
1655        assert_eq!(policy_len(&strategy), 2);
1656    }
1657
1658    #[test]
1659    fn adaptive_policy_keys_partition_map_by_external_callsite() {
1660        let strategy = parallel_strategy();
1661
1662        map_partition_from_same_callsite(&strategy, 16);
1663        let _: (Vec<_>, Vec<_>) = strategy.map_partition_collect_vec(0..16, |x| {
1664            if x % 2 == 0 { (x, Some(x)) } else { (x, None) }
1665        });
1666
1667        assert_eq!(policy_len(&strategy), 2);
1668    }
1669
1670    #[test]
1671    fn join_does_not_use_adaptive_policy() {
1672        let strategy = parallel_strategy();
1673
1674        let result = strategy.join(|| 1, || 2);
1675
1676        assert_eq!(result, (1, 2));
1677        assert_eq!(policy_len(&strategy), 0);
1678    }
1679
1680    #[test]
1681    fn sequential_spawn_runs_job() {
1682        let result = futures::executor::block_on(Sequential.spawn(1, |_| 7));
1683
1684        assert_eq!(result, 7);
1685    }
1686
1687    #[test]
1688    fn rayon_spawn_runs_job_on_pool() {
1689        let strategy = parallel_strategy();
1690
1691        let result = futures::executor::block_on(strategy.spawn(1, |_| {
1692            assert!(rayon::current_thread_index().is_some());
1693            7
1694        }));
1695
1696        assert_eq!(result, 7);
1697
1698        // Spawn trains only the spawn-side policy: no run entries are created.
1699        assert_eq!(policy_len(&strategy), 0);
1700    }
1701
1702    #[test]
1703    fn rayon_spawn_runs_inline_on_current_thread_single_worker_pool() {
1704        let pool = ThreadPoolBuilder::new()
1705            .num_threads(1)
1706            .use_current_thread()
1707            .build()
1708            .unwrap();
1709        let strategy =
1710            Rayon::with_pool(Arc::new(pool)).with_parallelism(NonZeroUsize::new(4).unwrap());
1711
1712        assert_eq!(strategy.manual().parallelism(), 4);
1713
1714        let result = strategy.spawn(1, |_| 7).now_or_never();
1715
1716        assert_eq!(result, Some(7));
1717        assert_eq!(policy_len(&strategy), 0);
1718    }
1719
1720    #[test]
1721    #[should_panic(expected = "boom")]
1722    fn rayon_spawn_propagates_job_panic() {
1723        // A panic on a pool worker must surface at the await point, not abort the process.
1724        let strategy = parallel_strategy();
1725
1726        let _: () = futures::executor::block_on(strategy.spawn(1, |_| panic!("boom")));
1727    }
1728
1729    #[test]
1730    #[should_panic(expected = "boom")]
1731    fn sequential_spawn_propagates_job_panic() {
1732        let _: () = futures::executor::block_on(Sequential.spawn(1, |_| panic!("boom")));
1733    }
1734
1735    proptest! {
1736        #[test]
1737        fn parallel_fold_init_matches_sequential(data in prop::collection::vec(any::<i32>(), 0..500)) {
1738            let sequential = Sequential;
1739            let parallel = parallel_strategy();
1740
1741            let seq_result: Vec<i32> = sequential.fold_init(
1742                &data,
1743                || (),
1744                Vec::new,
1745                |mut acc, _, &x| { acc.push(x.wrapping_mul(2)); acc },
1746                |mut a, b| { a.extend(b); a },
1747            );
1748
1749            let par_result: Vec<i32> = parallel.fold_init(
1750                &data,
1751                || (),
1752                Vec::new,
1753                |mut acc, _, &x| { acc.push(x.wrapping_mul(2)); acc },
1754                |mut a, b| { a.extend(b); a },
1755            );
1756
1757            prop_assert_eq!(seq_result, par_result);
1758        }
1759
1760        #[test]
1761        fn fold_equals_fold_init(data in prop::collection::vec(any::<i32>(), 0..500)) {
1762            let s = Sequential;
1763
1764            let via_fold: Vec<i32> = s.fold(
1765                &data,
1766                Vec::new,
1767                |mut acc, &x| { acc.push(x); acc },
1768                |mut a, b| { a.extend(b); a },
1769            );
1770
1771            let via_fold_init: Vec<i32> = s.fold_init(
1772                &data,
1773                || (),
1774                Vec::new,
1775                |mut acc, _, &x| { acc.push(x); acc },
1776                |mut a, b| { a.extend(b); a },
1777            );
1778
1779            prop_assert_eq!(via_fold, via_fold_init);
1780        }
1781
1782        #[test]
1783        fn parallel_try_fold_matches_sequential(data in prop::collection::vec(any::<i32>(), 0..500)) {
1784            let sequential: Result<i32, ()> = Sequential.try_fold(
1785                &data,
1786                || 0i32,
1787                |acc, &x| Ok(acc.wrapping_add(x)),
1788                |a, b| a.wrapping_add(b),
1789            );
1790            let parallel: Result<i32, ()> = parallel_strategy().try_fold(
1791                &data,
1792                || 0i32,
1793                |acc, &x| Ok(acc.wrapping_add(x)),
1794                |a, b| a.wrapping_add(b),
1795            );
1796
1797            prop_assert_eq!(sequential, parallel);
1798        }
1799
1800        #[test]
1801        fn map_collect_vec_equals_fold(data in prop::collection::vec(any::<i32>(), 0..500)) {
1802            let s = Sequential;
1803            let map_op = |&x: &i32| x.wrapping_mul(3);
1804
1805            let via_map: Vec<i32> = s.map_collect_vec(&data, map_op);
1806
1807            let via_fold: Vec<i32> = s.fold(
1808                &data,
1809                Vec::new,
1810                |mut acc, item| { acc.push(map_op(item)); acc },
1811                |mut a, b| { a.extend(b); a },
1812            );
1813
1814            prop_assert_eq!(via_map, via_fold);
1815        }
1816
1817        #[test]
1818        fn try_map_collect_vec_collects_successes(data in prop::collection::vec(any::<i32>(), 0..500)) {
1819            let expected: Vec<i32> = data.iter().map(|x| x.wrapping_mul(5)).collect();
1820
1821            let sequential: Result<Vec<i32>, ()> =
1822                Sequential.try_map_collect_vec(&data, |&x| Ok(x.wrapping_mul(5)));
1823            prop_assert_eq!(sequential, Ok(expected.clone()));
1824
1825            let parallel: Result<Vec<i32>, ()> =
1826                parallel_strategy().try_map_collect_vec(&data, |&x| Ok(x.wrapping_mul(5)));
1827            prop_assert_eq!(parallel, Ok(expected));
1828        }
1829
1830        #[test]
1831        fn try_map_collect_vec_returns_first_error(data in prop::collection::vec(any::<i32>(), 0..500)) {
1832            let expected_error = data.iter().position(|x| x % 7 == 0);
1833            let result: Result<Vec<i32>, usize> =
1834                Sequential.try_map_collect_vec(data.iter().enumerate(), |(i, &x)| {
1835                    if x % 7 == 0 {
1836                        Err(i)
1837                    } else {
1838                        Ok(x)
1839                    }
1840                });
1841
1842            match expected_error {
1843                Some(i) => prop_assert_eq!(result, Err(i)),
1844                None => prop_assert_eq!(result, Ok(data)),
1845            }
1846        }
1847
1848        #[test]
1849        fn map_init_collect_vec_equals_fold_init(data in prop::collection::vec(any::<i32>(), 0..500)) {
1850            let s = Sequential;
1851
1852            let via_map: Vec<i32> = s.map_init_collect_vec(
1853                &data,
1854                || 0i32,
1855                |counter, &x| { *counter += 1; x.wrapping_add(*counter) },
1856            );
1857
1858            let via_fold_init: Vec<i32> = s.fold_init(
1859                &data,
1860                || 0i32,
1861                Vec::new,
1862                |mut acc, counter, &x| {
1863                    *counter += 1;
1864                    acc.push(x.wrapping_add(*counter));
1865                    acc
1866                },
1867                |mut a, b| { a.extend(b); a },
1868            );
1869
1870            prop_assert_eq!(via_map, via_fold_init);
1871        }
1872
1873        #[test]
1874        fn map_partition_collect_vec_returns_valid_results(data in prop::collection::vec(any::<i32>(), 0..500)) {
1875            let s = Sequential;
1876
1877            let map_op = |&x: &i32| {
1878                let value = if x % 2 == 0 { Some(x.wrapping_mul(2)) } else { None };
1879                (x, value)
1880            };
1881
1882            let (results, filtered) = s.map_partition_collect_vec(data.iter(), map_op);
1883
1884            // Verify results contains doubled even numbers
1885            let expected_results: Vec<i32> = data.iter().filter(|&&x| x % 2 == 0).map(|&x| x.wrapping_mul(2)).collect();
1886            prop_assert_eq!(results, expected_results);
1887
1888            // Verify filtered contains odd numbers
1889            let expected_filtered: Vec<i32> = data.iter().filter(|&&x| x % 2 != 0).copied().collect();
1890            prop_assert_eq!(filtered, expected_filtered);
1891        }
1892    }
1893
1894    #[test]
1895    fn try_map_collect_vec_sequential_short_circuits() {
1896        let calls = AtomicUsize::new(0);
1897        let result: Result<Vec<usize>, usize> = Sequential.try_map_collect_vec(0..10, |i| {
1898            calls.fetch_add(1, Ordering::Relaxed);
1899            if i == 3 { Err(i) } else { Ok(i) }
1900        });
1901
1902        assert_eq!(result, Err(3));
1903        assert_eq!(calls.load(Ordering::Relaxed), 4);
1904    }
1905
1906    #[test]
1907    fn try_map_collect_vec_parallel_returns_an_error() {
1908        let result: Result<Vec<usize>, usize> = parallel_strategy()
1909            .try_map_collect_vec(0..128, |i| if i == 17 || i == 42 { Err(i) } else { Ok(i) });
1910
1911        assert!(matches!(result, Err(17 | 42)));
1912    }
1913}