Files
improvise/crates/improvise-incremental/src/shapes.rs
T

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)
}
}