Files
improvise/crates/improvise-incremental/tests/measure.rs
T

229 lines
7.6 KiB
Rust

//! What `Incremental` costs in operations, at two sizes of one deterministic table.
//! Evidence: `context/test-evidence/improvise-incremental.md`. Counts, never time.
use improvise_core::formula::ast::AggFunc;
use improvise_core::model::cell::{CellKey, CellValue};
use improvise_core::model::{Aggregation, Model};
use improvise_incremental::{Derived, Edit, Incremental, Maintained, Meter, Reading};
fn cell(r: usize, p: usize, m: usize) -> CellKey {
CellKey::new(vec![
("Region".into(), format!("r{r}")),
("Product".into(), format!("p{p}")),
("Month".into(), format!("m{m}")),
("_Measure".into(), "Revenue".into()),
])
}
/// `n` regions by `n` products by 6 months, one measure, values from a fixed sequence.
fn table(n: usize, func: AggFunc) -> Model {
let mut model = Model::new("table");
for (cat, prefix, count) in [("Region", "r", n), ("Product", "p", n), ("Month", "m", 6)] {
model.add_category(cat).unwrap();
for i in 0..count {
model
.category_mut(cat)
.unwrap()
.add_item(format!("{prefix}{i}"));
}
}
model.category_mut("_Measure").unwrap().add_item("Revenue");
model.measure_agg.insert("Revenue".into(), func);
let mut seed = 1u64;
for r in 0..n {
for p in 0..n {
for m in 0..6 {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let v = ((seed >> 33) % 1000) as f64;
model.set_cell(cell(r, p, m), CellValue::Number(v));
}
}
}
model
}
fn region_partial() -> CellKey {
CellKey::new(vec![
("Region".into(), "r1".into()),
("_Measure".into(), "Revenue".into()),
])
}
fn product_partial() -> CellKey {
CellKey::new(vec![
("Product".into(), "p2".into()),
("_Measure".into(), "Revenue".into()),
])
}
fn since(before: Meter, after: Meter) -> Meter {
Meter {
updates: after.updates - before.updates,
reads: after.reads - before.reads,
builds: after.builds - before.builds,
built_cells: after.built_cells - before.built_cells,
stems: after.stems - before.stems,
formula_cells: after.formula_cells - before.formula_cells,
rebuilds: after.rebuilds - before.rebuilds,
}
}
#[test]
fn a_group_read_comes_from_a_maintained_group() {
let model = table(6, AggFunc::Min);
let mut backend = Incremental::new(model.clone(), vec![]);
let want = backend.raw_fold(&region_partial());
assert!(matches!(want, Some(CellValue::Number(_))));
assert_eq!(backend.meter().builds, 1, "the first read builds its shape");
assert_eq!(backend.meter().reads, 1);
backend.apply(&Edit::SetCell(cell(1, 0, 0), CellValue::Number(-5.0)));
assert_eq!(
backend.raw_fold(&region_partial()),
Some(CellValue::Number(-5.0)),
"the new minimum comes from the delta"
);
assert_eq!(
backend.meter().builds,
1,
"and no second pass over the store"
);
}
/// The operations one overwrite costs once `shapes` category sets are maintained.
fn overwrite_cost(n: usize, shapes: usize, func: AggFunc) -> (Meter, usize) {
let mut backend = Incremental::new(table(n, func), vec![]);
let partials = [region_partial(), product_partial()];
for partial in partials.iter().take(shapes) {
backend.raw_fold(partial);
}
let before = backend.meter();
backend.apply(&Edit::SetCell(cell(1, 2, 3), CellValue::Number(2000.0)));
let cost = since(before, backend.meter());
assert!(backend.raw_fold(&region_partial()).is_some());
(cost, backend.shape_count())
}
#[test]
fn an_overwrite_costs_two_updates_per_shape_whatever_the_table_size() {
let funcs = [
AggFunc::Max,
AggFunc::Min,
AggFunc::Count,
AggFunc::Sum,
AggFunc::Avg,
];
for func in funcs {
for shapes in [1, 2] {
let (small, built) = overwrite_cost(6, shapes, func.clone());
let (large, _) = overwrite_cost(12, shapes, func.clone());
assert!(built >= shapes);
let updates = 2 * shapes as u64;
assert_eq!(
small.updates, updates,
"{func:?}: retract and insert, per shape"
);
assert_eq!(
small, large,
"{func:?}: 216 cells and 864 cells cost the same"
);
assert_eq!(large.builds, 0);
}
}
}
#[test]
fn a_sum_read_after_a_write_comes_from_the_delta_and_equals_a_fresh_sum() {
let mut backend = Incremental::new(table(6, AggFunc::Sum), vec![]);
backend.raw_fold(&region_partial());
backend.apply(&Edit::SetCell(cell(1, 0, 0), CellValue::Number(0.1)));
let built = backend.meter().builds;
let got = backend.raw_fold(&region_partial());
assert_eq!(
backend.meter().builds,
built,
"no second pass over the store"
);
let fresh = Incremental::new(backend.model().clone(), vec![]);
assert_eq!(got, fresh.raw_fold(&region_partial()));
}
#[test]
fn a_read_visits_one_group_where_eager_visits_every_cell_it_covers() {
for n in [6, 12] {
let model = table(n, AggFunc::Min);
let backend = Incremental::new(model.clone(), vec![]);
backend.raw_fold(&region_partial());
let before = backend.meter();
backend.raw_fold(&region_partial());
assert_eq!(since(before, backend.meter()).reads, 1, "n = {n}");
let eager_visits = model.data.matching_values(&region_partial().0).len();
assert_eq!(
eager_visits,
n * 6,
"eager folds a region's {} cells",
n * 6
);
}
}
#[test]
fn a_collapsed_group_reads_one_group_per_member_it_covers() {
let mut model = table(6, AggFunc::Count);
let region = model.category_mut("Region").unwrap();
region.add_group(improvise_core::model::category::Group::new("Coast"));
for item in ["r0", "r1", "r2"] {
region.items.get_mut(item).unwrap().group = Some("Coast".into());
}
let backend = Incremental::new(model, vec![]);
let coast = CellKey::new(vec![
("Region".into(), "Coast".into()),
("_Measure".into(), "Revenue".into()),
]);
backend.raw_fold(&coast);
let before = backend.meter();
assert_eq!(
backend.raw_fold(&coast),
Some(CellValue::Number(3.0 * 6.0 * 6.0))
);
assert_eq!(since(before, backend.meter()).reads, 3, "r0, r1 and r2");
}
#[test]
fn occupancy_with_a_hidden_item_reads_the_groups_it_subtracts() {
let backend = Incremental::new(table(6, AggFunc::Count), vec![]);
let reading = Reading::new(
Aggregation::OverHidden,
[("Product".to_string(), "p3".to_string())],
);
let line = CellKey::new(vec![("Region".into(), "r0".into())]);
assert_eq!(
backend.occupancy(&line, &reading),
5 * 6,
"p3's six cells are left out"
);
let before = backend.meter();
backend.occupancy(&line, &reading);
assert_eq!(
since(before, backend.meter()).reads,
2,
"the line, less p3 under it"
);
}
#[test]
fn a_group_whose_cells_are_all_cleared_is_no_longer_read() {
let mut backend = Incremental::new(table(2, AggFunc::Count), vec![]);
let one = cell(1, 1, 1);
assert_eq!(backend.raw_fold(&one), Some(CellValue::Number(1.0)));
backend.apply(&Edit::ClearCell(cell(1, 1, 1)));
let before = backend.meter();
assert_eq!(backend.raw_fold(&one), None, "nothing is left under it");
assert_eq!(
since(before, backend.meter()).reads,
0,
"and no empty group stands in for it"
);
}