use std::convert::Infallible;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicBool, AtomicUsize, Ordering},
};
use crossbeam_channel::Sender;
use crate::{
collator::Collator,
dataset::Dataset,
error::Result,
transform::{Transform, apply_all},
};
pub(super) type WorkerMsg<B> = (usize, std::thread::Result<Result<B>>);
pub(super) fn process_batch<D, T, C>(
dataset: &D,
transform: &T,
indices: &[usize],
collator: &C,
item_concurrency: usize,
pool: Option<&rayon::ThreadPool>,
) -> Result<C::Batch>
where
D: Dataset,
T: Transform<D::Item>,
C: Collator<T::Output>,
{
let items = match (pool, indices) {
(Some(_), &[index]) if item_concurrency > 1 => vec![load(dataset, transform, index)?],
(Some(pool), _) if item_concurrency > 1 => {
fetch_in_lanes(dataset, transform, indices, item_concurrency, pool)?
}
_ => apply_all(transform, indices, dataset.get_batch(indices)?)?,
};
collator.collate(items)
}
fn load<D: Dataset, T: Transform<D::Item>>(
dataset: &D,
transform: &T,
index: usize,
) -> Result<T::Output> {
transform.apply(index, dataset.get(index)?)
}
fn fetch_in_lanes<D: Dataset, T: Transform<D::Item>>(
dataset: &D,
transform: &T,
indices: &[usize],
lanes: usize,
pool: &rayon::ThreadPool,
) -> Result<Vec<T::Output>> {
let next = AtomicUsize::new(0);
let failed = AtomicBool::new(false);
let slots: Vec<Mutex<Option<Result<T::Output>>>> =
indices.iter().map(|_| Mutex::new(None)).collect();
pool.scope(|scope| {
for _ in 0..lanes.min(indices.len()) {
scope.spawn(|_| {
while !failed.load(Ordering::Relaxed) {
let position = next.fetch_add(1, Ordering::Relaxed);
let Some(&index) = indices.get(position) else {
break;
};
let item = load(dataset, transform, index);
if item.is_err() {
failed.store(true, Ordering::Relaxed);
}
*slots[position]
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(item);
}
});
}
});
let mut items = Vec::with_capacity(indices.len());
for slot in slots {
match slot.into_inner().unwrap_or_else(PoisonError::into_inner) {
Some(Ok(item)) => items.push(item),
Some(Err(e)) => return Err(e),
None => {}
}
}
Ok(items)
}
pub(super) struct Epoch<D, T, C>
where
D: Dataset,
T: Transform<D::Item>,
C: Collator<T::Output>,
{
dataset: Arc<D>,
transform: Arc<T>,
collator: Arc<C>,
item_concurrency: usize,
pool: Arc<rayon::ThreadPool>,
chunks: Vec<Vec<usize>>,
next_claim: AtomicUsize,
next_out: AtomicUsize,
window: usize,
tasks: AtomicUsize,
max_tasks: usize,
cancelled: AtomicBool,
result_tx: Sender<WorkerMsg<C::Batch>>,
}
impl<D, T, C> Epoch<D, T, C>
where
D: Dataset,
T: Transform<D::Item>,
C: Collator<T::Output>,
C::Batch: Send + 'static,
{
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
dataset: Arc<D>,
transform: Arc<T>,
collator: Arc<C>,
item_concurrency: usize,
pool: Arc<rayon::ThreadPool>,
chunks: Vec<Vec<usize>>,
window: usize,
max_tasks: usize,
result_tx: Sender<WorkerMsg<C::Batch>>,
) -> Self {
Self {
dataset,
transform,
collator,
item_concurrency,
pool,
chunks,
next_claim: AtomicUsize::new(0),
next_out: AtomicUsize::new(0),
window,
tasks: AtomicUsize::new(0),
max_tasks,
cancelled: AtomicBool::new(false),
result_tx,
}
}
pub(super) fn len(&self) -> usize {
self.chunks.len()
}
fn claimable(&self, claim: usize) -> bool {
!self.cancelled.load(Ordering::SeqCst)
&& claim < self.chunks.len()
&& claim < self.next_out.load(Ordering::SeqCst) + self.window
}
fn claim(&self) -> Option<usize> {
self.next_claim
.try_update(Ordering::SeqCst, Ordering::SeqCst, |claim| {
self.claimable(claim).then_some(claim + 1)
})
.ok()
}
fn reserve_task(&self) -> bool {
self.claimable(self.next_claim.load(Ordering::SeqCst))
&& self
.tasks
.try_update(Ordering::SeqCst, Ordering::SeqCst, |running| {
(running < self.max_tasks).then_some(running + 1)
})
.is_ok()
}
pub(super) fn spawn_tasks(self: &Arc<Self>, alive: &Sender<Infallible>) {
while self.reserve_task() {
let epoch = Arc::clone(self);
let alive = alive.clone();
self.pool.spawn(move || {
epoch.run();
drop(epoch);
drop(alive);
});
}
}
pub(super) fn advance(self: &Arc<Self>, next_out: usize, alive: &Sender<Infallible>) {
self.next_out.store(next_out, Ordering::SeqCst);
self.spawn_tasks(alive);
}
pub(super) fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
fn run(&self) {
loop {
while let Some(batch_idx) = self.claim() {
let result = catch_unwind(AssertUnwindSafe(|| {
process_batch(
&*self.dataset,
&*self.transform,
&self.chunks[batch_idx],
&*self.collator,
self.item_concurrency,
Some(&self.pool),
)
}));
let panicked = result.is_err();
if self.result_tx.send((batch_idx, result)).is_err() || panicked {
self.cancel();
}
}
self.tasks.fetch_sub(1, Ordering::SeqCst);
if !self.reserve_task() {
return;
}
}
}
}