fix(zset): a chunked rebuild is one tick, empty groups have no row, a last chunk is Done (improvise-bw4p.1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
ba17f92bc9
commit
0849981ae1
+80
-17
@@ -47,7 +47,8 @@ impl<K: Ord> Output<K> {
|
||||
}
|
||||
|
||||
trait Node {
|
||||
fn eval(&mut self);
|
||||
/// `cont` is true for a later piece of a tick that has already begun.
|
||||
fn eval(&mut self, cont: bool);
|
||||
}
|
||||
|
||||
struct Unary<I, O, F> {
|
||||
@@ -57,7 +58,7 @@ struct Unary<I, O, F> {
|
||||
}
|
||||
|
||||
impl<I, O, F: FnMut(&I) -> O> Node for Unary<I, O, F> {
|
||||
fn eval(&mut self) {
|
||||
fn eval(&mut self, _cont: bool) {
|
||||
let v = (self.f)(&self.src.0.borrow());
|
||||
*self.out.0.borrow_mut() = v;
|
||||
}
|
||||
@@ -71,12 +72,47 @@ struct Binary<A, B, O, F> {
|
||||
}
|
||||
|
||||
impl<A, B, O, F: FnMut(&A, &B) -> O> Node for Binary<A, B, O, F> {
|
||||
fn eval(&mut self) {
|
||||
fn eval(&mut self, _cont: bool) {
|
||||
let v = (self.f)(&self.a.0.borrow(), &self.b.0.borrow());
|
||||
*self.out.0.borrow_mut() = v;
|
||||
}
|
||||
}
|
||||
|
||||
/// out(t) = in(t - 1). A continued piece adds to what is held and emits nothing.
|
||||
struct Delay<K> {
|
||||
src: Stream<ZSet<K>>,
|
||||
out: Stream<ZSet<K>>,
|
||||
held: ZSet<K>,
|
||||
}
|
||||
|
||||
impl<K: Ord + Clone> Node for Delay<K> {
|
||||
fn eval(&mut self, cont: bool) {
|
||||
let d = self.src.0.borrow();
|
||||
let v = if cont {
|
||||
self.held += &*d;
|
||||
ZSet::new()
|
||||
} else {
|
||||
std::mem::replace(&mut self.held, d.clone())
|
||||
};
|
||||
*self.out.0.borrow_mut() = v;
|
||||
}
|
||||
}
|
||||
|
||||
/// out(t) = in(0) + .. + in(t). A continued piece emits only its own input.
|
||||
struct Integrate<K> {
|
||||
src: Stream<ZSet<K>>,
|
||||
out: Stream<ZSet<K>>,
|
||||
acc: ZSet<K>,
|
||||
}
|
||||
|
||||
impl<K: Ord + Clone> Node for Integrate<K> {
|
||||
fn eval(&mut self, cont: bool) {
|
||||
let d = self.src.0.borrow();
|
||||
self.acc += &*d;
|
||||
*self.out.0.borrow_mut() = if cont { d.clone() } else { self.acc.clone() };
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves what was pushed into the circuit's stream.
|
||||
struct Source<K> {
|
||||
pending: Rc<RefCell<ZSet<K>>>,
|
||||
@@ -84,7 +120,7 @@ struct Source<K> {
|
||||
}
|
||||
|
||||
impl<K: Ord> Node for Source<K> {
|
||||
fn eval(&mut self) {
|
||||
fn eval(&mut self, _cont: bool) {
|
||||
*self.out.0.borrow_mut() = std::mem::take(&mut *self.pending.borrow_mut());
|
||||
}
|
||||
}
|
||||
@@ -105,8 +141,18 @@ impl Circuit {
|
||||
|
||||
/// Runs one clock tick: every node, once, in order.
|
||||
pub fn step(&mut self) {
|
||||
self.run(false);
|
||||
}
|
||||
|
||||
/// Runs another piece of the tick the last `step` began; the clock does not advance.
|
||||
/// Pieces sum to one `step` of their combined input (`context/code/zset.md`).
|
||||
pub fn step_continue(&mut self) {
|
||||
self.run(true);
|
||||
}
|
||||
|
||||
fn run(&mut self, cont: bool) {
|
||||
for n in &mut self.nodes {
|
||||
n.eval();
|
||||
n.eval(cont);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,17 +207,24 @@ impl Circuit {
|
||||
|
||||
/// out(t) = in(t - 1); out(0) is zero.
|
||||
pub fn delay<K: Ord + Clone + 'static>(&mut self, s: &Stream<Z<K>>) -> Stream<Z<K>> {
|
||||
let mut held = ZSet::new();
|
||||
self.unary(s, move |d: &Z<K>| std::mem::replace(&mut held, d.clone()))
|
||||
let out = Stream(Rc::default());
|
||||
self.nodes.push(Box::new(Delay {
|
||||
src: s.clone(),
|
||||
out: out.clone(),
|
||||
held: ZSet::new(),
|
||||
}));
|
||||
out
|
||||
}
|
||||
|
||||
/// out(t) = in(0) + .. + in(t).
|
||||
pub fn integrate<K: Ord + Clone + 'static>(&mut self, s: &Stream<Z<K>>) -> Stream<Z<K>> {
|
||||
let mut acc = ZSet::new();
|
||||
self.unary(s, move |d: &Z<K>| {
|
||||
acc += d;
|
||||
acc.clone()
|
||||
})
|
||||
let out = Stream(Rc::default());
|
||||
self.nodes.push(Box::new(Integrate {
|
||||
src: s.clone(),
|
||||
out: out.clone(),
|
||||
acc: ZSet::new(),
|
||||
}));
|
||||
out
|
||||
}
|
||||
|
||||
/// out(t) = in(t) - in(t - 1).
|
||||
@@ -306,12 +359,13 @@ pub enum Progress {
|
||||
Done,
|
||||
}
|
||||
|
||||
/// Building from a snapshot as a sliced job: the snapshot enters an input a chunk at a
|
||||
/// time, one circuit step per chunk. Outputs sum to what one big delta would give.
|
||||
/// A snapshot load as a sliced job: one logical tick, a `step` then `step_continue`s.
|
||||
/// Outputs sum to one big delta's, temporal operators included (`context/code/zset.md`).
|
||||
pub struct Rebuild<K> {
|
||||
input: Input<K>,
|
||||
rest: std::collections::btree_map::IntoIter<K, i64>,
|
||||
chunk: usize,
|
||||
started: bool,
|
||||
}
|
||||
|
||||
impl<K: Ord> Rebuild<K> {
|
||||
@@ -321,19 +375,28 @@ impl<K: Ord> Rebuild<K> {
|
||||
input,
|
||||
rest: snapshot.into_iter(),
|
||||
chunk: chunk.max(1),
|
||||
started: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs chunks until done or `expired()`, at least one per call. A predicate, not an
|
||||
/// `Instant`, which panics on wasm32 (`context/code/zset.md`).
|
||||
/// `Instant` (wasm32). An empty snapshot takes its one tick; a finished job none.
|
||||
pub fn step(&mut self, circuit: &mut Circuit, mut expired: impl FnMut() -> bool) -> Progress {
|
||||
loop {
|
||||
let chunk: ZSet<K> = self.rest.by_ref().take(self.chunk).collect();
|
||||
if chunk.is_zero() {
|
||||
if chunk.is_zero() && self.started {
|
||||
return Progress::Done;
|
||||
}
|
||||
self.input.push(chunk);
|
||||
circuit.step();
|
||||
if self.started {
|
||||
circuit.step_continue();
|
||||
} else {
|
||||
circuit.step();
|
||||
self.started = true;
|
||||
}
|
||||
if self.rest.len() == 0 {
|
||||
return Progress::Done;
|
||||
}
|
||||
if expired() {
|
||||
return Progress::Pending;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ impl<TA, TB> JoinState<TA, TB> {
|
||||
}
|
||||
|
||||
/// Incremental group-by: `f` folds one key's integrated values to at most one result.
|
||||
/// Only the touched keys are re-folded: out = -(k, f(old)) + (k, f(new)).
|
||||
/// An empty group has no row, and `f` is not asked about it. Only touched keys are re-folded: out = -(k, f(old)) + (k, f(new)).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AggregateState<T> {
|
||||
groups: T,
|
||||
@@ -104,13 +104,18 @@ impl<T> AggregateState<T> {
|
||||
}
|
||||
let mut out = ZSet::new();
|
||||
for (k, changes) in touched {
|
||||
if let Some(r) = f(k, &self.groups.lookup(k)) {
|
||||
let before = self.groups.lookup(k);
|
||||
if let Some(r) = Some(&before)
|
||||
.filter(|g| !g.is_empty())
|
||||
.and_then(|g| f(k, g))
|
||||
{
|
||||
out.insert((k.clone(), r), -1);
|
||||
}
|
||||
for (v, w) in changes {
|
||||
self.groups.apply(k, v, w);
|
||||
}
|
||||
if let Some(r) = f(k, &self.groups.lookup(k)) {
|
||||
let after = self.groups.lookup(k);
|
||||
if let Some(r) = Some(&after).filter(|g| !g.is_empty()).and_then(|g| f(k, g)) {
|
||||
out.insert((k.clone(), r), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc da6d39e3f2d54dc796842f642b0fc17d09a1491a9937c3d3f95d453d2d9487ed # shrinks to prior = [(3, -1)], snapshot = [(3, 2), (5, -2), (0, 1), (1, 1)], chunk = 0, budget = 1
|
||||
@@ -377,4 +377,131 @@ fn aggregate_state_refolds_only_the_touched_keys() {
|
||||
let out = st.step(&Z::singleton((7, 1), 1), f);
|
||||
assert_eq!(folds.get(), 2, "one key, folded before and after");
|
||||
assert_eq!(out, Z::from_iter([((7, 1), -1), ((7, 2), 1)]));
|
||||
folds.set(0);
|
||||
let out = st.step(&Z::singleton((7, 1), -1), f);
|
||||
assert_eq!(folds.get(), 2, "the second step also folds only its key");
|
||||
assert_eq!(out, Z::from_iter([((7, 2), -1), ((7, 1), 1)]));
|
||||
}
|
||||
|
||||
fn temporal(c: &mut Circuit, s: &Stream<Z<u8>>) -> Stream<Z<u8>> {
|
||||
let d = c.delay(s);
|
||||
let i = c.integrate(s);
|
||||
let df = c.differentiate(s);
|
||||
let a = c.plus(&d, &i);
|
||||
let b = c.plus(&a, &df);
|
||||
c.distinct(&b)
|
||||
}
|
||||
|
||||
/// The sums of three sinks over a snapshot loaded after one prior tick.
|
||||
fn loaded(snapshot: &Z<u8>, chunk: usize, budget: usize) -> (Z<u8>, Z<u8>, Z<u8>) {
|
||||
let mut c = Circuit::new();
|
||||
let (input, s) = c.input::<u8>();
|
||||
let (d, i, df) = (c.delay(&s), c.integrate(&s), c.differentiate(&s));
|
||||
let sinks = (c.output(&d), c.output(&i), c.output(&df));
|
||||
input.push(Z::singleton(9, 1));
|
||||
c.step();
|
||||
let _ = (sinks.0.take(), sinks.1.take(), sinks.2.take());
|
||||
let mut job = Rebuild::new(input, snapshot.clone(), chunk);
|
||||
for _ in 0..50 {
|
||||
let left = Cell::new(budget);
|
||||
let p = job.step(&mut c, || {
|
||||
left.set(left.get() - 1);
|
||||
left.get() == 0
|
||||
});
|
||||
if p == Progress::Done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(sinks.0.take(), sinks.1.take(), sinks.2.take())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chunked_rebuild_is_one_logical_step_for_delay_integrate_and_differentiate() {
|
||||
let snap: Z<u8> = [(1, 1), (2, 1)].into_iter().collect();
|
||||
let want = loaded(&snap, 5, 5);
|
||||
assert_eq!(
|
||||
want.0,
|
||||
Z::singleton(9, 1),
|
||||
"delay shows the previous tick once"
|
||||
);
|
||||
assert_eq!(loaded(&snap, 1, 1), want, "[temporal-chunk]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_snapshot_is_still_one_tick() {
|
||||
let mut c = Circuit::new();
|
||||
let (input, s) = c.input::<u8>();
|
||||
let d = c.delay(&s);
|
||||
input.push(Z::singleton(1, 1));
|
||||
c.step();
|
||||
let mut job = Rebuild::new(input, Z::new(), 3);
|
||||
assert_eq!(job.step(&mut c, || false), Progress::Done);
|
||||
assert_eq!(d.current().len(), 1, "the empty load ticked the clock once");
|
||||
assert_eq!(job.step(&mut c, || false), Progress::Done);
|
||||
assert_eq!(d.current().len(), 1, "a finished job does not tick again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_last_chunk_is_done_even_when_the_deadline_has_expired() {
|
||||
let mut c = Circuit::new();
|
||||
let (input, _s) = c.input::<u8>();
|
||||
let mut job = Rebuild::new(input, Z::singleton(1, 1), 4);
|
||||
assert_eq!(job.step(&mut c, || true), Progress::Done);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn a_chunked_temporal_rebuild_equals_one_step(
|
||||
prior in pairs(), snapshot in pairs(), chunk in 0usize..4, budget in 1usize..4,
|
||||
) {
|
||||
let (prior, snapshot): (Z<u8>, Z<u8>) = (prior.into_iter().collect(), snapshot.into_iter().collect());
|
||||
let run = |chunk: usize, budget: usize| {
|
||||
let mut c = Circuit::new();
|
||||
let (input, s) = c.input::<u8>();
|
||||
let out = temporal(&mut c, &s);
|
||||
let sink = c.output(&out);
|
||||
input.push(prior.clone());
|
||||
c.step();
|
||||
let _ = sink.take();
|
||||
let mut job = Rebuild::new(input, snapshot.clone(), chunk);
|
||||
for _ in 0..50 {
|
||||
let left = Cell::new(budget);
|
||||
if job.step(&mut c, || { left.set(left.get() - 1); left.get() == 0 }) == Progress::Done { break; }
|
||||
}
|
||||
sink.take()
|
||||
};
|
||||
prop_assert_eq!(run(chunk, budget), run(100, 100), "[temporal-rebuild]");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_that_empties_leaves_no_phantom_row_for_a_count() {
|
||||
let mut st = AggregateState::new(MemTrace::<u8, u8>::new());
|
||||
let count = |_: &u8, vals: &[(u8, i64)]| Some(vals.len() as i64);
|
||||
assert_eq!(
|
||||
st.step(&Z::singleton((7, 1), 1), count),
|
||||
Z::singleton((7, 1), 1)
|
||||
);
|
||||
assert_eq!(
|
||||
st.step(&Z::singleton((7, 1), -1), count),
|
||||
Z::singleton((7, 1), -1)
|
||||
);
|
||||
assert_eq!(
|
||||
st.step(&Z::singleton((7, 2), 1), count),
|
||||
Z::singleton((7, 1), 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_aggregate_that_returns_some_on_empty_input_matches_recompute() {
|
||||
let steps = vec![
|
||||
Z::singleton((7u8, 1u8), 1),
|
||||
Z::singleton((7, 1), -1),
|
||||
Z::singleton((7, 2), 1),
|
||||
];
|
||||
matches_recompute(
|
||||
&steps,
|
||||
|c, s| c.aggregate(s, |_, vals| Some(vals.len() as i64)),
|
||||
|z| group(z, |vals| Some(vals.len() as i64)),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user