ce6e36112d
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
190 lines
5.4 KiB
Rust
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");
|
|
}
|