#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StreamClass {
pub size: usize,
pub count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamLayout {
pub basic: Vec<StreamClass>,
pub recursive: StreamClass,
pub unused: usize,
}
const RECURSIVE_STREAM_FLOOR: usize = 1;
const UNIFORM_COLLAPSE_RATIO: f64 = 1.25;
const REGULARS_PER_LARGE: usize = 2;
impl StreamLayout {
pub fn n_basic_streams(&self) -> usize {
self.basic.iter().map(|c| c.count).sum()
}
pub fn basic_stream_sizes(&self) -> Vec<usize> {
self.basic.iter().flat_map(|c| std::iter::repeat_n(c.size, c.count)).collect()
}
}
#[derive(Clone, Copy)]
struct Carve {
n_large: usize,
n_regular: usize,
remaining: usize,
}
fn grow(budget: usize, large: usize, regular: usize, reserve: usize, max_basic_streams: usize) -> Carve {
let mut carve = Carve { n_large: 1, n_regular: 0, remaining: budget - large };
while carve.n_large + carve.n_regular < max_basic_streams {
let regular_first = carve.n_regular < REGULARS_PER_LARGE * carve.n_large;
let wanted = if regular_first { [regular, large] } else { [large, regular] };
let affordable = carve.remaining.saturating_sub(reserve);
match wanted.iter().find(|&&s| s > 0 && s <= affordable) {
Some(&size) => {
carve.remaining -= size;
if size == large {
carve.n_large += 1;
} else {
carve.n_regular += 1;
}
}
None => break,
}
}
carve
}
pub fn plan_stream_layout(
budget: usize,
basic_sizes_desc: &[usize],
class_floor: usize,
recursive_size: usize,
max_basic_streams: usize,
max_recursive_streams: usize,
) -> Option<StreamLayout> {
if max_basic_streams == 0 {
return None;
}
let mut sizes: Vec<usize> = basic_sizes_desc.iter().map(|&s| s.max(class_floor)).collect();
sizes.sort_unstable_by(|a, b| b.cmp(a));
sizes.dedup();
let &large = sizes.first()?;
if large == 0 || budget < large {
return None;
}
let reserve = recursive_size * RECURSIVE_STREAM_FLOOR.min(max_recursive_streams);
let uniform = grow(budget, large, 0, reserve, max_basic_streams);
let (regular, carve) = sizes
.iter()
.skip(1)
.map(|®ular| (regular, grow(budget, large, regular, reserve, max_basic_streams)))
.chain(std::iter::once((0, uniform)))
.max_by_key(|(size, c)| (c.n_large + c.n_regular, *size))
.expect("the chained fallback is always a candidate");
let split_streams = carve.n_large + carve.n_regular;
let (regular, carve) =
if regular > 0 && uniform.n_large >= split_streams && large as f64 <= regular as f64 * UNIFORM_COLLAPSE_RATIO {
(0, uniform)
} else {
(regular, carve)
};
let mut basic = vec![StreamClass { size: large, count: carve.n_large }];
if carve.n_regular > 0 {
basic.push(StreamClass { size: regular, count: carve.n_regular });
}
let mut remaining = carve.remaining;
let n_recursive = match recursive_size {
0 => 0,
size => max_recursive_streams.min(remaining / size),
};
remaining -= n_recursive * recursive_size;
Some(StreamLayout { basic, recursive: StreamClass { size: recursive_size, count: n_recursive }, unused: remaining })
}
#[cfg(test)]
mod tests {
use super::*;
const ZISK_BASIC_GB: &[f64] = &[14.57, 6.45, 6.26, 5.82, 5.57, 4.74, 4.52, 4.43, 3.82, 2.63, 1.47, 0.75];
const ZISK_COMPRESSOR_GB: f64 = 6.24;
const ZISK_RECURSIVE_GB: f64 = 1.33;
const ZISK_BUDGET_GB: f64 = 25.23;
fn gb(v: f64) -> usize {
(v * (1 << 30) as f64 / 8.0) as usize
}
fn zisk_layout(budget_gb: f64, max_basic: usize, max_recursive: usize) -> StreamLayout {
let sizes: Vec<usize> = ZISK_BASIC_GB.iter().copied().map(gb).collect();
plan_stream_layout(
gb(budget_gb),
&sizes,
gb(ZISK_COMPRESSOR_GB),
gb(ZISK_RECURSIVE_GB),
max_basic,
max_recursive,
)
.expect("zisk budget holds the largest air")
}
#[test]
fn the_carve_never_has_more_than_two_sizes() {
let uneven = [gb(14.5), gb(12.0), gb(9.1), gb(7.0), gb(3.2)];
for budget in [20.0, 26.15, 33.0, 40.0, 60.0, 80.0, 200.0] {
for sizes in [&ZISK_BASIC_GB.iter().copied().map(gb).collect::<Vec<_>>()[..], &uneven[..]] {
let layout =
plan_stream_layout(gb(budget), sizes, gb(ZISK_COMPRESSOR_GB), gb(ZISK_RECURSIVE_GB), 16, 10);
let Some(layout) = layout else { continue };
assert!(layout.basic.len() <= 2, "budget {budget}: {:?}", layout.basic);
}
}
}
#[test]
fn classes_grow_in_equilibrium() {
let expected = [(1, 0), (1, 1), (1, 2), (2, 2), (2, 3), (2, 4), (3, 4)];
for (max_basic, (n_large, n_regular)) in expected.into_iter().enumerate() {
let layout = zisk_layout(120.0, max_basic + 1, 10);
let got = (layout.basic[0].count, layout.basic.get(1).map_or(0, |c| c.count));
assert_eq!(got, (n_large, n_regular), "max_basic {}: {:?}", max_basic + 1, layout.basic);
}
}
#[test]
fn a_class_that_no_longer_fits_yields_to_the_other() {
let layout = zisk_layout(70.0, 16, 10);
assert_eq!(
layout.basic,
vec![StreamClass { size: gb(14.57), count: 2 }, StreamClass { size: gb(6.45), count: 6 }]
);
}
#[test]
fn a_smaller_regular_is_only_taken_when_it_buys_a_stream() {
for budget in [ZISK_BUDGET_GB, 33.0, 40.0, 60.0, 80.0] {
let layout = zisk_layout(budget, 16, 10);
assert_eq!(layout.basic[1].size, gb(6.45), "budget {budget}: {:?}", layout.basic);
}
}
#[test]
fn basic_classes_are_funded_before_recursive_streams() {
let layout = zisk_layout(ZISK_BUDGET_GB, 16, 10);
assert_eq!(
layout.basic,
vec![StreamClass { size: gb(14.57), count: 1 }, StreamClass { size: gb(6.45), count: 1 }]
);
assert!(layout.basic[1].size >= gb(ZISK_COMPRESSOR_GB), "every basic class must hold a compressor");
assert!(layout.recursive.count >= 3, "aggregation still gets the remainder: {:?}", layout.recursive);
}
#[test]
fn preloaded_sizes_put_the_second_class_at_the_compressor_floor() {
let sizes = [gb(14.57), gb(5.9), gb(3.8), gb(2.6), gb(0.75)];
let layout =
plan_stream_layout(gb(26.15), &sizes, gb(ZISK_COMPRESSOR_GB), gb(ZISK_RECURSIVE_GB), 16, 10).unwrap();
assert_eq!(
layout.basic,
vec![StreamClass { size: gb(14.57), count: 1 }, StreamClass { size: gb(ZISK_COMPRESSOR_GB), count: 1 }]
);
assert_eq!(layout.recursive.count, 4);
}
#[test]
fn the_recursive_floor_does_not_cost_the_second_class() {
let sizes = [gb(14.57), gb(5.9), gb(3.8), gb(2.6), gb(0.75)];
let layout =
plan_stream_layout(gb(26.15), &sizes, gb(ZISK_COMPRESSOR_GB), gb(ZISK_RECURSIVE_GB), 16, 10).unwrap();
assert_eq!(layout.n_basic_streams(), 2, "second class lost to the floor: {:?}", layout.basic);
assert_eq!(layout.recursive.count, 4, "floor is a minimum, not a cap: {:?}", layout.recursive);
}
#[test]
fn a_large_budget_still_gets_recursive_streams() {
let sizes = [gb(14.5), gb(12.0), gb(6.23), gb(3.8), gb(1.5)];
for budget in [33.0, 40.0, 80.0] {
let layout = plan_stream_layout(gb(budget), &sizes, gb(6.24), gb(ZISK_RECURSIVE_GB), 16, 10).unwrap();
assert!(
layout.recursive.count >= RECURSIVE_STREAM_FLOOR,
"budget {budget}: {:?} / {:?}",
layout.basic,
layout.recursive
);
}
}
#[test]
fn the_recursive_cap_still_bounds_the_remainder() {
let layout = zisk_layout(ZISK_BUDGET_GB, 16, 1);
assert_eq!(layout.recursive.count, 1);
assert!(layout.unused > 0);
}
#[test]
fn a_larger_budget_cuts_more_classes() {
let layout = zisk_layout(80.0, 16, 8);
assert!(layout.n_basic_streams() >= 4, "got {:?}", layout.basic);
let capped = zisk_layout(80.0, 3, 8);
assert_eq!(capped.n_basic_streams(), 3);
assert_eq!(capped.recursive.count, 8);
}
#[test]
fn classes_never_fall_below_the_compressor_floor() {
let layout = zisk_layout(ZISK_BUDGET_GB, 16, 0);
for class in &layout.basic {
assert!(class.size >= gb(ZISK_COMPRESSOR_GB), "class {class:?} cannot host a compressor");
}
}
#[test]
fn the_basic_stream_cap_is_respected() {
let layout = zisk_layout(80.0, 2, 0);
assert_eq!(layout.n_basic_streams(), 2);
}
#[test]
fn leftover_is_smaller_than_any_further_class() {
let layout = zisk_layout(ZISK_BUDGET_GB, 16, 3);
assert!(layout.unused < gb(ZISK_COMPRESSOR_GB));
}
#[test]
fn carve_shape_with_a_second_near_outlier() {
let sizes = [gb(14.5), gb(12.0), gb(6.23), gb(3.8), gb(1.5)];
for budget in [20.0, 26.15, 33.0, 40.0, 80.0] {
let layout = plan_stream_layout(gb(budget), &sizes, gb(6.24), gb(ZISK_RECURSIVE_GB), 16, 10).unwrap();
let classes: Vec<String> = layout
.basic
.iter()
.map(|c| format!("{} x {:.2}", c.count, c.size as f64 * 8.0 / (1 << 30) as f64))
.collect();
println!(
"budget {budget:>6.2} -> basic [{}] recursive {} unused {:.2}",
classes.join(" + "),
layout.recursive.count,
layout.unused as f64 * 8.0 / (1 << 30) as f64,
);
}
}
#[test]
fn an_air_too_big_for_the_regular_class_falls_back_to_the_large_one() {
let sizes = [gb(14.5), gb(12.0), gb(6.23)];
let layout = plan_stream_layout(gb(26.15), &sizes, gb(6.24), gb(ZISK_RECURSIVE_GB), 16, 10).unwrap();
assert_eq!(
layout.basic,
vec![StreamClass { size: gb(14.5), count: 1 }, StreamClass { size: gb(6.24), count: 1 }]
);
assert!(layout.basic[1].size >= gb(6.23));
assert!(layout.recursive.count >= 3);
}
#[test]
fn a_split_that_buys_no_stream_collapses_to_uniform() {
let sizes: Vec<usize> =
[7.42, 5.99, 5.82, 5.57, 5.31, 4.52, 4.43, 3.82, 2.60, 1.47, 0.75].iter().map(|&g| gb(g)).collect();
let layout = plan_stream_layout(gb(26.18), &sizes, gb(6.24), gb(1.33), 16, 10).unwrap();
assert_eq!(layout.basic, vec![StreamClass { size: gb(7.42), count: 3 }], "should be uniform");
assert!(sizes.iter().all(|&s| s <= layout.basic[0].size));
assert!(layout.recursive.count >= 1, "aggregation must keep at least the floor");
}
#[test]
fn a_real_outlier_still_gets_its_own_class() {
let layout = zisk_layout(70.0, 16, 10);
assert_eq!(layout.basic.len(), 2, "outlier must not be collapsed: {:?}", layout.basic);
assert_eq!(layout.basic[0].size, gb(14.57));
}
#[test]
fn too_small_a_budget_is_rejected() {
let sizes = [gb(14.57)];
assert!(plan_stream_layout(gb(10.0), &sizes, gb(6.24), gb(1.33), 16, 8).is_none());
}
#[test]
fn first_class_covers_both_the_largest_air_and_the_compressor() {
let layout = plan_stream_layout(gb(40.0), &[gb(2.0)], gb(9.0), gb(1.0), 16, 0).unwrap();
assert_eq!(layout.basic[0].size, gb(9.0));
let layout = plan_stream_layout(gb(40.0), &[gb(12.0)], gb(9.0), gb(1.0), 16, 0).unwrap();
assert_eq!(layout.basic[0].size, gb(12.0));
}
#[test]
fn stream_sizes_expand_classes_largest_first() {
let layout = StreamLayout {
basic: vec![StreamClass { size: 9, count: 1 }, StreamClass { size: 4, count: 2 }],
recursive: StreamClass { size: 1, count: 3 },
unused: 0,
};
assert_eq!(layout.basic_stream_sizes(), vec![9, 4, 4]);
assert_eq!(layout.n_basic_streams(), 3);
}
}