use super::plan::BlockPlan;
use crate::ExecutionContext;
use rayon::prelude::*;
use std::fmt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct WorkerPanicked;
impl fmt::Display for WorkerPanicked {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a worker thread panicked")
}
}
impl std::error::Error for WorkerPanicked {}
#[cfg(test)]
pub(crate) fn map_blocks<T, F>(
plan: BlockPlan,
workers: usize,
block: F,
) -> Result<Vec<T>, WorkerPanicked>
where
T: Send,
F: Fn(usize, std::ops::Range<usize>) -> T + Sync,
{
SharedPool::global().map(plan, workers, block)
}
pub fn map_blocks_in<T, F>(
plan: BlockPlan,
context: &ExecutionContext,
block: F,
) -> Result<Vec<T>, WorkerPanicked>
where
T: Send,
F: Fn(usize, std::ops::Range<usize>) -> T + Sync,
{
context.executor().map(plan, context.worker_budget(), block)
}
pub(crate) struct SharedPool {
pub(super) pool: Option<rayon::ThreadPool>,
capacity: usize,
}
impl fmt::Debug for SharedPool {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SharedPool")
.field("capacity", &self.capacity)
.finish_non_exhaustive()
}
}
impl SharedPool {
pub(crate) fn global() -> Arc<Self> {
static SHARED: OnceLock<Arc<SharedPool>> = OnceLock::new();
Arc::clone(SHARED.get_or_init(|| Arc::new(Self::build())))
}
fn build() -> Self {
let requested = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(requested)
.thread_name(|index| format!("molframe-worker-{index}"))
.build()
.ok();
let capacity = pool
.as_ref()
.map_or(1, rayon::ThreadPool::current_num_threads);
Self { pool, capacity }
}
pub(crate) const fn capacity(&self) -> usize {
self.capacity
}
pub(crate) fn map<T, F>(
&self,
plan: BlockPlan,
workers: usize,
block: F,
) -> Result<Vec<T>, WorkerPanicked>
where
T: Send,
F: Fn(usize, std::ops::Range<usize>) -> T + Sync,
{
let mut output = Vec::new();
let window = plan
.useful_workers(workers.min(self.capacity))
.saturating_mul(2);
for start in (0..plan.blocks()).step_by(window) {
output.extend(self.map_window(
plan,
start..start.saturating_add(window).min(plan.blocks()),
workers,
&block,
)?);
}
Ok(output)
}
pub(crate) fn map_window<T, F>(
&self,
plan: BlockPlan,
indices: std::ops::Range<usize>,
workers: usize,
block: &F,
) -> Result<Vec<T>, WorkerPanicked>
where
T: Send,
F: Fn(usize, std::ops::Range<usize>) -> T + Sync,
{
let workers = workers.max(1).min(self.capacity).min(indices.len());
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if workers <= 1 || self.pool.is_none() || rayon::current_thread_index().is_some() {
return indices
.filter_map(|index| plan.range(index).map(|range| block(index, range)))
.collect();
}
let slots: Vec<_> = indices.clone().map(|_| Mutex::new(None)).collect();
let next = AtomicUsize::new(indices.start);
if let Some(pool) = &self.pool {
pool.install(|| {
(0..workers).into_par_iter().for_each(|_| {
loop {
let index = next.fetch_add(1, Ordering::Relaxed);
if index >= indices.end {
break;
}
let Some(range) = plan.range(index) else {
break;
};
let value = block(index, range);
let mut slot = match slots[index - indices.start].lock() {
Ok(slot) => slot,
Err(poisoned) => poisoned.into_inner(),
};
*slot = Some(value);
}
});
});
}
slots
.into_iter()
.filter_map(|slot| match slot.into_inner() {
Ok(value) => value,
Err(poisoned) => poisoned.into_inner(),
})
.collect()
}))
.map_err(|_| WorkerPanicked)
}
}
#[cfg(test)]
#[path = "execute_tests.rs"]
mod tests;