9d675c9c33
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
208 lines
7.0 KiB
Rust
208 lines
7.0 KiB
Rust
//! The maintained groups, one map per set of named categories (ADR 009 clause 7).
|
|
//! Reasoning: `context/code/improvise-incremental.md`, steps 4 and 6.
|
|
|
|
use crate::group::Group;
|
|
use crate::{CellDelta, DeltaValue};
|
|
use improvise_core::model::cell::{CellChange, CellKey, CellValue};
|
|
use improvise_core::model::source::CellSource;
|
|
use std::cell::{Cell, RefCell};
|
|
use std::collections::BTreeMap;
|
|
|
|
/// How much work was done: operations, never time.
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
pub struct Meter {
|
|
/// Group updates applied from a delta.
|
|
pub updates: u64,
|
|
/// Exact-sum additions and retractions applied from the change log.
|
|
pub sum_updates: u64,
|
|
/// Groups looked up to answer a read.
|
|
pub reads: u64,
|
|
/// Shapes built from the store.
|
|
pub builds: u64,
|
|
/// Cells visited by those builds.
|
|
pub built_cells: u64,
|
|
/// Stems whose formulas were evaluated.
|
|
pub stems: u64,
|
|
/// Formula cells evaluated, passes included.
|
|
pub formula_cells: u64,
|
|
/// Times the stems were rebuilt from the store, as a formula edit does.
|
|
pub rebuilds: u64,
|
|
/// Whole-cache recomputes by the fallback for documents whose stems read each other. One is
|
|
/// every stem's formulas for every pass, so it is counted as an event, not as cells.
|
|
pub recomputes: u64,
|
|
}
|
|
|
|
impl Meter {
|
|
pub fn plus(self, o: Meter) -> Meter {
|
|
Meter {
|
|
updates: self.updates + o.updates,
|
|
sum_updates: self.sum_updates + o.sum_updates,
|
|
reads: self.reads + o.reads,
|
|
builds: self.builds + o.builds,
|
|
built_cells: self.built_cells + o.built_cells,
|
|
stems: self.stems + o.stems,
|
|
formula_cells: self.formula_cells + o.formula_cells,
|
|
rebuilds: self.rebuilds + o.rebuilds,
|
|
recomputes: self.recomputes + o.recomputes,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The groups of one category set: a stored cell lands in the group of its coordinates
|
|
/// on those categories, if it has them all.
|
|
struct Shape {
|
|
groups: BTreeMap<CellKey, Group>,
|
|
}
|
|
|
|
fn project(key: &CellKey, cats: &[String]) -> Option<CellKey> {
|
|
let coords: Option<Vec<(String, String)>> = cats
|
|
.iter()
|
|
.map(|c| key.get(c).map(|i| (c.clone(), i.to_string())))
|
|
.collect();
|
|
coords.map(CellKey::new)
|
|
}
|
|
|
|
/// A change that left the same bits behind moves no sum or sign.
|
|
fn same_bits(change: &CellChange) -> bool {
|
|
match (&change.old, &change.new) {
|
|
(Some(CellValue::Number(a)), Some(CellValue::Number(b))) => a.to_bits() == b.to_bits(),
|
|
(a, b) => a == b,
|
|
}
|
|
}
|
|
|
|
impl Shape {
|
|
/// The delta moves counts and multisets; the raw changes move sums and the sign of a zero.
|
|
/// Returns the group updates and the sum updates.
|
|
fn apply(&mut self, cats: &[String], delta: &CellDelta, changes: &[CellChange]) -> (u64, u64) {
|
|
let (mut updates, mut sums) = (0, 0);
|
|
let mut touched = Vec::new();
|
|
for ((key, value), weight) in delta.iter() {
|
|
let Some(at) = project(key, cats) else {
|
|
continue;
|
|
};
|
|
self.groups
|
|
.entry(at.clone())
|
|
.or_default()
|
|
.apply(value, weight);
|
|
touched.push(at);
|
|
updates += 1;
|
|
}
|
|
for change in changes.iter().filter(|c| !same_bits(c)) {
|
|
let Some(at) = project(&change.key, cats) else {
|
|
continue;
|
|
};
|
|
let Some(group) = self.groups.get_mut(&at) else {
|
|
continue;
|
|
};
|
|
if let Some(CellValue::Number(old)) = &change.old {
|
|
group.add_number(*old, -1);
|
|
sums += 1;
|
|
}
|
|
if let Some(CellValue::Number(new)) = &change.new {
|
|
group.add_number(*new, 1);
|
|
sums += 1;
|
|
}
|
|
touched.push(at);
|
|
}
|
|
for at in touched {
|
|
if self.groups.get(&at).is_some_and(Group::is_empty) {
|
|
self.groups.remove(&at);
|
|
}
|
|
}
|
|
(updates, sums)
|
|
}
|
|
|
|
/// The shape of `source`'s cells, one pass.
|
|
fn of(source: &dyn CellSource, cats: &[String]) -> (Shape, u64) {
|
|
let mut shape = Shape {
|
|
groups: BTreeMap::new(),
|
|
};
|
|
let mut visited = 0;
|
|
for (key, value) in source.cells(&[]) {
|
|
visited += 1;
|
|
let Some(at) = project(&key, cats) else {
|
|
continue;
|
|
};
|
|
let group = shape.groups.entry(at).or_default();
|
|
group.apply(&DeltaValue::new(&value), 1);
|
|
if let CellValue::Number(x) = value {
|
|
group.add_number(x, 1);
|
|
}
|
|
}
|
|
(shape, visited)
|
|
}
|
|
}
|
|
|
|
/// Every shape a read has asked for. A shape is built from the source on its first read (a
|
|
/// full pass) and fed every delta after.
|
|
#[derive(Default)]
|
|
pub struct Shapes {
|
|
built: RefCell<BTreeMap<Vec<String>, Shape>>,
|
|
meter: Cell<Meter>,
|
|
}
|
|
|
|
impl Shapes {
|
|
pub fn meter(&self) -> Meter {
|
|
self.meter.get()
|
|
}
|
|
|
|
fn bump(&self, f: impl FnOnce(&mut Meter)) {
|
|
let mut m = self.meter.get();
|
|
f(&mut m);
|
|
self.meter.set(m);
|
|
}
|
|
|
|
/// Groups held across all shapes.
|
|
pub fn group_count(&self) -> usize {
|
|
self.built.borrow().values().map(|s| s.groups.len()).sum()
|
|
}
|
|
|
|
pub fn shape_count(&self) -> usize {
|
|
self.built.borrow().len()
|
|
}
|
|
|
|
/// Feeds a batch of changes, and the delta they make, to every shape built so far.
|
|
pub fn apply(&mut self, delta: &CellDelta, changes: &[CellChange]) {
|
|
let (mut updates, mut sums) = (0, 0);
|
|
for (cats, shape) in self.built.get_mut().iter_mut() {
|
|
let (u, s) = shape.apply(cats, delta, changes);
|
|
(updates, sums) = (updates + u, sums + s);
|
|
}
|
|
self.bump(|m| {
|
|
m.updates += updates;
|
|
m.sum_updates += sums;
|
|
});
|
|
}
|
|
|
|
/// Runs `read` on the groups at `keys` in the shape of their categories, building the
|
|
/// shape first if no read has asked for it. A key with no group is left out.
|
|
pub fn read<R>(
|
|
&self,
|
|
source: &dyn CellSource,
|
|
keys: &[CellKey],
|
|
read: impl FnOnce(&[&Group]) -> R,
|
|
) -> R {
|
|
let Some(first) = keys.first() else {
|
|
return read(&[]);
|
|
};
|
|
let cats: Vec<String> = first.0.iter().map(|(c, _)| c.clone()).collect();
|
|
debug_assert!(
|
|
keys.iter().all(|k| k.0.len() == cats.len()
|
|
&& k.0.iter().zip(&cats).all(|((c, _), want)| c == want)),
|
|
"every key read together must name the first key's categories"
|
|
);
|
|
let mut built = self.built.borrow_mut();
|
|
let shape = built.entry(cats.clone()).or_insert_with(|| {
|
|
let (shape, visited) = Shape::of(source, &cats);
|
|
self.bump(|m| {
|
|
m.builds += 1;
|
|
m.built_cells += visited;
|
|
});
|
|
shape
|
|
});
|
|
let groups: Vec<&Group> = keys.iter().filter_map(|k| shape.groups.get(k)).collect();
|
|
self.bump(|m| m.reads += keys.len() as u64);
|
|
read(&groups)
|
|
}
|
|
}
|