Files
improvise/crates/zset/tests/cost.rs
T

190 lines
5.4 KiB
Rust

//! What the circuit copies and holds, counted in key clones and live keys, never in time.
//! Evidence: `context/test-evidence/zset.md`.
use std::cell::Cell;
use zset::{Circuit, ZSet};
thread_local! {
static CLONES: Cell<usize> = const { Cell::new(0) };
static LIVE: Cell<isize> = const { Cell::new(0) };
}
/// A key that counts its clones and how many of it are alive on this thread.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Counted(u32);
impl Counted {
fn new(n: u32) -> Self {
LIVE.with(|l| l.set(l.get() + 1));
Counted(n)
}
}
impl Clone for Counted {
fn clone(&self) -> Self {
CLONES.with(|c| c.set(c.get() + 1));
Counted::new(self.0)
}
}
impl Drop for Counted {
fn drop(&mut self) {
LIVE.with(|l| l.set(l.get() - 1));
}
}
fn clones() -> usize {
CLONES.with(Cell::get)
}
fn live() -> isize {
LIVE.with(Cell::get)
}
fn keys(n: u32) -> ZSet<Counted> {
(0..n).map(|i| (Counted::new(i), 1)).collect()
}
const N: u32 = 100;
#[test]
fn an_eval_borrows_its_input_instead_of_cloning_it() {
let mut c = Circuit::new();
let (input, s) = c.input::<Counted>();
let m = c.map(&s, |k| k.0);
let sink = c.output(&m);
c.push(&input, keys(N));
let before = clones();
c.step();
assert_eq!(
clones() - before,
0,
"[piece] a step over {N} keys cloned keys"
);
assert_eq!(c.take(&sink).len(), N as usize);
}
#[test]
fn an_integrate_stream_holds_its_integral_twice_when_the_tick_is_whole() {
let base = live();
let mut c = Circuit::new();
let (input, s) = c.input::<Counted>();
let i = c.integrate(&s);
c.push(&input, keys(N));
c.step();
// The input's piece (N), the integral (N) and the integrate stream's piece (N).
assert_eq!(
live() - base,
3 * N as isize,
"[whole] live keys after one step"
);
drop((c, input, s, i));
assert_eq!(live(), base, "[leak] every key is dropped with the circuit");
}
#[test]
fn a_sink_adds_its_input_with_one_clone_per_key() {
let mut c = Circuit::new();
let (input, s) = c.input::<Counted>();
let sink = c.output(&s);
c.push(&input, keys(N));
let before = clones();
c.step();
assert_eq!(
clones() - before,
N as usize,
"[sink] one clone per key it sums"
);
assert_eq!(c.take(&sink).len(), N as usize);
}
type E = (Counted, u8);
#[derive(Clone, Copy)]
enum Mode {
Plain,
Commit,
Rollback,
}
/// Every stateful operator over `Counted` keys, loaded with `n` keys; then one step of a
/// two-key delta in `mode`. Returns the key clones that step (and `begin`/`end`) made.
fn clones_of_one_step(n: u32, mode: Mode) -> usize {
let mut c = Circuit::new();
let (a, sa) = c.input::<E>();
let (b, sb) = c.input::<E>();
let d = c.distinct(&sa);
let dl = c.delay(&sa);
let j = c.join_indexed(&sa, &sb, |k, x, y| (k.0, *x, *y));
let g = c.aggregate(&sa, |_, vals| Some(vals.len() as i64));
let _ig = c.integrate(&sa);
let sinks = (c.output(&d), c.output(&dl), c.output(&j), c.output(&g));
let rows =
|lo: u32, hi: u32| -> ZSet<E> { (lo..hi).map(|i| ((Counted::new(i), 0), 1)).collect() };
c.push(&a, rows(0, n));
c.push(&b, rows(0, n));
c.step();
for z in [&sinks.0, &sinks.1] {
c.take(z);
}
c.take(&sinks.2);
c.take(&sinks.3);
// A small step after the load: a `delay` holds the previous tick's input, so a
// transaction opened right after a big load would copy that load once.
c.push(&a, rows(n, n + 2));
c.push(&b, rows(n, n + 2));
c.step();
let before = clones();
if !matches!(mode, Mode::Plain) {
c.begin();
}
c.push(&a, rows(n + 2, n + 4));
c.push(&b, rows(n + 2, n + 4));
c.step();
match mode {
Mode::Plain => {}
Mode::Commit => c.commit(),
Mode::Rollback => c.rollback(),
}
clones() - before
}
#[test]
fn a_transaction_costs_the_keys_a_step_touches_and_not_the_size_of_the_circuit() {
for mode in [Mode::Commit, Mode::Rollback] {
let extra = |n| clones_of_one_step(n, mode) - clones_of_one_step(n, Mode::Plain);
let (small, large) = (extra(10), extra(300));
assert!(small > 0, "[cost] the log copies the keys it records");
assert_eq!(small, large, "[cost] the overhead grew with the state");
}
}
/// Clones made by one 1-key `step_continue` over an integral of `n` keys.
fn clones_of_one_continue(n: u32, txn: bool) -> usize {
let mut c = Circuit::new();
let (input, s) = c.input::<Counted>();
let _i = c.integrate(&s);
c.push(&input, (0..n).map(|k| (Counted::new(k), 1)).collect());
c.step();
let before = clones();
if txn {
c.begin();
}
c.push(&input, (n..n + 1).map(|k| (Counted::new(k), 1)).collect());
c.step_continue();
if txn {
c.commit();
}
clones() - before
}
/// A limit, pinned so the docs cannot drift from it: a continued piece clones the edge's
/// whole value once per transaction, which is the size of the state.
#[test]
fn a_split_tick_in_a_transaction_costs_the_size_of_the_edge() {
let extra = |n| clones_of_one_continue(n, true) - clones_of_one_continue(n, false);
let (small, large) = (extra(10), extra(300));
assert!(large >= 300, "[split] the edge's value is cloned: {large}");
assert!(large > small, "[split] and it grows with the state");
}