use std::sync::atomic::{AtomicUsize, Ordering};
static FREE: AtomicUsize = AtomicUsize::new(0);
static BUDGET: AtomicUsize = AtomicUsize::new(0);
pub fn set_threads(threads: usize) {
let helpers = threads.saturating_sub(1);
let previous = BUDGET.swap(helpers, Ordering::SeqCst);
if helpers >= previous {
FREE.fetch_add(helpers - previous, Ordering::SeqCst);
} else {
let _ = FREE.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |free| {
Some(free.saturating_sub(previous - helpers))
});
}
}
#[must_use]
pub fn budget() -> usize {
BUDGET.load(Ordering::SeqCst)
}
struct Lease(usize);
impl Lease {
fn take(wanted: usize) -> Self {
let mut taken = 0;
let _ = FREE.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |free| {
taken = free.min(wanted);
Some(free - taken)
});
Self(taken)
}
}
impl Drop for Lease {
fn drop(&mut self) {
FREE.fetch_add(self.0, Ordering::SeqCst);
}
}
pub fn run_parts<T, E, F>(parts: usize, part: F) -> Result<Vec<T>, E>
where
T: Send,
E: Send,
F: Fn(usize) -> Result<T, E> + Sync,
{
if parts <= 1 {
return (0..parts).map(&part).collect();
}
let lease = Lease::take(parts - 1);
if lease.0 == 0 {
return (0..parts).map(&part).collect();
}
let next = AtomicUsize::new(0);
let workers = lease.0 + 1;
let mut slots: Vec<Option<Result<T, E>>> = (0..parts).map(|_| None).collect();
let results = std::sync::Mutex::new(&mut slots);
let work = || loop {
let index = next.fetch_add(1, Ordering::Relaxed);
if index >= parts {
break;
}
let outcome = part(index);
let mut guard = results
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard[index] = Some(outcome);
};
let helper_reads = std::thread::scope(|scope| {
let mut helpers = Vec::with_capacity(workers - 1);
for _ in 1..workers {
helpers.push(scope.spawn(|| {
work();
crate::read_accounting::take_local()
}));
}
work();
helpers
.into_iter()
.map(|helper| {
helper
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
})
.collect::<Vec<_>>()
});
for reads in helper_reads {
crate::read_accounting::add_local(reads);
}
drop(lease);
slots
.into_iter()
.map(|slot| slot.expect("every part ran"))
.collect()
}
#[must_use]
pub fn split(len: u64, parts: usize) -> Vec<std::ops::Range<u64>> {
let parts = u64::try_from(parts.max(1)).unwrap_or(1).min(len.max(1));
let size = len.div_ceil(parts);
(0..parts)
.map(|i| (i * size)..((i + 1) * size).min(len))
.filter(|range| !range.is_empty())
.collect()
}
#[must_use]
pub fn parts_for(units: u64, min_units: u64) -> usize {
let by_size = usize::try_from(units / min_units.max(1)).unwrap_or(usize::MAX);
by_size.clamp(1, budget() + 1)
}
#[cfg(test)]
mod tests {
use super::*;
static BUDGET_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn exclusive() -> std::sync::MutexGuard<'static, ()> {
BUDGET_TESTS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[test]
fn parts_return_in_order_and_errors_by_lowest_part() {
let _serial = exclusive();
set_threads(4);
let out: Result<Vec<usize>, ()> = run_parts(16, |i| Ok(i * 2));
assert_eq!(out.unwrap(), (0..16).map(|i| i * 2).collect::<Vec<_>>());
let failed: Result<Vec<usize>, usize> =
run_parts(16, |i| if i % 5 == 3 { Err(i) } else { Ok(i) });
assert_eq!(failed, Err(3));
assert_eq!(FREE.load(Ordering::SeqCst), budget());
}
#[test]
fn reads_on_helper_threads_count_for_the_caller() {
let _serial = exclusive();
set_threads(4);
crate::read_accounting::enable(true);
let _ = crate::read_accounting::take_local();
let parts: Result<Vec<()>, ()> = run_parts(8, |part| {
crate::read_accounting::record("get", &[0x01, 0, 0, 0, 1, u8::try_from(part).unwrap()]);
Ok(())
});
parts.unwrap();
let counted: u64 = crate::read_accounting::take_local().values().sum();
assert_eq!(counted, 8, "every part's read is the caller's");
}
#[test]
fn split_covers_the_range_without_gaps() {
let ranges = split(10, 3);
assert_eq!(ranges, vec![0..4, 4..8, 8..10]);
assert_eq!(split(2, 8), vec![0..1, 1..2]);
assert!(split(0, 4).is_empty());
}
}