#[derive(Debug, Clone, Copy)]
pub(crate) struct ChunkCut {
max_items: usize,
max_weight: u64,
}
impl ChunkCut {
pub(crate) const fn new(max_items: usize, max_weight: u64) -> Self {
Self { max_items, max_weight }
}
fn boundary<T>(&self, items: &[T], start: usize, weigh: &impl Fn(&T) -> u64) -> usize {
let mut weight = 0u64;
for (offset, item) in items[start..].iter().enumerate() {
if offset > 0 && offset >= self.max_items {
return start + offset;
}
weight = weight.saturating_add(weigh(item));
if weight >= self.max_weight {
return start + offset + 1;
}
}
items.len()
}
}
pub(crate) fn drive_chunks<T, R>(
items: &[T],
cut: ChunkCut,
weigh: impl Fn(&T) -> u64,
mut process: impl FnMut(&[T]) -> Vec<R>,
mut absorb: impl FnMut(Vec<R>),
) {
drive_chunks_governed(
&mut (),
items,
|| Some(cut),
|(), item| weigh(item),
|(), chunk| process(chunk),
|(), produced| absorb(produced),
);
}
pub(crate) fn drive_chunks_governed<C, T, R>(
ctx: &mut C,
items: &[T],
mut next_cut: impl FnMut() -> Option<ChunkCut>,
weigh: impl Fn(&C, &T) -> u64,
mut process: impl FnMut(&C, &[T]) -> Vec<R>,
mut absorb: impl FnMut(&mut C, Vec<R>),
) {
let mut start = 0usize;
while start < items.len() {
let Some(cut) = next_cut() else {
return;
};
let end = {
let ctx: &C = ctx;
cut.boundary(items, start, &|item: &T| weigh(ctx, item))
};
let produced = process(ctx, &items[start..end]);
absorb(ctx, produced);
start = end;
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
fn chunk_lengths<T>(items: &[T], cut: ChunkCut, weigh: impl Fn(&T) -> u64) -> Vec<usize> {
let mut lengths = Vec::new();
drive_chunks(
items,
cut,
weigh,
|chunk| {
lengths.push(chunk.len());
Vec::<()>::new()
},
|_| {},
);
lengths
}
#[test]
fn the_weight_budget_cuts_chunks_the_count_bound_would_not() {
let heavy: Vec<u64> = vec![4 * 1024 * 1024; 8];
let cut = ChunkCut::new(1024, 8 * 1024 * 1024);
let lengths = chunk_lengths(&heavy, cut, |bytes| *bytes);
assert_eq!(
lengths,
vec![2, 2, 2, 2],
"eight 4 MiB items under an 8 MiB budget must cut every two items, not once at 1024"
);
}
#[test]
fn the_count_bound_governs_when_items_are_light() {
let light: Vec<u64> = vec![16; 10];
let lengths = chunk_lengths(&light, ChunkCut::new(4, 8 * 1024 * 1024), |bytes| *bytes);
assert_eq!(lengths, vec![4, 4, 2]);
}
#[test]
fn an_item_heavier_than_the_budget_forms_a_chunk_of_one() {
let items: Vec<u64> = vec![u64::MAX, 1, 1];
let lengths = chunk_lengths(&items, ChunkCut::new(1024, 1024), |bytes| *bytes);
assert_eq!(lengths, vec![1, 2]);
}
#[test]
fn every_item_is_processed_exactly_once_in_order() {
let items: Vec<u32> = (0..37).collect();
let mut seen: Vec<u32> = Vec::new();
drive_chunks(
&items,
ChunkCut::new(5, 64),
|_| 8,
|chunk| chunk.to_vec(),
|produced| seen.extend(produced),
);
assert_eq!(seen, items);
}
#[test]
fn a_governed_drive_re_cuts_before_every_chunk() {
let items: Vec<u32> = (0..15).collect();
let mut budget = 8usize;
let mut lengths: Vec<usize> = Vec::new();
drive_chunks_governed(
&mut lengths,
&items,
|| {
let cut = ChunkCut::new(budget, u64::MAX);
budget = (budget / 2).max(1);
Some(cut)
},
|_, _| 0,
|_, chunk| vec![chunk.len()],
|lengths: &mut Vec<usize>, produced| lengths.extend(produced),
);
assert_eq!(lengths, vec![8, 4, 2, 1]);
}
#[test]
fn a_governed_drive_stops_when_the_cut_is_withdrawn() {
let items: Vec<u32> = (0..100).collect();
let mut driven = 0usize;
let mut cuts = 0usize;
drive_chunks_governed(
&mut driven,
&items,
|| {
cuts += 1;
(cuts <= 2).then(|| ChunkCut::new(5, u64::MAX))
},
|_, _| 0,
|_, chunk| vec![chunk.len()],
|driven: &mut usize, produced| *driven += produced.iter().sum::<usize>(),
);
assert_eq!(driven, 10, "the drive must stop at the first withdrawn cut");
}
struct LiveProbe;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
impl LiveProbe {
fn new() -> Self {
let live = LIVE.fetch_add(1, Ordering::Relaxed) + 1;
PEAK.fetch_max(live, Ordering::Relaxed);
Self
}
}
impl Drop for LiveProbe {
fn drop(&mut self) {
LIVE.fetch_sub(1, Ordering::Relaxed);
}
}
#[test]
fn no_chunk_result_is_live_past_its_own_chunk() {
LIVE.store(0, Ordering::Relaxed);
PEAK.store(0, Ordering::Relaxed);
let items: Vec<u32> = (0..512).collect();
let cut = ChunkCut::new(16, u64::MAX);
drive_chunks(
&items,
cut,
|_| 1,
|chunk| chunk.iter().map(|_| LiveProbe::new()).collect(),
|produced| {
assert_eq!(
LIVE.load(Ordering::Relaxed),
produced.len(),
"absorb must see exactly its own chunk's results and no earlier chunk's"
);
},
);
assert_eq!(
LIVE.load(Ordering::Relaxed),
0,
"the last chunk's results must be dropped too"
);
assert_eq!(
PEAK.load(Ordering::Relaxed),
16,
"peak live results must be the chunk size, not the 512 items driven"
);
}
}