const HAS_THREADS: bool = !cfg!(target_family = "wasm");
const PARALLEL_MIN: usize = 20_000;
const MAX_THREADS: usize = 8;
pub(crate) fn threads_for(total: usize) -> usize {
if total < PARALLEL_MIN || !HAS_THREADS {
1
} else {
std::thread::available_parallelism().map_or(1, |n| n.get()).min(MAX_THREADS)
}
}
pub(crate) fn runs<T>(items: &[T], threads: usize, weight: impl Fn(&T) -> usize) -> Vec<&[T]> {
let total: usize = items.iter().map(&weight).sum();
let per_thread = total.div_ceil(threads.max(1)).max(1);
let mut out = Vec::with_capacity(threads);
let (mut start, mut size) = (0, 0);
for (i, item) in items.iter().enumerate() {
size += weight(item);
if size >= per_thread {
out.push(&items[start..=i]);
(start, size) = (i + 1, 0);
}
}
if start < items.len() {
out.push(&items[start..]);
}
out
}
pub(crate) fn par_map<T: Sync, R: Send>(runs: &[&[T]], f: impl Fn(&[T]) -> R + Sync) -> Vec<R> {
match runs {
[one] => vec![f(one)],
_ if !HAS_THREADS => runs.iter().map(|run| f(run)).collect(),
_ => std::thread::scope(|s| {
let handles: Vec<_> = runs.iter().map(|run| s.spawn(|| f(run))).collect();
handles.into_iter().map(|h| h.join().expect("validation thread panicked")).collect()
}),
}
}
pub(crate) fn par_concat<T: Sync, R: Send>(runs: &[&[T]], f: impl Fn(&[T]) -> Vec<R> + Sync) -> Vec<R> {
par_map(runs, f).into_iter().flatten().collect()
}
pub(crate) fn par_each<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> R + Sync) -> Vec<R> {
if !HAS_THREADS {
return items.iter().map(f).collect();
}
std::thread::scope(|s| {
let handles: Vec<_> = items.iter().map(|item| s.spawn(|| f(item))).collect();
handles.into_iter().map(|h| h.join().expect("validation thread panicked")).collect()
})
}
pub(crate) fn par_two<A: Send, B: Send>(first: impl FnOnce() -> A + Send, second: impl FnOnce() -> B) -> (A, B) {
if !HAS_THREADS {
let a = first();
return (a, second());
}
std::thread::scope(|s| {
let first = s.spawn(first);
let b = second();
(first.join().expect("validation thread panicked"), b)
})
}
pub(crate) fn par_groups(
dataset: &cimmodel::CimDataset,
groups: &[fn(&cimmodel::CimDataset) -> Vec<crate::Violation>],
) -> Vec<crate::Violation> {
if !HAS_THREADS {
return groups.iter().flat_map(|g| g(dataset)).collect();
}
std::thread::scope(|s| {
let handles: Vec<_> = groups.iter().map(|g| s.spawn(move || g(dataset))).collect();
handles
.into_iter()
.flat_map(|h| h.join().expect("validation thread panicked"))
.collect()
})
}
#[cfg(test)]
mod tests {
use super::runs;
#[test]
fn runs_cover_the_input_in_order() {
let items: Vec<usize> = (0..1000).collect();
for threads in [1, 2, 3, 7, 8, 16, 2000] {
let rs = runs(&items, threads, |_| 1);
assert!(rs.len() <= threads.max(1), "{threads} threads gave {} runs", rs.len());
assert!(rs.iter().all(|r| !r.is_empty()));
let joined: Vec<usize> = rs.concat();
assert_eq!(joined, items, "{threads} threads");
}
}
#[test]
fn runs_balance_by_weight() {
let items = [100, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
let rs = runs(&items, 2, |w| *w);
assert_eq!(rs.concat(), items);
assert_eq!(rs[0], &[100]);
}
#[test]
fn empty_input_has_no_runs() {
assert!(runs::<u8>(&[], 4, |_| 1).is_empty());
}
}