use std::ops::Range;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock, PoisonError, TryLockError};
use kurbo::{Affine, BezPath, PathEl, Rect, RoundedRect, Shape};
use peniko::Fill;
use frust_scene::{Command, DashPattern, PathStyle, Scene};
use vello_common::clip::PathDataRef;
use vello_common::fearless_simd::Level;
use vello_common::strip::Strip;
use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage};
use super::blur_rrect::inflated_bounds;
use super::cull::{generate_fill, generate_stroke, rounded_rect_elements};
use super::layers::RetainedStack;
use super::{
FLATTEN_TOLERANCE, check_finite, command_on_grid, dash_path, fast_rect, retained_entry,
round_stroke, rounded_rect_radii,
};
pub const DEFAULT_PARALLEL_WORKERS: usize = 3;
pub const DEFAULT_PARALLEL_MIN_COST: u64 = 8_000;
const JOB_BASE_COST: u64 = 64;
const MAX_RETAINED_SLOTS: usize = 1024;
const RETAINED_SLOT_STRIPS: usize = 256;
const RETAINED_SLOT_ALPHAS: usize = 16 * 1024;
pub const MAX_PARALLEL_WORKERS: usize = 8;
const CHUNKS_PER_HELPER: u64 = 8;
const MAX_ALPHA_INDEX: u32 = u32::MAX >> 1;
pub(crate) fn available_threads() -> usize {
static THREADS: OnceLock<usize> = OnceLock::new();
*THREADS.get_or_init(|| std::thread::available_parallelism().map_or(1, usize::from))
}
#[must_use]
pub(crate) fn default_workers() -> usize {
if crate::config::parallel_walk_disabled() {
1
} else {
crate::config::parallel_workers()
.unwrap_or(DEFAULT_PARALLEL_WORKERS)
.min(available_threads())
.max(1)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Geometry<'a> {
FastRect(Rect),
RectPath {
rect: Rect,
transform: Affine,
},
Rounded {
shape: RoundedRect,
transform: Affine,
},
Line {
line: [PathEl; 2],
width: f64,
transform: Affine,
},
Fill {
path: &'a BezPath,
transform: Affine,
},
Stroke {
path: &'a BezPath,
width: f64,
dash: Option<DashPattern>,
transform: Affine,
},
}
impl<'a> Geometry<'a> {
pub(crate) fn rect(rect: Rect, transform: Affine) -> Self {
match fast_rect(rect, transform) {
Some(device) => Self::FastRect(device),
None => Self::RectPath { rect, transform },
}
}
pub(crate) fn of(command: &'a Command, combined: Affine) -> Option<Self> {
let geometry = match command {
Command::FillRect {
rect, transform, ..
} => Self::rect(*rect, combined * *transform),
Command::RoundedRect {
rect,
radii,
transform,
..
} => Self::Rounded {
shape: RoundedRect::from_rect(*rect, rounded_rect_radii(*radii)),
transform: combined * *transform,
},
Command::Line {
p0,
p1,
width,
transform,
..
} => Self::Line {
line: [PathEl::MoveTo(*p0), PathEl::LineTo(*p1)],
width: *width,
transform: combined * *transform,
},
Command::Path {
path,
style,
transform,
..
} => match style {
PathStyle::Fill => Self::Fill {
path,
transform: combined * *transform,
},
PathStyle::Stroke { width, dash } => Self::Stroke {
path,
width: *width,
dash: dash.filter(DashPattern::is_effective),
transform: combined * *transform,
},
},
Command::Image {
dest, transform, ..
} => Self::rect(*dest, combined * *transform),
Command::BlurredRoundedRect {
rect,
std_dev,
transform,
..
} => Self::rect(inflated_bounds(*rect, *std_dev), combined * *transform),
_ => return None,
};
Some(geometry)
}
pub(crate) fn is_fast_rect(&self) -> bool {
matches!(self, Self::FastRect(_))
}
pub(crate) fn generate(
&self,
generator: &mut StripGenerator,
storage: &mut StripStorage,
clip: Option<PathDataRef<'_>>,
) {
match *self {
Self::FastRect(device) => {
generator.generate_filled_rect_fast(&device, storage, clip);
}
Self::RectPath { rect, transform } => {
generator.generate_filled_path(
rect.path_elements(FLATTEN_TOLERANCE),
Fill::NonZero,
transform,
None,
storage,
clip,
);
}
Self::Rounded { shape, transform } => {
generate_fill(
generator,
rounded_rect_elements(&shape),
transform,
storage,
clip,
);
}
Self::Line {
line,
width,
transform,
} => {
generate_stroke(
generator,
line,
&round_stroke(width),
transform,
storage,
clip,
);
}
Self::Fill { path, transform } => {
generate_fill(generator, path.iter(), transform, storage, clip);
}
Self::Stroke {
path,
width,
dash,
transform,
} => {
let stroke = round_stroke(width);
match dash {
Some(dash) => {
let dashed = dash_path(path, dash);
generate_stroke(
generator,
dashed.iter(),
&stroke,
transform,
storage,
clip,
);
}
None => {
generate_stroke(generator, path.iter(), &stroke, transform, storage, clip);
}
}
}
}
}
fn cost(&self, viewport: Rect) -> u64 {
let (bounds, transform, reach) = match *self {
Self::FastRect(device) => (device, Affine::IDENTITY, 0.0),
Self::RectPath { rect, transform } => (rect, transform, 0.0),
Self::Rounded { shape, transform } => (shape.rect(), transform, 0.0),
Self::Line {
line,
width,
transform,
} => (control_box(line.iter().copied()), transform, width),
Self::Fill { path, transform } => (control_box(path.iter()), transform, 0.0),
Self::Stroke {
path,
width,
transform,
..
} => (control_box(path.iter()), transform, width),
};
let device = transform
.transform_rect_bbox(bounds.inflate(reach, reach))
.intersect(viewport);
let extent = (device.width().max(0.0) + device.height().max(0.0)).min(1e9);
JOB_BASE_COST.saturating_add(extent as u64)
}
}
fn control_box(elements: impl Iterator<Item = PathEl>) -> Rect {
let mut bounds: Option<Rect> = None;
let mut include = |point: kurbo::Point| {
bounds = Some(bounds.map_or(Rect::from_points(point, point), |b| b.union_pt(point)));
};
for element in elements {
match element {
PathEl::MoveTo(p) | PathEl::LineTo(p) => include(p),
PathEl::QuadTo(p1, p2) => {
include(p1);
include(p2);
}
PathEl::CurveTo(p1, p2, p3) => {
include(p1);
include(p2);
include(p3);
}
PathEl::ClosePath => {}
}
}
bounds.unwrap_or(Rect::ZERO)
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Job<'a> {
command: usize,
combined: Affine,
geometry: Geometry<'a>,
cost: u64,
}
#[derive(Debug, Default)]
pub(crate) struct Plan<'a> {
jobs: Vec<Job<'a>>,
cost: u64,
}
impl Plan<'_> {
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.jobs.is_empty()
}
#[must_use]
pub(crate) fn cost(&self) -> u64 {
self.cost
}
}
#[cfg(test)]
pub(crate) fn plan<'a>(scene: &'a Scene, root: Affine, viewport: Rect) -> Plan<'a> {
plan_with(scene, root, viewport, false)
}
pub(crate) fn plan_with<'a>(
scene: &'a Scene,
root: Affine,
viewport: Rect,
retain: bool,
) -> Plan<'a> {
let mut plan = Plan::default();
let mut retained_stack = RetainedStack::new();
let mut brackets: Vec<bool> = Vec::new();
let mut masks = 0_usize;
let push = |brackets: &mut Vec<bool>, masks: &mut usize, mask: bool| {
brackets.push(mask);
*masks += usize::from(mask);
};
let pop = |brackets: &mut Vec<bool>, masks: &mut usize| {
if brackets.pop() == Some(true) {
*masks = masks.saturating_sub(1);
}
};
for (index, command) in scene.commands().iter().enumerate() {
match command {
Command::GlyphRun(_)
| Command::ClearRect { .. }
| Command::ShaderQuad { .. }
| Command::SceneTexture { .. }
| Command::DrawRetained { .. }
| Command::BackdropBlur { .. } => continue,
Command::FillRect { .. }
| Command::RoundedRect { .. }
| Command::Line { .. }
| Command::Image { .. }
| Command::BlurredRoundedRect { .. }
| Command::Path { .. }
| Command::PushClip { .. }
| Command::PushClipRounded { .. }
| Command::PushClipPath { .. }
| Command::PopClip
| Command::PushLayer { .. }
| Command::PopLayer
| Command::PushRetained { .. }
| Command::PopRetained => {}
}
let combined = root * retained_stack.correction();
let on_grid = command_on_grid(command, combined);
if !on_grid {
match command {
Command::PushClip { .. }
| Command::PushClipRounded { .. }
| Command::PushClipPath { .. }
| Command::PushLayer { .. } => push(&mut brackets, &mut masks, false),
Command::PushRetained {
rect,
scale,
transform,
..
} => {
let (scale, transform) = retained_entry(on_grid, *scale, *transform);
retained_stack.enter(*rect, scale, transform);
}
Command::FillRect { .. }
| Command::RoundedRect { .. }
| Command::Line { .. }
| Command::Image { .. }
| Command::BlurredRoundedRect { .. }
| Command::Path { .. }
| Command::PopClip
| Command::PopLayer
| Command::PopRetained
| Command::GlyphRun(_)
| Command::ClearRect { .. }
| Command::ShaderQuad { .. }
| Command::SceneTexture { .. }
| Command::DrawRetained { .. }
| Command::BackdropBlur { .. } => {}
}
continue;
}
match command {
Command::PushClip { rect, transform }
| Command::PushLayer {
rect, transform, ..
} => {
let mask = fast_rect(*rect, combined * *transform).is_none();
push(&mut brackets, &mut masks, mask);
}
Command::PushClipRounded {
rect,
radii,
transform,
} => {
let square = radii.top_left <= 0.0
&& radii.top_right <= 0.0
&& radii.bottom_right <= 0.0
&& radii.bottom_left <= 0.0;
let mask = !square || fast_rect(*rect, combined * *transform).is_none();
push(&mut brackets, &mut masks, mask);
}
Command::PushClipPath { .. } => push(&mut brackets, &mut masks, true),
Command::PopClip | Command::PopLayer => pop(&mut brackets, &mut masks),
Command::PushRetained {
rect,
alpha,
scale,
transform,
..
} => {
if retained_stack.enter(*rect, *scale, *transform) {
let corrected = root * retained_stack.correction() * *transform;
if (retain || *alpha < 1.0) && check_finite(corrected).is_ok() {
let mask = *alpha < 1.0 && fast_rect(*rect, corrected).is_none();
push(&mut brackets, &mut masks, mask);
retained_stack.record_layer(brackets.len());
}
}
}
Command::PopRetained => {
if retained_stack.leave(brackets.len()) {
pop(&mut brackets, &mut masks);
}
}
Command::FillRect { .. }
| Command::RoundedRect { .. }
| Command::Line { .. }
| Command::Image { .. }
| Command::BlurredRoundedRect { .. }
| Command::Path { .. } => {
if masks == 0
&& let Some(geometry) = Geometry::of(command, combined)
&& !geometry.is_fast_rect()
{
let cost = geometry.cost(viewport);
plan.cost = plan.cost.saturating_add(cost);
plan.jobs.push(Job {
command: index,
combined,
geometry,
cost,
});
}
}
Command::GlyphRun(_)
| Command::ClearRect { .. }
| Command::ShaderQuad { .. }
| Command::SceneTexture { .. }
| Command::DrawRetained { .. }
| Command::BackdropBlur { .. } => {}
}
}
plan
}
#[derive(Debug)]
struct Slot {
state: SlotState,
storage: StripStorage,
}
impl Default for Slot {
fn default() -> Self {
Self {
state: SlotState::Pending,
storage: StripStorage::new(GenerationMode::Append),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SlotState {
Pending,
Ready,
Taken,
}
#[derive(Debug, Default)]
pub(crate) struct Pool {
slots: Vec<Mutex<Slot>>,
helpers: Vec<StripGenerator>,
}
impl Pool {
pub(crate) fn run<R>(
&mut self,
plan: &Plan<'_>,
helpers: usize,
level: Level,
(width, height): (u16, u16),
walk: impl FnOnce(&Merge<'_, '_>) -> R,
) -> R {
let jobs = plan.jobs.as_slice();
if self.slots.len() < jobs.len() {
self.slots.resize_with(jobs.len(), Mutex::default);
}
for slot in &mut self.slots[..jobs.len()] {
slot.get_mut().unwrap_or_else(PoisonError::into_inner).state = SlotState::Pending;
}
while self.helpers.len() < helpers {
self.helpers.push(StripGenerator::new(width, height, level));
}
let chunks = chunk(jobs, helpers);
let slots = &self.slots[..jobs.len()];
let claimed = AtomicUsize::new(0);
let reached = AtomicUsize::new(0);
let done = AtomicBool::new(false);
#[cfg(test)]
let finished = AtomicUsize::new(0);
let helper = Helper {
jobs,
chunks: &chunks,
slots,
claimed: &claimed,
reached: &reached,
done: &done,
size: (width, height),
};
let result = std::thread::scope(|scope| {
for generator in self.helpers.iter_mut().take(helpers) {
#[cfg(test)]
let finished = &finished;
if let Err(err) = std::thread::Builder::new().spawn_scoped(scope, move || {
helper.work(generator);
#[cfg(test)]
finished.fetch_add(1, Ordering::Release);
}) {
static SAID: AtomicBool = AtomicBool::new(false);
if !SAID.swap(true, Ordering::Relaxed) {
log::warn!(
"frust-engine: could not spawn a strip helper thread ({err}); the walk \
generates its share inline"
);
}
}
}
let merge = Merge {
jobs,
slots,
next: std::cell::Cell::new(0),
reached: &reached,
#[cfg(test)]
finished: &finished,
};
let result = walk(&merge);
done.store(true, Ordering::Release);
result
});
self.trim();
result
}
fn trim(&mut self) {
self.slots.truncate(MAX_RETAINED_SLOTS);
for slot in &mut self.slots {
let storage = &mut slot
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.storage;
storage.clear();
storage.strips.shrink_to(RETAINED_SLOT_STRIPS);
storage.alphas.shrink_to(RETAINED_SLOT_ALPHAS);
}
}
#[cfg(test)]
fn retained(&mut self) -> (usize, usize, usize) {
let mut strips = 0;
let mut alphas = 0;
for slot in &mut self.slots {
let storage = &slot
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.storage;
strips = strips.max(storage.strips.capacity());
alphas = alphas.max(storage.alphas.capacity());
}
(self.slots.len(), strips, alphas)
}
}
fn chunk(jobs: &[Job<'_>], helpers: usize) -> Vec<Range<usize>> {
let total: u64 = jobs.iter().map(|job| job.cost).sum();
let pieces = (helpers as u64).max(1).saturating_mul(CHUNKS_PER_HELPER);
let target = (total / pieces).max(1);
let mut chunks = Vec::new();
let mut start = 0;
let mut acc = 0_u64;
for (index, job) in jobs.iter().enumerate() {
acc = acc.saturating_add(job.cost);
if acc >= target {
chunks.push(start..index + 1);
start = index + 1;
acc = 0;
}
}
if start < jobs.len() {
chunks.push(start..jobs.len());
}
chunks
}
#[derive(Clone, Copy)]
struct Helper<'p, 'a> {
jobs: &'p [Job<'a>],
chunks: &'p [Range<usize>],
slots: &'p [Mutex<Slot>],
claimed: &'p AtomicUsize,
reached: &'p AtomicUsize,
done: &'p AtomicBool,
size: (u16, u16),
}
impl Helper<'_, '_> {
fn work(self, generator: &mut StripGenerator) {
generator.reset(self.size.0, self.size.1);
loop {
let claim = self.claimed.fetch_add(1, Ordering::Relaxed);
let Some(chunk) = self
.chunks
.len()
.checked_sub(claim.saturating_add(1))
.and_then(|index| self.chunks.get(index))
else {
return;
};
for index in chunk.clone().rev() {
if !self.generate(index, generator) {
return;
}
}
}
}
fn generate(&self, index: usize, generator: &mut StripGenerator) -> bool {
let (Some(job), Some(slot)) = (self.jobs.get(index), self.slots.get(index)) else {
return false;
};
let mut slot = match slot.try_lock() {
Ok(slot) => slot,
Err(TryLockError::WouldBlock | TryLockError::Poisoned(_)) => return false,
};
if slot.state != SlotState::Pending
|| self.done.load(Ordering::Acquire)
|| index < self.reached.load(Ordering::Acquire)
{
return false;
}
slot.storage.clear();
job.geometry.generate(generator, &mut slot.storage, None);
slot.state = SlotState::Ready;
true
}
}
pub(crate) struct Merge<'p, 'a> {
jobs: &'p [Job<'a>],
slots: &'p [Mutex<Slot>],
next: std::cell::Cell<usize>,
reached: &'p AtomicUsize,
#[cfg(test)]
finished: &'p AtomicUsize,
}
impl<'p> Merge<'p, '_> {
#[cfg(test)]
fn helpers_finished(&self) -> usize {
self.finished.load(Ordering::Acquire)
}
pub(crate) fn claim(&self, command: usize, combined: Affine) -> Option<Claim<'p>> {
let mut next = self.next.get();
while self.jobs.get(next).is_some_and(|job| job.command < command) {
next += 1;
}
let job = self.jobs.get(next).filter(|job| job.command == command);
let passed = next + usize::from(job.is_some());
self.next.set(passed);
self.reached.store(passed, Ordering::Release);
let job = job?;
let same = job
.combined
.as_coeffs()
.iter()
.zip(combined.as_coeffs().iter())
.all(|(a, b)| a.to_bits() == b.to_bits());
same.then(|| Claim {
slot: &self.slots[next],
})
}
}
pub(crate) struct Claim<'p> {
slot: &'p Mutex<Slot>,
}
impl Claim<'_> {
pub(crate) fn append_to(self, storage: &mut StripStorage) -> bool {
let mut slot = self.slot.lock().unwrap_or_else(PoisonError::into_inner);
match slot.state {
SlotState::Ready => append_rebased(&slot.storage, storage),
SlotState::Pending | SlotState::Taken => {
slot.state = SlotState::Taken;
false
}
}
}
}
pub(crate) fn append_rebased(run: &StripStorage, storage: &mut StripStorage) -> bool {
let Ok(base) = u32::try_from(storage.alphas.len()) else {
return false;
};
let fits = run.strips.iter().all(|strip| {
strip
.alpha_idx()
.checked_add(base)
.is_some_and(|index| index <= MAX_ALPHA_INDEX)
});
if !fits {
return false;
}
storage.strips.extend(run.strips.iter().map(|strip| {
let mut strip: Strip = *strip;
strip.set_alpha_idx(strip.alpha_idx() + base);
strip
}));
storage.alphas.extend_from_slice(&run.alphas);
true
}
#[cfg(test)]
mod tests {
use super::*;
use kurbo::Circle;
fn generator() -> StripGenerator {
StripGenerator::new(256, 256, Level::try_detect().unwrap_or(Level::baseline()))
}
#[test]
fn a_rebased_run_equals_one_generated_in_place() {
let first = Circle::new((60.0, 60.0), 40.0).to_path(0.1);
let second = Circle::new((130.0, 90.0), 55.5).to_path(0.1);
let fill = |path| Geometry::Fill {
path,
transform: Affine::IDENTITY,
};
let mut direct = StripStorage::new(GenerationMode::Append);
let mut walk = generator();
fill(&first).generate(&mut walk, &mut direct, None);
fill(&second).generate(&mut walk, &mut direct, None);
let mut merged = StripStorage::new(GenerationMode::Append);
fill(&first).generate(&mut walk, &mut merged, None);
let mut run = StripStorage::new(GenerationMode::Append);
fill(&second).generate(&mut generator(), &mut run, None);
assert!(append_rebased(&run, &mut merged));
assert_eq!(direct.strips, merged.strips);
assert_eq!(direct.alphas, merged.alphas);
}
#[test]
fn a_helper_generates_every_run_the_walk_lets_it() {
let mut scene = Scene::new();
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
for i in 0..40 {
let center =
kurbo::Point::new(20.0 + f64::from(i) * 5.5, 30.0 + f64::from(i % 7) * 25.0);
let path = Circle::new(center, 9.0 + f64::from(i % 5)).to_path(0.1);
builder.fill_path(path.clone(), peniko::Brush::Solid(peniko::Color::BLACK));
builder.stroke_path(path, 1.5, peniko::Brush::Solid(peniko::Color::WHITE));
}
}
let viewport = Rect::new(0.0, 0.0, 256.0, 256.0);
let plan = plan(&scene, Affine::IDENTITY, viewport);
assert_eq!(plan.jobs.len(), 80);
let level = Level::try_detect().unwrap_or(Level::baseline());
let mut expected = StripStorage::new(GenerationMode::Append);
let mut walk = generator();
for job in &plan.jobs {
job.geometry.generate(&mut walk, &mut expected, None);
}
let mut pool = Pool::default();
let merged = pool.run(&plan, 1, level, (256, 256), |merge| {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while merge.helpers_finished() < 1 {
assert!(std::time::Instant::now() < deadline, "the helper finished");
std::thread::yield_now();
}
assert!(merge.slots.iter().all(|slot| {
slot.lock().unwrap_or_else(PoisonError::into_inner).state == SlotState::Ready
}));
let mut merged = StripStorage::new(GenerationMode::Append);
for job in &plan.jobs {
let claim = merge.claim(job.command, Affine::IDENTITY).expect("planned");
assert!(claim.append_to(&mut merged), "the helper generated it");
}
merged
});
assert_eq!(expected.strips, merged.strips);
assert_eq!(expected.alphas, merged.alphas);
let merged = pool.run(&plan, 0, level, (256, 256), |merge| {
let first = plan.jobs[0].command;
merge.claim(first, Affine::translate((0.5, 0.0))).is_none()
});
assert!(merged);
}
#[test]
fn a_run_past_the_alpha_index_field_is_refused() {
let mut run = StripStorage::new(GenerationMode::Append);
run.strips.push(Strip::new(0, 0, MAX_ALPHA_INDEX, false));
let mut storage = StripStorage::new(GenerationMode::Append);
storage.alphas.push(0);
assert!(!append_rebased(&run, &mut storage));
assert!(storage.strips.is_empty());
}
#[test]
fn the_pool_is_trimmed_back_to_its_bounds_after_a_run() {
let mut pool = Pool::default();
pool.slots
.resize_with(MAX_RETAINED_SLOTS + 500, Mutex::default);
{
let storage = &mut pool.slots[0]
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.storage;
storage
.strips
.resize(RETAINED_SLOT_STRIPS * 4, Strip::new(0, 0, 0, false));
storage.alphas.resize(RETAINED_SLOT_ALPHAS * 4, 0);
}
let (slots, strips, alphas) = pool.retained();
assert_eq!(slots, MAX_RETAINED_SLOTS + 500);
assert!(strips >= RETAINED_SLOT_STRIPS * 4 && alphas >= RETAINED_SLOT_ALPHAS * 4);
pool.trim();
let (slots, strips, alphas) = pool.retained();
assert_eq!(slots, MAX_RETAINED_SLOTS);
assert!(strips <= RETAINED_SLOT_STRIPS, "{strips} strips kept");
assert!(alphas <= RETAINED_SLOT_ALPHAS, "{alphas} alpha bytes kept");
let scene = Scene::new();
let plan = plan(&scene, Affine::IDENTITY, Rect::new(0.0, 0.0, 8.0, 8.0));
pool.run(&plan, 1, Level::baseline(), (8, 8), |_| ());
assert_eq!(pool.retained().0, MAX_RETAINED_SLOTS);
}
#[test]
fn chunks_cover_every_job_once_in_order() {
let path = BezPath::new();
let jobs: Vec<Job<'_>> = (0..37)
.map(|index| Job {
command: index,
combined: Affine::IDENTITY,
geometry: Geometry::Fill {
path: &path,
transform: Affine::IDENTITY,
},
cost: 1 + (index as u64 % 5) * 100,
})
.collect();
for helpers in 1..4 {
let chunks = chunk(&jobs, helpers);
let flat: Vec<usize> = chunks.iter().flat_map(Clone::clone).collect();
assert_eq!(flat, (0..jobs.len()).collect::<Vec<_>>());
assert!(chunks.iter().all(|range| !range.is_empty()));
}
assert!(chunk(&[], 2).is_empty());
}
}