use super::super::geom::{Cell, Rect};
use super::super::identity::IdError;
use super::super::store::{AuthorityError, Budget, Store};
use super::support::build_from;
use super::support::*;
fn fam(refs: Vec<super::super::proj::RefProj>, l: u64) -> Formula {
Formula {
refs,
l,
literal: 1,
}
}
fn column_family(m: &mut Model, col: u32, rows: std::ops::Range<u32>) {
for r in rows {
m.cells.insert((0, r, col), fam(vec![rel(0, -1, 0)], 0));
}
}
fn state(
s: &Store,
) -> (
super::super::store::Counts,
super::super::store::Digest,
u64,
) {
(s.counts(), s.digest(), s.heap_bytes())
}
#[test]
fn budget_rejection_leaves_state_unchanged() {
let mut m = Model::default();
column_family(&mut m, 3, 0..200);
column_family(&mut m, 5, 10..300);
let mut s = build_from(&m);
let mut g = Rng(0xB0D);
let mut rejected = 0;
let mut admitted = 0;
for step in 0..300 {
let limit = s.heap_bytes() + u64::from(g.below(400));
s.budget = Budget {
retained: Some(limit),
scratch: None,
};
let before = state(&s);
let cell: Cell = (0, g.below(320), 2 + g.below(5));
let result = if g.chance(50) {
s.clear_cell(cell)
} else {
let f = fam(vec![rel(0, -1, 0)], 0);
s.set_formula(cell, &f.facts())
};
match result {
Ok(r) => {
admitted += 1;
assert!(
r.actual.bytes <= limit,
"admitted past the limit at step {step}"
);
}
Err(AuthorityError::Admission {
resource,
needed,
limit: l,
}) => {
rejected += 1;
assert_eq!(resource, "retained");
assert!(needed > l);
assert_eq!(
state(&s),
before,
"rejection changed the state at step {step}"
);
}
Err(e) => panic!("unexpected {e}"),
}
s.check().unwrap();
}
assert!(
rejected > 10 && admitted > 10,
"rejected {rejected} admitted {admitted}"
);
s.budget = Budget {
retained: None,
scratch: Some(0),
};
let before = state(&s);
let f = fam(vec![rel(0, -1, 0), abs(1, 1, 0)], 7);
match s.set_formula((0, 400, 9), &f.facts()) {
Err(AuthorityError::Admission { resource, .. }) => assert_eq!(resource, "scratch"),
other => panic!("expected a scratch rejection, got {other:?}"),
}
assert_eq!(state(&s), before);
let mut m2 = Model::default();
column_family(&mut m2, 3, 0..5);
let mut limited = Store::with_id_limit(5);
for (c, f) in m2.cells.iter() {
limited.set_formula(*c, &f.facts()).unwrap();
}
let before = state(&limited);
match limited.set_formula((0, 9, 3), &fam(vec![rel(0, -1, 0)], 0).facts()) {
Err(AuthorityError::Identity(IdError::Exhausted { .. })) => {}
other => panic!("expected exhaustion, got {other:?}"),
}
assert_eq!(state(&limited), before);
limited
.set_formula((0, 2, 3), &fam(vec![rel(0, -2, 0)], 1).facts())
.unwrap();
}
#[test]
fn punch_zero_refs_admission_charged() {
let mut m = Model::default();
column_family(&mut m, 3, 0..100);
let mut s = build_from(&m);
let before = s.counts();
assert_eq!((before.records, before.owners, before.nodes), (1, 1, 1));
let r = s.clear_cell((0, 50, 3)).unwrap();
assert_eq!(r.predicted, r.actual);
assert_eq!(
(r.actual.records, r.actual.owners, r.actual.nodes),
(2, 2, 2)
);
assert_eq!(r.actual.runs, 2);
assert!(r.actual.bytes > before.bytes, "the punch must be charged");
}
#[test]
fn repeated_punch_refill_scales_linearly() {
let mut per_op = Vec::new();
for width in [50u32, 200, 800] {
let mut m = Model::default();
for c in 0..width {
m.cells.insert((0, 5, c), fam(vec![rel(-1, 0, 0)], 0));
}
let mut s = build_from(&m);
let mut g = Rng(u64::from(width) * 31 + 7);
let n = 600u64;
let base = s.stats.clone();
for _ in 0..n {
let c = g.below(width);
s.clear_cell((0, 5, c)).unwrap();
s.set_formula((0, 5, c), &fam(vec![rel(-1, 0, 0)], 0).facts())
.unwrap();
}
s.check().unwrap();
let st = &s.stats;
assert!(st.max_run_relabels_one_op <= 5, "width {width}: {st:?}");
let work = (st.run_relabels - base.run_relabels)
+ (st.repartition_work - base.repartition_work)
+ (st.records_created - base.records_created)
+ (st.owners_created - base.owners_created);
per_op.push(work as f64 / (2 * n) as f64);
assert!(st.max_work_per_creation <= 8.0 + 4.0, "{st:?}");
}
assert!(per_op[2] <= per_op[0] * 2.0 + 2.0, "per-op work {per_op:?}");
}
#[test]
fn whole_group_fragmentation_then_trigger() {
let mut m = Model::default();
for col in 0..12u32 {
column_family(&mut m, 2 * col + 1, 0..20);
}
let mut s = build_from(&m);
let g0 = s.counts();
assert_eq!(g0.records, 12);
let r = s.clear_rect(0, Rect::new(10, 0, 10, 30)).unwrap();
assert_eq!(r.predicted, r.actual);
assert_eq!(r.actual.records, 24, "one band clear doubles the group");
assert_eq!(r.repartitions, 0, "L = 24 = 2B, within 2B + 2");
let checks = s.stats.repartition_checks;
for cell in [(0, 3, 1), (0, 4, 3)] {
s.clear_cell(cell).unwrap();
m.cells.remove(&cell);
s.check().unwrap();
if s.stats.repartition_checks > checks {
break;
}
}
assert!(
s.stats.repartition_checks > checks,
"the trigger did not fire: {:?}",
s.stats
);
for (c, _) in m.cells.clone() {
if c.1 == 10 {
m.cells.remove(&c);
}
}
assert_eq!(s.digest(), build_from(&m).digest());
}
#[test]
fn node_repartition_never_grows_bytes() {
let mut m = Model::default();
let f = fam(vec![rel(0, -1, 0)], 0);
m.cells.insert((0, 0, 1), f.clone());
m.cells.insert((0, 2, 1), f.clone());
let mut s = build_from(&m);
s.set_formula((0, 1, 1), &f.facts()).unwrap();
for r in 3..12u32 {
s.set_formula((0, r, 1), &f.facts()).unwrap();
}
s.check().unwrap();
assert!(s.stats.repartitions_committed > 0);
assert_eq!(s.stats.repartition_live_up, 0, "{:?}", s.stats);
let o = s.owner_at((0, 5, 1)).unwrap();
assert!(s.owner_dom(o).2, "the column re-formed a family node");
}
#[test]
fn no_merge_reentry_behind_the_dry_run() {
let mut m = Model::default();
for col in 0..80u32 {
for r in 0..9u32 {
m.cells.insert(
(0, r, 2 * col + 1),
fam(vec![rel(0, -1, 0), abs(0, 0, 0)], 0),
);
}
}
let mut s = build_from(&m);
let r = s
.set_formula(
(0, 9, 7),
&fam(vec![rel(0, -1, 0), abs(0, 0, 0)], 0).facts(),
)
.unwrap();
assert_eq!(r.predicted, r.actual);
assert!(r.observed_index_transient <= r.predicted_transient);
s.check().unwrap();
}
#[test]
fn skipped_repartition_is_counted_and_recovers() {
let mut m = Model::default();
column_family(&mut m, 3, 0..3000);
let mut s = build_from(&m);
s.budget.scratch = Some(40_000);
let mut refused = 0;
for r in (1..3000u32).step_by(3) {
let before = state(&s);
match s.clear_cell((0, r, 3)) {
Ok(_) => {}
Err(AuthorityError::Admission {
resource: "scratch",
..
}) => {
refused += 1;
assert_eq!(state(&s), before);
}
Err(e) => panic!("{e}"),
}
}
s.check().unwrap();
assert!(
s.stats.repartition_skipped > 0,
"{:?} refused {refused}",
s.stats
);
s.budget.scratch = None;
let skipped = s.stats.repartition_skipped;
let checks = s.stats.repartition_checks;
for r in (2..3000u32).step_by(3) {
s.clear_cell((0, r, 3)).unwrap();
}
s.check().unwrap();
assert_eq!(s.stats.repartition_skipped, skipped);
assert!(s.stats.repartition_checks > checks);
}
#[test]
fn dry_run_exact_on_large_traces() {
for seed in 0..3u64 {
let mut g = Rng(0xD0 + seed);
let mut m = Model::default();
for col in 1..40u32 {
let f = random_formula(&mut g, 400);
for r in 2..(2 + g.below(300)) {
if Model::valid(&f, (0, r, col)) {
m.cells.insert((0, r, col), f.clone());
}
}
}
let mut s = build_from(&m);
for step in 0..1500 {
let cell: Cell = (0, g.below(400), 1 + g.below(40));
let r = if g.chance(45) {
m.cells.remove(&cell);
s.clear_cell(cell).unwrap()
} else {
let src = (cell.0, cell.1.saturating_sub(1), cell.2);
let f = m
.cells
.get(&src)
.cloned()
.unwrap_or_else(|| random_formula(&mut g, 400));
if !Model::valid(&f, cell) {
continue;
}
m.cells.insert(cell, f.clone());
s.set_formula(cell, &f.facts()).unwrap()
};
assert_eq!(r.predicted, r.actual, "seed {seed} step {step}");
assert!(r.observed_index_transient <= r.predicted_transient);
if step % 100 == 0 {
s.check().unwrap();
}
}
s.check().unwrap();
assert_eq!(s.stats.repartition_live_up, 0);
assert_eq!(s.digest(), build_from(&m).digest(), "seed {seed}");
}
}
#[test]
#[ignore]
fn singleton_memory_breakdown() {
let mut g = Rng(42);
let mut m = Model::default();
let n = 16_384u32;
for r in 0..n {
let a = g.below(n) as i32 - r as i32;
let b = g.below(n) as i32 - r as i32;
m.cells.insert(
(0, r, 1),
Formula {
refs: vec![rel(a, -1, 0), rel(b, -1, 0)],
l: u64::from(r % 7),
literal: r % 7,
},
);
}
let s = build_from(&m);
for (k, v) in s.bytes_breakdown() {
eprintln!("{k:>16} {v:>10} {:>8.1} B/formula", v as f64 / f64::from(n));
}
eprintln!(
"total {} = {:.1} B/formula",
s.heap_bytes(),
s.heap_bytes() as f64 / f64::from(n)
);
}