beck_core/engine.rs
1//! The incremental view engine: the thing that flows deltas through a [`crate::plan::Plan`].
2//!
3//! [`docs/03-type-and-effect-system.md`](../../../../../docs/03-type-and-effect-system.md) §3.8:
4//! "`remaining` updates by ±1 per event, never by recount." Until now that sentence described an
5//! intention. This is the machine that makes it true, and
6//! [`docs/24-incremental-views-report.md`](../../../../../docs/24-incremental-views-report.md) is the
7//! measurement.
8//!
9//! # The one hard problem, and where it is solved
10//!
11//! A Beck program's state is a *value*: `todos = durable(fold(apply_event, empty, events))` produces
12//! a whole new accumulator per event. A dataflow plan consumes *changes*. Something has to convert
13//! one into the other, and doing it by comparing the old and new accumulator entry by entry would
14//! be `O(n)` per event — which is the recount §3.8 exists to abolish, moved one level down where it
15//! is harder to see.
16//!
17//! [`crate::pmap::PMap::diff`] is the conversion, and it is `O(δ log n)` because `Map[K, V]` is a
18//! persistent tree: two versions that differ by one insert *share* every subtree the insert did not
19//! pass through, by pointer, and the diff skips a shared subtree whole. So the delta at the source
20//! costs what the delta is worth. Everything downstream of that is ordinary differential dataflow.
21//!
22//! # Correctness before speed
23//!
24//! The engine's output must be *identical* to recomputing the view — not close, identical, because
25//! the rendered page is diffed into a patch stream and replayed bit for bit (§4.8). Three things
26//! make that checkable rather than hoped for:
27//!
28//! 1. **Every operator the plan cannot decompose is a full recompute** ([`Op::Pointwise`]), so a
29//! program the analysis does not understand is *slow*, never wrong.
30//! 2. **Order is a key, not a sort.** Each arrangement is a `BTreeMap` whose key reproduces the
31//! order the recompute would have produced ([`crate::plan`]), so `map_values` order, `sort_by`
32//! stability and `concat_lists` position all fall out of the key rather than out of a final pass.
33//! 3. **An error resets the engine.** A per-element function that fails leaves an arrangement
34//! half-updated, so [`Engine::render`] discards everything and the next call rebuilds. A stale
35//! arrangement is the one failure mode that would be invisible.
36//!
37//! `beck-cli/tests/incremental_engine.rs` is the harness: every corpus program, every event of a
38//! generated log, engine against recompute, byte for byte.
39//!
40//! # What "changed" means, and why it is never a deep comparison
41//!
42//! A pointwise operator re-runs when an input changed. Deciding that by structural equality would
43//! reintroduce the `O(n)` this module exists to remove, so `same` is a *conservative* test:
44//! scalars compare by value, collections and rendered trees by pointer. It answers "unchanged" only
45//! when it is certain, and "changed" costs a recompute that the old runtime did unconditionally.
46
47use std::collections::{BTreeMap, BTreeSet, VecDeque};
48use std::sync::atomic::{AtomicU64, Ordering};
49use std::sync::{Arc, OnceLock, RwLock};
50
51use beck_diag::Span;
52
53use crate::backend::{Backend, Callable, ExecError};
54use crate::core::Value;
55use crate::plan::{Fun, Op, OpId, Plan};
56use crate::pmap::PMap;
57use crate::split::Placed;
58
59/// What orders an entry inside an arrangement. See [`crate::plan`] for where each operator's key
60/// comes from.
61pub type Key = Arc<[Value]>;
62
63/// One entry's fate at an operator's output.
64#[derive(Clone, Debug)]
65pub struct Change {
66 pub key: Key,
67 pub old: Option<Value>,
68 pub new: Option<Value>,
69}
70
71/// An operator's output as a keyed collection — §5.3's "arrangement".
72#[derive(Clone, Debug, Default)]
73struct Arrangement {
74 entries: BTreeMap<Key, Value>,
75 /// The `Value::List` a pointwise consumer needs, built on demand and dropped when the
76 /// arrangement moves. This is the boundary between the maintained region and the rest: a
77 /// consumer that only asks for the *size* never forces it, which is exactly why `list_len` is
78 /// an operator rather than a pointwise call.
79 ///
80 /// A `OnceLock` rather than an `Option` because a *shared* arrangement is read by many
81 /// subscribers at once, through a read lock ([`SharedDataflow`]): the first one to need the
82 /// list builds it, and the rest get the same `Arc`. With an `Option` the cache would need the
83 /// write lock, which would serialise every subscriber behind the first — and the list is the
84 /// one thing worth sharing most, because building it is the `O(n)` §24.6 named.
85 listed: OnceLock<Value>,
86}
87
88impl Arrangement {
89 fn touch(&mut self) {
90 self.listed = OnceLock::new();
91 }
92
93 /// The list this arrangement stands for, and how many entries had to be copied to build it —
94 /// zero when another reader already had.
95 fn listed_value(&self) -> (Value, u64) {
96 if let Some(v) = self.listed.get() {
97 return (v.clone(), 0);
98 }
99 let listed = Value::List(Arc::new(self.entries.values().cloned().collect()));
100 // A race loses the loser's copy and keeps the winner's; both are the same list, so which
101 // one wins is not observable. `get_or_init` would be neater and would hold a lock.
102 match self.listed.set(listed.clone()) {
103 Ok(()) => (listed, self.entries.len() as u64),
104 Err(_) => (
105 self.listed.get().cloned().unwrap_or(listed),
106 self.entries.len() as u64,
107 ),
108 }
109 }
110}
111
112#[derive(Clone, Debug)]
113enum Out {
114 Val(Value),
115 Arr(Arrangement),
116}
117
118impl Default for Out {
119 fn default() -> Self {
120 Out::Val(Value::Unit)
121 }
122}
123
124/// One operator's runtime state.
125#[derive(Default)]
126struct Cell {
127 out: Out,
128 changed: bool,
129 /// For an arrangement: what moved this tick.
130 changes: Vec<Change>,
131 /// Set when this operator threw its arrangement away and rebuilt it this tick.
132 ///
133 /// A rebuild emits *inserts only* — there is no previous arrangement left to derive removals
134 /// from — so a consumer that merely applied those inserts to its own arrangement would keep
135 /// every entry the rebuild dropped. This is how a subscriber switching sessions saw another
136 /// subscriber's rows, and the flag is the fix: a rebuild is contagious downstream.
137 rebuilt: bool,
138 /// `map_values`: the map this operator last saw, so the next one can be diffed against it.
139 seen_map: PMap<Value, Value>,
140 /// For each input that arrives as a plain list rather than as an arrangement, the copy this
141 /// operator last saw. A list-valued input has no deltas of its own, so the operator makes them.
142 shadow: Vec<BTreeMap<Key, Value>>,
143 /// `sort_by`: where each input key currently sits in the output order.
144 positions: BTreeMap<Key, Key>,
145 /// `flatten`: how many entries each input key currently contributes, so the old ones can be
146 /// withdrawn without scanning the arrangement.
147 counts: BTreeMap<Key, usize>,
148}
149
150/// What one [`Engine::render`] cost, in units that do not depend on the machine.
151///
152/// Wall-clock is measured in the harness; this is what a test asserts on, because "the count did
153/// not visit every row" is the claim, and a timing assertion in CI is a flake.
154#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
155pub struct Work {
156 /// Per-element functions applied — the `f` of a `map_list`, the predicate of a `filter_list`.
157 pub applications: u64,
158 /// Entries a delta operator inserted, updated or removed.
159 pub touched: u64,
160 /// Entries copied to hand a pointwise consumer a `Value::List`.
161 pub materialised: u64,
162 /// Pointwise operators re-evaluated.
163 pub recomputed: u64,
164}
165
166impl Work {
167 /// Everything that scales with the collection rather than with the change.
168 pub fn total(&self) -> u64 {
169 self.applications + self.touched + self.materialised + self.recomputed
170 }
171}
172
173/// A plan with every operator's code prepared: one per *program*, shared by every subscription.
174///
175/// The split between this and [`Engine`] is the difference between a plan and an arrangement, and
176/// it is load-bearing for §5.3's fanout. Preparing an operator means asking the backend to turn
177/// `Core` into something callable, which a compiling backend does expensively and even a
178/// tree-walker does by cloning the expression; doing it per subscriber cost about 90 KB of a
179/// subscription that then held 60 entries. A thousand subscribers share one of these.
180pub struct Prepared {
181 plan: Arc<Plan>,
182 /// The pointwise operators' bodies and the collection operators' per-element functions.
183 code: Vec<Option<Callable>>,
184 funs: Vec<Option<Callable>>,
185 /// Constants, evaluated once here and never recomputed.
186 consts: Vec<Option<Value>>,
187}
188
189impl Prepared {
190 pub fn new(plan: Arc<Plan>, backend: &dyn Backend) -> Result<Prepared, ExecError> {
191 let n = plan.nodes.len();
192 let mut code: Vec<Option<Callable>> = Vec::with_capacity(n);
193 let mut funs: Vec<Option<Callable>> = Vec::with_capacity(n);
194 for node in &plan.nodes {
195 code.push(match &node.op {
196 Op::Pointwise { code } => Some(backend.function(code)?),
197 _ => None,
198 });
199 funs.push(match &node.op {
200 Op::MapList { f } | Op::FilterList { f } | Op::SortBy { f } | Op::FlatMap { f } => {
201 Some(backend.function(&f.code)?)
202 }
203 _ => None,
204 });
205 }
206 let mut consts: Vec<Option<Value>> = vec![None; n];
207 for (&id, expr) in &plan.constants {
208 consts[id] = Some(backend.constant(expr)?);
209 }
210 Ok(Prepared {
211 plan,
212 code,
213 funs,
214 consts,
215 })
216 }
217
218 /// Compile and prepare a sliced program's view in one step.
219 pub fn compile(placed: &Placed, backend: &dyn Backend) -> Result<Prepared, ExecError> {
220 Prepared::new(Arc::new(Plan::compile(placed)), backend)
221 }
222
223 pub fn plan(&self) -> &Arc<Plan> {
224 &self.plan
225 }
226}
227
228/// One subscriber's arrangements over a [`Prepared`] plan.
229pub struct Engine {
230 prepared: Arc<Prepared>,
231 cells: Vec<Cell>,
232 /// Which of the plan's operators this engine computes and holds.
233 ///
234 /// All of them for a standalone engine. For a subscriber attached to a [`SharedDataflow`] it is
235 /// exactly the `per_session` nodes: the rest arrive from upstream, held once between every
236 /// subscriber, which is §5.3's sentence.
237 owns: Arc<[bool]>,
238 /// Whether any state at all has been established. Cleared by an error, so the next render
239 /// rebuilds rather than trusting a half-updated arrangement.
240 warm: bool,
241 /// The shared version this engine last rendered against, so the changes it has not yet seen can
242 /// be found. Meaningless for a standalone engine, which has no upstream to lag behind.
243 seen: u64,
244 /// This engine's place in a [`SharedDataflow`]'s reader set, for as long as it lives.
245 ///
246 /// `None` for a standalone engine, which owns every operator and has nobody to tell when it
247 /// goes away.
248 attached: Option<Attachment>,
249 work: Work,
250}
251
252/// A subscriber's membership of a shared dataflow's reader set.
253///
254/// Two facts the dataflow cannot learn any other way: that this reader exists — so the
255/// arrangements are not dropped underneath it — and how far behind it is, which is what bounds
256/// how much change history is worth keeping.
257///
258/// The frontier is an atomic rather than an entry in a map under the dataflow's lock, because it
259/// is written on **every** render and read only when the dataflow advances. A map would make the
260/// hot path take a write lock and serialise the concurrent renders §5.3 exists to allow.
261struct Attachment {
262 shared: Arc<SharedDataflow>,
263 id: ReaderId,
264 /// The version this reader has rendered up to, or [`UNRENDERED`].
265 frontier: Arc<AtomicU64>,
266}
267
268impl Drop for Engine {
269 fn drop(&mut self) {
270 if let Some(a) = &self.attached {
271 a.shared.detach(a.id);
272 }
273 }
274}
275
276impl Engine {
277 /// A fresh subscriber's view over a plan the program prepared once, computing every operator
278 /// itself.
279 pub fn new(prepared: Arc<Prepared>) -> Engine {
280 let owns: Arc<[bool]> = (0..prepared.plan.nodes.len()).map(|_| true).collect();
281 Engine::for_nodes(prepared, owns)
282 }
283
284 /// A subscriber's half of a plan whose shared prefix a [`SharedDataflow`] maintains.
285 ///
286 /// It owns the `per_session` operators and nothing else. Rendering it requires the shared side
287 /// — [`SharedDataflow::render`] — because the operators it does not own are where its inputs
288 /// come from.
289 pub fn subscriber(prepared: Arc<Prepared>) -> Engine {
290 let owns: Arc<[bool]> = prepared.plan.nodes.iter().map(|n| n.per_session).collect();
291 Engine::for_nodes(prepared, owns)
292 }
293
294 fn for_nodes(prepared: Arc<Prepared>, owns: Arc<[bool]>) -> Engine {
295 let mut cells: Vec<Cell> = (0..prepared.plan.nodes.len())
296 .map(|_| Cell::default())
297 .collect();
298 for (i, v) in prepared.consts.iter().enumerate() {
299 if let Some(v) = v {
300 if owns[i] {
301 cells[i].out = Out::Val(v.clone());
302 }
303 }
304 }
305 Engine {
306 prepared,
307 cells,
308 owns,
309 warm: false,
310 seen: 0,
311 attached: None,
312 work: Work::default(),
313 }
314 }
315
316 /// Whether this engine computes an operator itself, rather than reading it from upstream.
317 fn owns(&self, id: OpId) -> bool {
318 self.owns[id]
319 }
320
321 pub fn plan(&self) -> &Arc<Plan> {
322 &self.prepared.plan
323 }
324
325 /// What the last [`Engine::render`] cost.
326 pub fn work(&self) -> Work {
327 self.work
328 }
329
330 /// How many entries every arrangement is holding — §5.3's per-session memory, in the unit that
331 /// scales.
332 pub fn arranged(&self) -> u64 {
333 self.arrangement_entries(|_| true)
334 }
335
336 /// The same count, restricted to arrangements that do **not** read the session.
337 ///
338 /// This is the part §5.3 says a thousand subscribers should hold *once* between them. A
339 /// subscriber attached to a [`SharedDataflow`] does not own those operators at all, so this is
340 /// zero for it and the entries are counted once, on [`SharedDataflow::arranged`].
341 pub fn arranged_shared(&self) -> u64 {
342 self.arrangement_entries(|per_session| !per_session)
343 }
344
345 fn arrangement_entries(&self, want: impl Fn(bool) -> bool) -> u64 {
346 self.cells
347 .iter()
348 .enumerate()
349 .filter(|(i, _)| self.owns[*i])
350 .map(|(i, c)| match &c.out {
351 Out::Arr(a) if want(self.prepared.plan.nodes[i].per_session) => {
352 a.entries.len() as u64
353 }
354 _ => 0,
355 })
356 .sum()
357 }
358
359 /// Discard everything. The next render rebuilds from the state it is given.
360 pub fn reset(&mut self) {
361 for (i, cell) in self.cells.iter_mut().enumerate() {
362 *cell = Cell::default();
363 // A constant's value is still valid — only the arrangements are suspect.
364 if let Some(v) = &self.prepared.consts[i] {
365 if self.owns[i] {
366 cell.out = Out::Val(v.clone());
367 }
368 }
369 }
370 self.warm = false;
371 self.seen = 0;
372 }
373
374 /// Render this subscriber's view of a state, maintaining whatever the plan can maintain.
375 ///
376 /// Correct for *any* state, not only the successor of the last one: an operator that cannot
377 /// derive a delta rebuilds. That matters because a reconnecting subscriber is rendered against
378 /// an older state (`beck-rt`'s resumption path), and an engine that assumed monotonic progress
379 /// would quietly serve it the wrong page.
380 pub fn render(
381 &mut self,
382 state: &Value,
383 session: &Value,
384 presence: &Value,
385 ) -> Result<Value, ExecError> {
386 self.render_from(None, state, session, presence)
387 }
388
389 /// The same render, with the operators this engine does not own arriving from upstream.
390 fn render_from(
391 &mut self,
392 up: Option<Upstream<'_>>,
393 state: &Value,
394 session: &Value,
395 presence: &Value,
396 ) -> Result<Value, ExecError> {
397 self.work = Work::default();
398 match self
399 .tick(up, state, session, presence)
400 .and_then(|()| self.materialise(up, self.prepared.plan.root))
401 {
402 Ok(v) => {
403 self.warm = true;
404 Ok(v)
405 }
406 Err(e) => {
407 // A failed per-element function leaves an arrangement holding entries from two
408 // different states. Nothing downstream could detect that, so it is thrown away.
409 self.reset();
410 Err(e)
411 }
412 }
413 }
414
415 /// Advance the operators this engine owns, without assembling a page from them.
416 ///
417 /// This is the shared half of [`SharedDataflow`]: the root of the plan is per-session and this
418 /// engine does not own it, so there is nothing at the top to materialise.
419 fn advance(&mut self, state: &Value) -> Result<(), ExecError> {
420 self.work = Work::default();
421 // The shared half owns no `Op::Presence` — everything downstream of one is per-subscriber
422 // — so the value it would be given is never read.
423 match self.tick(None, state, &Value::Unit, &Value::Unit) {
424 Ok(()) => {
425 self.warm = true;
426 Ok(())
427 }
428 Err(e) => {
429 self.reset();
430 Err(e)
431 }
432 }
433 }
434
435 fn tick(
436 &mut self,
437 up: Option<Upstream<'_>>,
438 state: &Value,
439 session: &Value,
440 presence: &Value,
441 ) -> Result<(), ExecError> {
442 let cold = !self.warm;
443 // The plan is behind an `Arc`, so this is one refcount rather than a clone of every
444 // operator's `Core` — which is what matching on `self.plan` directly would have cost, once
445 // per node per event.
446 let plan = self.prepared.plan.clone();
447 for id in 0..plan.nodes.len() {
448 // Not ours: it belongs to the shared dataflow, and reading it goes through `up`.
449 if !self.owns(id) {
450 continue;
451 }
452 match &plan.nodes[id].op {
453 Op::State => {
454 self.cells[id].rebuilt = false;
455 // Always "changed": the caller renders because the fold moved, and proving it
456 // did not would cost a structural comparison of the whole accumulator — the
457 // recount this engine exists to remove. Every consumer below is either a field
458 // read (`O(1)`) or a `map_values` (`O(δ log n)`).
459 self.cells[id].out = Out::Val(state.clone());
460 self.cells[id].changed = true;
461 }
462 Op::Session => {
463 self.cells[id].rebuilt = false;
464 let changed =
465 cold || !matches!(&self.cells[id].out, Out::Val(v) if same(v, session));
466 self.cells[id].out = Out::Val(session.clone());
467 self.cells[id].changed = changed;
468 }
469 // Compared rather than assumed changed, like the session and unlike the
470 // accumulator: most renders are provoked by an event rather than by a connection,
471 // so the common case is one comparison of two identical rosters and nothing below
472 // this operator re-runs.
473 Op::Presence => {
474 self.cells[id].rebuilt = false;
475 let changed =
476 cold || !matches!(&self.cells[id].out, Out::Val(v) if same(v, presence));
477 self.cells[id].out = Out::Val(presence.clone());
478 self.cells[id].changed = changed;
479 }
480 Op::Const => {
481 self.cells[id].rebuilt = false;
482 self.cells[id].changed = cold;
483 }
484 Op::Pointwise { .. } => self.pointwise(up, id, cold)?,
485 Op::MapValues => self.map_values(up, id, cold)?,
486 Op::MapList { f } => self.map_list(up, id, f, cold)?,
487 Op::FilterList { f } => self.filter_list(up, id, f, cold)?,
488 Op::SortBy { f } => self.sort_by(up, id, f, cold)?,
489 Op::Concat => self.concat(up, id, cold)?,
490 Op::Flatten => self.flatten(up, id, None, cold)?,
491 Op::FlatMap { f } => self.flatten(up, id, Some(f), cold)?,
492 Op::Count => self.aggregate(up, id, cold, false)?,
493 Op::IsEmpty => self.aggregate(up, id, cold, true)?,
494 }
495 }
496 Ok(())
497 }
498
499 // ---------------------------------------------------------------------------------------
500 // Operators
501 // ---------------------------------------------------------------------------------------
502
503 fn pointwise(
504 &mut self,
505 up: Option<Upstream<'_>>,
506 id: OpId,
507 cold: bool,
508 ) -> Result<(), ExecError> {
509 self.cells[id].rebuilt = false;
510 // An `Arc` bump, not a copy: this runs for every pointwise operator on every tick.
511 let plan = self.prepared.plan.clone();
512 let inputs = &plan.nodes[id].inputs;
513 if !cold && !inputs.iter().any(|&i| self.changed_of(up, i)) {
514 self.cells[id].changed = false;
515 return Ok(());
516 }
517 let mut args = Vec::with_capacity(inputs.len());
518 for &i in inputs {
519 args.push(self.materialise(up, i)?);
520 }
521 let f = self.prepared.code[id]
522 .as_ref()
523 .ok_or_else(|| ExecError::new("a pointwise operator has no prepared body", Span::NONE))?
524 .clone();
525 let next = f(args)?;
526 self.work.recomputed += 1;
527 let changed = match &self.cells[id].out {
528 Out::Val(prev) => !same(prev, &next),
529 Out::Arr(_) => true,
530 };
531 self.cells[id].out = Out::Val(next);
532 self.cells[id].changed = changed || cold;
533 Ok(())
534 }
535
536 /// `map_values(m)` — the source. Every other operator's deltas descend from this one.
537 fn map_values(
538 &mut self,
539 up: Option<Upstream<'_>>,
540 id: OpId,
541 cold: bool,
542 ) -> Result<(), ExecError> {
543 let input = self.prepared.plan.nodes[id].inputs[0];
544 if !cold && !self.changed_of(up, input) {
545 self.cells[id].changed = false;
546 self.cells[id].changes.clear();
547 self.cells[id].rebuilt = false;
548 return Ok(());
549 }
550 let source = match self.out_of(up, input)? {
551 Out::Val(Value::Map(m)) => Some(m.clone()),
552 // Not a map. The plan said this was `map_values`, so the only way here is a program the
553 // checker would have refused; rebuild wholesale rather than guess.
554 _ => None,
555 };
556 let Some(next) = source else {
557 let whole = self.materialise(up, input)?;
558 let entries = list_entries(&whole);
559 return self.replace(id, entries);
560 };
561 let seen = if cold {
562 PMap::new()
563 } else {
564 self.cells[id].seen_map.clone()
565 };
566 let mut arr = if cold {
567 Arrangement::default()
568 } else {
569 match std::mem::take(&mut self.cells[id].out) {
570 Out::Arr(a) => a,
571 Out::Val(_) => Arrangement::default(),
572 }
573 };
574 let mut changes = Vec::new();
575 for c in seen.diff(&next) {
576 let key: Key = Arc::from(vec![c.key]);
577 match &c.new {
578 Some(v) => {
579 arr.entries.insert(key.clone(), v.clone());
580 }
581 None => {
582 arr.entries.remove(&key);
583 }
584 }
585 changes.push(Change {
586 key,
587 old: c.old,
588 new: c.new,
589 });
590 }
591 self.cells[id].seen_map = next;
592 self.publish(id, arr, changes, cold);
593 Ok(())
594 }
595
596 fn map_list(
597 &mut self,
598 up: Option<Upstream<'_>>,
599 id: OpId,
600 f: &Fun,
601 cold: bool,
602 ) -> Result<(), ExecError> {
603 let (incoming, rebuild) = self.incoming(up, id, 0, f, cold)?;
604 if incoming.is_empty() && !rebuild {
605 self.cells[id].changed = false;
606 self.cells[id].changes.clear();
607 // Cleared, and this is not housekeeping. `rebuilt` means "threw its arrangement away
608 // *this tick*"; leaving the cold start's `true` here made it mean "has ever rebuilt",
609 // and a rebuild is contagious downstream — so every operator below a collection that
610 // had stopped changing rebuilt on every event, for the life of the subscription.
611 // `concat` and `flatten` always cleared it; these three never did.
612 self.cells[id].rebuilt = false;
613 return Ok(());
614 }
615 let call = self.fun_of(id)?;
616 let captured = self.captures(up, f)?;
617 let mut arr = self.take_arrangement(id, rebuild);
618 let mut changes = Vec::new();
619 for c in incoming {
620 match c.new {
621 Some(v) => {
622 let mut args = captured.clone();
623 args.push(v);
624 let mapped = call(args)?;
625 self.work.applications += 1;
626 let old = arr.entries.insert(c.key.clone(), mapped.clone());
627 changes.push(Change {
628 key: c.key,
629 old,
630 new: Some(mapped),
631 });
632 }
633 None => {
634 let old = arr.entries.remove(&c.key);
635 if old.is_some() {
636 changes.push(Change {
637 key: c.key,
638 old,
639 new: None,
640 });
641 }
642 }
643 }
644 }
645 self.publish(id, arr, changes, rebuild);
646 Ok(())
647 }
648
649 fn filter_list(
650 &mut self,
651 up: Option<Upstream<'_>>,
652 id: OpId,
653 f: &Fun,
654 cold: bool,
655 ) -> Result<(), ExecError> {
656 let (incoming, rebuild) = self.incoming(up, id, 0, f, cold)?;
657 if incoming.is_empty() && !rebuild {
658 self.cells[id].changed = false;
659 self.cells[id].changes.clear();
660 // Cleared, and this is not housekeeping. `rebuilt` means "threw its arrangement away
661 // *this tick*"; leaving the cold start's `true` here made it mean "has ever rebuilt",
662 // and a rebuild is contagious downstream — so every operator below a collection that
663 // had stopped changing rebuilt on every event, for the life of the subscription.
664 // `concat` and `flatten` always cleared it; these three never did.
665 self.cells[id].rebuilt = false;
666 return Ok(());
667 }
668 let call = self.fun_of(id)?;
669 let captured = self.captures(up, f)?;
670 let mut arr = self.take_arrangement(id, rebuild);
671 let mut changes = Vec::new();
672 for c in incoming {
673 let keep = match &c.new {
674 Some(v) => {
675 let mut args = captured.clone();
676 args.push(v.clone());
677 let verdict = call(args)?;
678 self.work.applications += 1;
679 verdict.as_bool().unwrap_or(false)
680 }
681 None => false,
682 };
683 if keep {
684 let v = c.new.expect("kept means present");
685 let old = arr.entries.insert(c.key.clone(), v.clone());
686 changes.push(Change {
687 key: c.key,
688 old,
689 new: Some(v),
690 });
691 } else if let Some(old) = arr.entries.remove(&c.key) {
692 changes.push(Change {
693 key: c.key,
694 old: Some(old),
695 new: None,
696 });
697 }
698 }
699 self.publish(id, arr, changes, rebuild);
700 Ok(())
701 }
702
703 /// `sort_by(xs, k)` — an ordered arrangement, maintained by insertion.
704 ///
705 /// The output key is `k(x)` followed by the input's key. That second component is what makes
706 /// the sort *stable* in the same way the recompute's is: two elements with equal keys keep the
707 /// order they had at the input, and "the order they had" is exactly the input's key.
708 fn sort_by(
709 &mut self,
710 up: Option<Upstream<'_>>,
711 id: OpId,
712 f: &Fun,
713 cold: bool,
714 ) -> Result<(), ExecError> {
715 let (incoming, rebuild) = self.incoming(up, id, 0, f, cold)?;
716 if incoming.is_empty() && !rebuild {
717 self.cells[id].changed = false;
718 self.cells[id].changes.clear();
719 // Cleared, and this is not housekeeping. `rebuilt` means "threw its arrangement away
720 // *this tick*"; leaving the cold start's `true` here made it mean "has ever rebuilt",
721 // and a rebuild is contagious downstream — so every operator below a collection that
722 // had stopped changing rebuilt on every event, for the life of the subscription.
723 // `concat` and `flatten` always cleared it; these three never did.
724 self.cells[id].rebuilt = false;
725 return Ok(());
726 }
727 let call = self.fun_of(id)?;
728 let captured = self.captures(up, f)?;
729 let mut arr = self.take_arrangement(id, rebuild);
730 if rebuild {
731 self.cells[id].positions.clear();
732 }
733 let mut positions = std::mem::take(&mut self.cells[id].positions);
734 let mut changes = Vec::new();
735 for c in incoming {
736 if let Some(was) = positions.remove(&c.key) {
737 if let Some(old) = arr.entries.remove(&was) {
738 changes.push(Change {
739 key: was,
740 old: Some(old),
741 new: None,
742 });
743 }
744 }
745 let Some(v) = c.new else { continue };
746 let mut args = captured.clone();
747 args.push(v.clone());
748 let sort_key = call(args)?;
749 self.work.applications += 1;
750 let mut out_key: Vec<Value> = vec![sort_key];
751 out_key.extend(c.key.iter().cloned());
752 let out_key: Key = Arc::from(out_key);
753 arr.entries.insert(out_key.clone(), v.clone());
754 positions.insert(c.key, out_key.clone());
755 changes.push(Change {
756 key: out_key,
757 old: None,
758 new: Some(v),
759 });
760 }
761 self.cells[id].positions = positions;
762 self.publish(id, arr, changes, rebuild);
763 Ok(())
764 }
765
766 /// `concat_lists([a, b, …])` — a union of delta streams, keyed by which stream.
767 fn concat(&mut self, up: Option<Upstream<'_>>, id: OpId, cold: bool) -> Result<(), ExecError> {
768 let plan = self.prepared.plan.clone();
769 let inputs = &plan.nodes[id].inputs;
770 let rebuild = cold || inputs.iter().any(|&i| self.rebuilt_of(up, i));
771 let mut arr = self.take_arrangement(id, rebuild);
772 let mut changes = Vec::new();
773 for (slot, input) in inputs.iter().copied().enumerate() {
774 let incoming = self.feed(up, id, slot, input, rebuild)?;
775 for c in incoming {
776 let mut key: Vec<Value> = vec![Value::Int(slot as i64)];
777 key.extend(c.key.iter().cloned());
778 let key: Key = Arc::from(key);
779 match c.new {
780 Some(v) => {
781 let old = arr.entries.insert(key.clone(), v.clone());
782 changes.push(Change {
783 key,
784 old,
785 new: Some(v),
786 });
787 }
788 None => {
789 if let Some(old) = arr.entries.remove(&key) {
790 changes.push(Change {
791 key,
792 old: Some(old),
793 new: None,
794 });
795 }
796 }
797 }
798 }
799 }
800 if changes.is_empty() && !rebuild {
801 self.cells[id].out = Out::Arr(arr);
802 self.cells[id].changed = false;
803 self.cells[id].changes.clear();
804 self.cells[id].rebuilt = false;
805 return Ok(());
806 }
807 self.publish(id, arr, changes, rebuild);
808 Ok(())
809 }
810
811 /// `concat_lists(xs)` where `xs` is a collection of lists — a flatten, and what every `for`
812 /// loop in a `ui:` block compiles to.
813 ///
814 /// The output key is the input's key followed by the position inside that element's list, so
815 /// one row's children move without disturbing anybody else's, and the order is the order the
816 /// recompute would have produced.
817 fn flatten(
818 &mut self,
819 up: Option<Upstream<'_>>,
820 id: OpId,
821 f: Option<&Fun>,
822 cold: bool,
823 ) -> Result<(), ExecError> {
824 let input = self.prepared.plan.nodes[id].inputs[0];
825 // With a function, the rebuild rule is `map_list`'s rather than `flatten`'s: a captured
826 // node that moved makes `f` a different function, so every element has to be reapplied.
827 let (incoming, rebuild) = match f {
828 Some(f) => self.incoming(up, id, 0, f, cold)?,
829 None => {
830 let rebuild = cold || self.rebuilt_of(up, input);
831 (self.feed(up, id, 0, input, rebuild)?, rebuild)
832 }
833 };
834 if incoming.is_empty() && !rebuild {
835 self.cells[id].changed = false;
836 self.cells[id].changes.clear();
837 self.cells[id].rebuilt = false;
838 return Ok(());
839 }
840 let call = match f {
841 Some(_) => Some(self.fun_of(id)?),
842 None => None,
843 };
844 let captured = match f {
845 Some(f) => self.captures(up, f)?,
846 None => Vec::new(),
847 };
848 let mut arr = self.take_arrangement(id, rebuild);
849 if rebuild {
850 self.cells[id].counts.clear();
851 }
852 let mut counts = std::mem::take(&mut self.cells[id].counts);
853 let mut changes = Vec::new();
854 for c in incoming {
855 if let Some(n) = counts.remove(&c.key) {
856 for i in 0..n {
857 let key = inner_key(&c.key, i);
858 if let Some(old) = arr.entries.remove(&key) {
859 changes.push(Change {
860 key,
861 old: Some(old),
862 new: None,
863 });
864 }
865 }
866 }
867 let Some(v) = c.new else { continue };
868 let v = match &call {
869 Some(call) => {
870 let mut args = captured.clone();
871 args.push(v);
872 let out = call(args)?;
873 self.work.applications += 1;
874 out
875 }
876 None => v,
877 };
878 let items = v.as_list().cloned().unwrap_or_default();
879 for (i, item) in items.iter().enumerate() {
880 let key = inner_key(&c.key, i);
881 arr.entries.insert(key.clone(), item.clone());
882 changes.push(Change {
883 key,
884 old: None,
885 new: Some(item.clone()),
886 });
887 }
888 counts.insert(c.key, items.len());
889 }
890 self.cells[id].counts = counts;
891 self.publish(id, arr, changes, rebuild);
892 Ok(())
893 }
894
895 /// `list_len` and `list_is_empty`: read the arrangement's size.
896 ///
897 /// This is §3.8's sentence, mechanised. It reads `entries.len()` — `O(1)` — and, crucially,
898 /// never calls [`Engine::materialise`], so a program that only asks how many there are never
899 /// pays for a list of them.
900 fn aggregate(
901 &mut self,
902 up: Option<Upstream<'_>>,
903 id: OpId,
904 cold: bool,
905 emptiness: bool,
906 ) -> Result<(), ExecError> {
907 self.cells[id].rebuilt = false;
908 let input = self.prepared.plan.nodes[id].inputs[0];
909 if !cold && !self.changed_of(up, input) {
910 self.cells[id].changed = false;
911 return Ok(());
912 }
913 let n = match self.out_of(up, input)? {
914 Out::Arr(a) => a.entries.len(),
915 Out::Val(Value::List(xs)) => xs.len(),
916 Out::Val(Value::Map(m)) => m.len(),
917 Out::Val(_) => {
918 let whole = self.materialise(up, input)?;
919 whole.as_list().map(|l| l.len()).unwrap_or(0)
920 }
921 };
922 let next = if emptiness {
923 Value::Bool(n == 0)
924 } else {
925 Value::Int(n as i64)
926 };
927 let changed = match &self.cells[id].out {
928 Out::Val(prev) => !same(prev, &next),
929 Out::Arr(_) => true,
930 };
931 self.cells[id].out = Out::Val(next);
932 self.cells[id].changed = changed || cold;
933 Ok(())
934 }
935
936 // ---------------------------------------------------------------------------------------
937 // Plumbing
938 // ---------------------------------------------------------------------------------------
939
940 /// The changes arriving at a collection operator, and whether it has to rebuild.
941 ///
942 /// Three things force a rebuild, and only the first is interesting:
943 ///
944 /// * the operator's per-element function *captured* something that moved —
945 /// `lambda t: t.owner == session.actor` is a different predicate for a different session, so
946 /// every element has to be reconsidered. This is the one case where the answer genuinely does
947 /// depend on the whole collection;
948 /// * an input rebuilt, because a rebuild's changes are inserts with no matching removals;
949 /// * the engine is cold.
950 fn incoming(
951 &mut self,
952 up: Option<Upstream<'_>>,
953 id: OpId,
954 slot: usize,
955 f: &Fun,
956 cold: bool,
957 ) -> Result<(Vec<Change>, bool), ExecError> {
958 let input = self.prepared.plan.nodes[id].inputs[slot];
959 let rebuild = cold
960 || self.rebuilt_of(up, input)
961 || f.captures.iter().any(|&c| self.changed_of(up, c));
962 let changes = self.feed(up, id, slot, input, rebuild)?;
963 Ok((changes, rebuild))
964 }
965
966 /// Changes at one input, whether it is an arrangement or a plain list.
967 fn feed(
968 &mut self,
969 up: Option<Upstream<'_>>,
970 id: OpId,
971 slot: usize,
972 input: OpId,
973 whole: bool,
974 ) -> Result<Vec<Change>, ExecError> {
975 let is_arr = matches!(self.out_of(up, input)?, Out::Arr(_));
976 if is_arr && !whole {
977 return Ok(if self.changed_of(up, input) {
978 self.changes_of(up, input)
979 } else {
980 Vec::new()
981 });
982 }
983 if is_arr {
984 let changes: Vec<Change> = match self.out_of(up, input)? {
985 Out::Arr(a) => a
986 .entries
987 .iter()
988 .map(|(k, v)| Change {
989 key: k.clone(),
990 old: None,
991 new: Some(v.clone()),
992 })
993 .collect(),
994 Out::Val(_) => Vec::new(),
995 };
996 while self.cells[id].shadow.len() <= slot {
997 self.cells[id].shadow.push(BTreeMap::new());
998 }
999 self.cells[id].shadow[slot].clear();
1000 return Ok(changes);
1001 }
1002 // A plain list: no deltas of its own, so this operator makes them by comparing against the
1003 // copy it last saw. `O(n)` in the list's length — which is the honest cost of a collection
1004 // that arrived from a `match` or an `if` rather than from an arrangement.
1005 if !whole && !self.changed_of(up, input) {
1006 return Ok(Vec::new());
1007 }
1008 let value = self.materialise(up, input)?;
1009 let next: BTreeMap<Key, Value> = list_entries(&value).into_iter().collect();
1010 while self.cells[id].shadow.len() <= slot {
1011 self.cells[id].shadow.push(BTreeMap::new());
1012 }
1013 if whole {
1014 self.cells[id].shadow[slot].clear();
1015 }
1016 // Diff before storing, so `next` moves into the shadow rather than being cloned into it.
1017 let prev = &self.cells[id].shadow[slot];
1018 let mut changes = Vec::new();
1019 for (key, v) in &next {
1020 match prev.get(key) {
1021 Some(before) if before == v => {}
1022 before => changes.push(Change {
1023 key: key.clone(),
1024 old: before.cloned(),
1025 new: Some(v.clone()),
1026 }),
1027 }
1028 }
1029 for (key, before) in prev {
1030 if !next.contains_key(key) {
1031 changes.push(Change {
1032 key: key.clone(),
1033 old: Some(before.clone()),
1034 new: None,
1035 });
1036 }
1037 }
1038 changes.sort_by(|a, b| a.key.cmp(&b.key));
1039 self.cells[id].shadow[slot] = next;
1040 Ok(changes)
1041 }
1042
1043 fn take_arrangement(&mut self, id: OpId, rebuild: bool) -> Arrangement {
1044 if rebuild {
1045 self.cells[id].positions.clear();
1046 return Arrangement::default();
1047 }
1048 match std::mem::take(&mut self.cells[id].out) {
1049 Out::Arr(a) => a,
1050 Out::Val(_) => Arrangement::default(),
1051 }
1052 }
1053
1054 fn publish(&mut self, id: OpId, mut arr: Arrangement, changes: Vec<Change>, rebuilt: bool) {
1055 self.work.touched += changes.len() as u64;
1056 arr.touch();
1057 self.cells[id].changed = !changes.is_empty() || rebuilt;
1058 self.cells[id].changes = changes;
1059 self.cells[id].rebuilt = rebuilt;
1060 self.cells[id].out = Out::Arr(arr);
1061 }
1062
1063 /// The whole collection as inserts — the path taken when an operator cannot derive a delta.
1064 fn replace(&mut self, id: OpId, entries: Vec<(Key, Value)>) -> Result<(), ExecError> {
1065 let mut arr = Arrangement::default();
1066 let mut changes = Vec::new();
1067 for (key, v) in entries {
1068 arr.entries.insert(key.clone(), v.clone());
1069 changes.push(Change {
1070 key,
1071 old: None,
1072 new: Some(v),
1073 });
1074 }
1075 self.publish(id, arr, changes, true);
1076 Ok(())
1077 }
1078
1079 fn fun_of(&self, id: OpId) -> Result<Callable, ExecError> {
1080 self.prepared.funs[id].clone().ok_or_else(|| {
1081 ExecError::new("a collection operator has no prepared function", Span::NONE)
1082 })
1083 }
1084
1085 fn captures(&mut self, up: Option<Upstream<'_>>, f: &Fun) -> Result<Vec<Value>, ExecError> {
1086 let mut out = Vec::with_capacity(f.captures.len());
1087 for &c in &f.captures {
1088 out.push(self.materialise(up, c)?);
1089 }
1090 Ok(out)
1091 }
1092
1093 /// The value of a node, building the list an arrangement stands for if a consumer needs it.
1094 ///
1095 /// This is where the remaining `O(n)` lives, and naming it is the point: assembling `n`
1096 /// elements into a `Value::List` for a pointwise consumer copies `n` handles per event even
1097 /// when one of them moved. What it does *not* do is re-derive the elements — those came from
1098 /// the arrangement, and only the changed ones were computed.
1099 ///
1100 /// For a *shared* arrangement it is also copied only once between every subscriber, because the
1101 /// cache lives beside the arrangement rather than in the engine that asked.
1102 fn materialise(&mut self, up: Option<Upstream<'_>>, id: OpId) -> Result<Value, ExecError> {
1103 let (listed, n) = match self.out_of(up, id)? {
1104 Out::Val(v) => return Ok(v.clone()),
1105 Out::Arr(a) => a.listed_value(),
1106 };
1107 self.work.materialised += n;
1108 Ok(listed)
1109 }
1110
1111 // ---------------------------------------------------------------------------------------
1112 // Reading a node this engine may not own
1113 // ---------------------------------------------------------------------------------------
1114
1115 /// A node's output, from this engine's own cells or from the shared dataflow above it.
1116 fn out_of<'e>(&'e self, up: Option<Upstream<'e>>, id: OpId) -> Result<&'e Out, ExecError> {
1117 if self.owns(id) {
1118 return Ok(&self.cells[id].out);
1119 }
1120 match up {
1121 Some(u) => Ok(u.out(id)),
1122 None => Err(missing_upstream(id)),
1123 }
1124 }
1125
1126 /// Whether a node moved since this engine last looked at it.
1127 ///
1128 /// For an upstream node that is "since the version this subscriber last rendered", not "at the
1129 /// latest version" — a subscriber that skipped three events has to see all three, or an
1130 /// operator below it would keep an entry the shared side has already withdrawn.
1131 fn changed_of(&self, up: Option<Upstream<'_>>, id: OpId) -> bool {
1132 if self.owns(id) {
1133 return self.cells[id].changed;
1134 }
1135 // No upstream where one is needed is an error the caller will raise when it reads the
1136 // value; answering "changed" here keeps it on the path that does.
1137 up.map(|u| u.changed(id)).unwrap_or(true)
1138 }
1139
1140 fn rebuilt_of(&self, up: Option<Upstream<'_>>, id: OpId) -> bool {
1141 if self.owns(id) {
1142 return self.cells[id].rebuilt;
1143 }
1144 up.map(|u| u.rebuilt(id)).unwrap_or(true)
1145 }
1146
1147 fn changes_of(&self, up: Option<Upstream<'_>>, id: OpId) -> Vec<Change> {
1148 if self.owns(id) {
1149 return self.cells[id].changes.clone();
1150 }
1151 up.map(|u| u.changes(id)).unwrap_or_default()
1152 }
1153}
1154
1155fn missing_upstream(id: OpId) -> ExecError {
1156 ExecError::new(
1157 format!("operator {id} belongs to the shared dataflow, and none was supplied"),
1158 Span::NONE,
1159 )
1160}
1161
1162// -------------------------------------------------------------------------------------------
1163// The shared dataflow (§5.3)
1164// -------------------------------------------------------------------------------------------
1165
1166/// What the shared dataflow did in advancing from one state version to the next.
1167///
1168/// A subscriber renders when it is woken, not when the fold moves, so it can be several versions
1169/// behind by the time it looks. Its per-session operators need every change since *its* last
1170/// render, not the latest one — an entry withdrawn at version 8 and never mentioned again would
1171/// otherwise survive in a subscriber that last rendered at version 7 and next renders at 9.
1172///
1173/// A rebuilt operator's changes are deliberately **not** kept: a consumer downstream of a rebuild
1174/// re-reads the whole arrangement instead of applying changes, so storing them would retain a copy
1175/// of the collection per remembered version for nothing.
1176struct Step {
1177 from: u64,
1178 to: u64,
1179 changed: BTreeSet<OpId>,
1180 rebuilt: BTreeSet<OpId>,
1181 changes: BTreeMap<OpId, Arc<[Change]>>,
1182}
1183
1184/// A reader's frontier before it has rendered anything.
1185///
1186/// It constrains nothing: a reader with no arrangements rebuilds from the current ones whatever
1187/// history is kept, so treating it as a frontier of 0 would retain the maximum for the one reader
1188/// that cannot use a single step of it. `u64::MAX` falls out of the minimum instead of having to be
1189/// filtered out of it.
1190const UNRENDERED: u64 = u64::MAX;
1191
1192type ReaderId = u64;
1193
1194struct SharedInner {
1195 engine: Engine,
1196 version: u64,
1197 /// Whether the shared prefix has been computed at all.
1198 ///
1199 /// Separate from `version` because a freshly recovered application is at version 0 with a real
1200 /// accumulator behind it — an empty log is a state, not the absence of one — so "already at the
1201 /// version you asked for" and "never advanced" are different facts and only one of them means
1202 /// there is nothing to do.
1203 started: bool,
1204 /// Oldest first, and contiguous: `history[k].to == history[k + 1].from`.
1205 history: VecDeque<Step>,
1206 /// Every attached subscriber, and how far behind it is.
1207 ///
1208 /// The set is what decides whether the arrangements are worth holding at all; the frontiers are
1209 /// what decide how much of the change history is. Both are read only under this lock, and the
1210 /// frontiers are *written* outside it — see [`Attachment`].
1211 readers: BTreeMap<ReaderId, Arc<AtomicU64>>,
1212}
1213
1214impl SharedInner {
1215 /// The oldest version any attached reader can still ask for changes since.
1216 ///
1217 /// A step whose `to` is at or below this is retained by nobody: every reader has already
1218 /// rendered past it. With no readers at all it is the current version, so everything is
1219 /// droppable — which is the same fact the release path acts on more thoroughly.
1220 fn floor(&self) -> u64 {
1221 self.readers
1222 .values()
1223 .map(|f| f.load(Ordering::Relaxed))
1224 .min()
1225 .unwrap_or(UNRENDERED)
1226 .min(self.version)
1227 }
1228
1229 /// Drop the steps no attached reader can still ask for, and cap what is left.
1230 ///
1231 /// Two bounds, and they are different kinds of thing. The floor is a *fact*: a step below it is
1232 /// retained for nobody. The depth is a *policy*: past it we would rather a very late subscriber
1233 /// rebuild than hold change history for it indefinitely.
1234 fn compact(&mut self, depth: usize) {
1235 let floor = self.floor();
1236 while self.history.front().is_some_and(|s| s.to <= floor) {
1237 self.history.pop_front();
1238 }
1239 while self.history.len() > depth {
1240 self.history.pop_front();
1241 }
1242 }
1243}
1244
1245/// How long a shared dataflow keeps what a subscriber might still ask for.
1246///
1247/// [`docs/26-arrangement-sharing-report.md`](../../../../../docs/26-arrangement-sharing-report.md)
1248/// §26.9 recorded both of these as constants that should have been policies: the history was 64
1249/// versions "because a subscriber further behind than that is not the bottleneck", and the
1250/// arrangements were never dropped at all.
1251#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1252pub struct Retention {
1253 /// The **ceiling** on retained change history, in versions. The reader frontiers are the floor,
1254 /// and they are usually far lower — this bounds what one subscriber that has stopped rendering
1255 /// can pin.
1256 pub depth: usize,
1257 /// Whether to give up the arrangements when the last subscriber goes.
1258 ///
1259 /// On, the process holds nothing between fanouts and the next subscriber pays a cold start. Off,
1260 /// they stay warm for a reconnection that may not come. The trade is a real one and it belongs
1261 /// to a deployment rather than to this file, which is why it is here and not a `const`.
1262 pub release_when_idle: bool,
1263}
1264
1265/// How many versions of change history a shared dataflow keeps **at most**.
1266///
1267/// The cost is one `Change` per entry that moved per remembered version — a delta, not a
1268/// collection, because a rebuilt operator's changes are not kept. The benefit is that a subscriber
1269/// this many events behind still updates by delta rather than rebuilding. 64 is well past the point
1270/// where a subscriber that far behind is the bottleneck.
1271///
1272/// It is a ceiling rather than the retention itself: what is actually kept is bounded below by the
1273/// oldest reader's frontier, which on a fanout of subscribers that all render is one step.
1274const HISTORY: usize = 64;
1275
1276impl Default for Retention {
1277 fn default() -> Retention {
1278 Retention {
1279 depth: HISTORY,
1280 release_when_idle: true,
1281 }
1282 }
1283}
1284
1285/// The operators of a plan that do not read the session, arranged **once** for every subscriber.
1286///
1287/// "Do not read the session" is the sentence §5.3 uses and it is one atom short: what this holds is
1288/// the operators that are a function of the accumulator alone, so everything downstream of
1289/// [`crate::plan::Op::Presence`] is excluded too. The reason is this type's `version` — it is the
1290/// log's `seq`, and a roster moves when `seq` does not
1291/// ([`docs/96`](../../../../../docs/96-presence-report.md) §96.5).
1292///
1293/// [`docs/05-tier-lowering.md`](../../../../../docs/05-tier-lowering.md) §5.3:
1294///
1295/// > a thousand connected users of `todos.map(filter_by(session.user))` must compile to *one*
1296/// > shared dataflow whose final per-session operators (filter, project, diff) run per subscriber
1297///
1298/// [`crate::plan::Plan`] has said which nodes those are since the plan existed — `per_session` is
1299/// false for exactly the operators reachable from the accumulator without passing through the
1300/// session. What was missing was somewhere for them to live that is not one subscriber's engine.
1301///
1302/// # The three choices §24.7 said this design had in it
1303///
1304/// 1. **Who advances it.** Not the sequencer: that would put view maintenance on the write path and
1305/// do it for a state nobody is looking at. The *first subscriber to render at a new version*
1306/// advances it, under a write lock, and every subscriber that renders at that version afterwards
1307/// finds it done. So the work happens once per version, is paid by a renderer that was about to
1308/// do it anyway, and does not happen at all when nobody is subscribed.
1309/// 2. **What a subscriber holds while it renders.** A read lock, for the whole of its own render.
1310/// Readers do not block readers, so a thousand subscribers render concurrently; the only writer
1311/// is the advance, which is `O(δ)`. The alternative — publishing an immutable snapshot per
1312/// version — has to copy any arrangement that moved, which is the `O(n)` this engine exists to
1313/// remove.
1314/// 3. **What happens to a subscriber that fell behind.** It replays the changes it missed, from a
1315/// bounded history of recent versions (`Step`). Beyond that history it rebuilds — correct at
1316/// any lag, because a rebuild reads the current arrangement whole and a rebuild is already
1317/// contagious downstream (`Cell::rebuilt`).
1318///
1319/// # What is still not shared
1320///
1321/// The *page* is per-session in every corpus program, so what is shared is the prefix below the
1322/// session, not the render. `24-feed.beck` is the case where that prefix is most of the plan and
1323/// the sketch is the case where it is least; `docs/26` has the table.
1324///
1325/// # The lifecycle: who keeps this alive, and for how long
1326///
1327/// The three choices above say how the dataflow is *maintained*. They are silent about when it
1328/// stops being worth maintaining, which [`26`](../../../../../docs/26-arrangement-sharing-report.md)
1329/// §26.9 recorded as two loose ends — arrangements that are never released, and a change history
1330/// that is a constant rather than a policy. Both are the same missing rule, and it is the
1331/// reader-frontier discipline of differential dataflow's shared arrangements: a reader set, a
1332/// frontier per reader, history compactable up to the minimum frontier, and the trace droppable
1333/// when the reader set is empty.
1334///
1335/// So a subscriber engine is **counted**. [`SharedDataflow::subscriber`] enters it in the reader
1336/// set and its `Drop` removes it; each render publishes the version it reached; an advance
1337/// compacts to the oldest frontier and, when the last reader goes, the arrangements are released
1338/// outright. What the process holds is then a function of who is connected rather than of what has
1339/// ever connected.
1340pub struct SharedDataflow {
1341 inner: RwLock<SharedInner>,
1342 retention: Retention,
1343 /// How many times the shared prefix has actually been advanced.
1344 ///
1345 /// The metric the whole design turns on: a thousand subscribers rendering at one version must
1346 /// advance it *once*, and a counter is how that is a test rather than a claim.
1347 advances: AtomicU64,
1348 /// How many times the arrangements have been given up because nobody was reading them.
1349 releases: AtomicU64,
1350 next_reader: AtomicU64,
1351}
1352
1353impl SharedDataflow {
1354 pub fn new(prepared: Arc<Prepared>) -> SharedDataflow {
1355 SharedDataflow::with_retention(prepared, Retention::default())
1356 }
1357
1358 pub fn with_retention(prepared: Arc<Prepared>, retention: Retention) -> SharedDataflow {
1359 let owns: Arc<[bool]> = prepared.plan.nodes.iter().map(|n| !n.per_session).collect();
1360 SharedDataflow {
1361 inner: RwLock::new(SharedInner {
1362 engine: Engine::for_nodes(prepared, owns),
1363 version: 0,
1364 started: false,
1365 history: VecDeque::new(),
1366 readers: BTreeMap::new(),
1367 }),
1368 retention,
1369 advances: AtomicU64::new(0),
1370 releases: AtomicU64::new(0),
1371 next_reader: AtomicU64::new(0),
1372 }
1373 }
1374
1375 pub fn retention(&self) -> Retention {
1376 self.retention
1377 }
1378
1379 /// A subscriber's engine over the same plan: the per-session operators, and nothing else.
1380 ///
1381 /// The engine is a **reader** of this dataflow for exactly as long as it lives. It takes an
1382 /// `Arc<Self>` because that is what makes the second half true: the engine has to be able to
1383 /// say it has gone, and a subscription ends by dropping its engine rather than by calling
1384 /// anything.
1385 pub fn subscriber(self: &Arc<Self>) -> Engine {
1386 let mut inner = self.write();
1387 let mut engine = Engine::subscriber(inner.engine.prepared.clone());
1388 let id = self.next_reader.fetch_add(1, Ordering::Relaxed);
1389 let frontier = Arc::new(AtomicU64::new(UNRENDERED));
1390 inner.readers.insert(id, frontier.clone());
1391 engine.attached = Some(Attachment {
1392 shared: self.clone(),
1393 id,
1394 frontier,
1395 });
1396 engine
1397 }
1398
1399 /// A reader of the shared arrangements that renders no page: [`crate::read`]'s SQL client.
1400 ///
1401 /// It is a member of the same reader set as a subscription, and that is the design rather than
1402 /// an implementation convenience. A SQL client holding a connection is a reason to keep the
1403 /// arrangements — it is going to ask again — and a SQL client that has gone is not, which is
1404 /// exactly what the reader set already decides for subscribers
1405 /// ([`51`](../../../../../docs/51-arrangement-lifecycle-report.md)). The alternative, reading the
1406 /// arrangements without joining the set, has a release racing every query.
1407 ///
1408 /// Its frontier stays at the unrendered one: a reader that never applies a delta cannot use the
1409 /// change history, so pinning any of it for this reader would retain history nobody reads.
1410 pub fn reader(self: &Arc<Self>) -> Reader {
1411 let mut inner = self.write();
1412 let id = self.next_reader.fetch_add(1, Ordering::Relaxed);
1413 let frontier = Arc::new(AtomicU64::new(UNRENDERED));
1414 inner.readers.insert(id, frontier);
1415 Reader {
1416 shared: self.clone(),
1417 id,
1418 }
1419 }
1420
1421 /// A subscriber has gone. Drop what only it could still have asked for.
1422 ///
1423 /// Called from [`Engine`]'s `Drop`, so it must not be reachable while this thread holds either
1424 /// guard — it is not: the engine a `SharedInner` owns is built by `Engine::for_nodes` and is
1425 /// never a reader of anything.
1426 fn detach(&self, id: ReaderId) {
1427 let mut inner = self.write();
1428 inner.readers.remove(&id);
1429 if inner.readers.is_empty() && self.retention.release_when_idle {
1430 self.release(&mut inner);
1431 } else {
1432 let depth = self.retention.depth;
1433 inner.compact(depth);
1434 }
1435 }
1436
1437 /// Give up the arrangements. Nobody is reading them and the accumulator they came from remains,
1438 /// so this costs the next subscriber a cold start and costs correctness nothing.
1439 ///
1440 /// Deliberately the same reset the error path takes, and for the same reason: what is left has
1441 /// to be a dataflow that says it has never been advanced, rather than one that has been
1442 /// advanced and then hollowed out.
1443 fn release(&self, inner: &mut SharedInner) {
1444 if !inner.started {
1445 return;
1446 }
1447 inner.engine.reset();
1448 inner.history.clear();
1449 inner.started = false;
1450 inner.version = 0;
1451 self.releases.fetch_add(1, Ordering::Relaxed);
1452 }
1453
1454 /// Render one subscriber's page, maintaining the shared prefix once for all of them.
1455 ///
1456 /// `version` identifies the state: two calls with the same `version` must pass the same
1457 /// `state`, because the second is served from what the first computed. Returns the page and the
1458 /// version it actually reflects, which may be **newer** than the one asked for — another
1459 /// subscriber may have advanced the shared side in between, and rendering the newer state is
1460 /// correct where rendering the older one would mean unwinding an arrangement.
1461 ///
1462 /// That returned version is not a courtesy. A patch frame is labelled with a `seq` and a
1463 /// resuming client is served the difference from it (§4.3), so a frame labelled with a state
1464 /// the page does not reflect is a wrong DOM after the next reconnect.
1465 pub fn render(
1466 &self,
1467 engine: &mut Engine,
1468 state: &Value,
1469 version: u64,
1470 session: &Value,
1471 presence: &Value,
1472 ) -> Result<(Value, u64), ExecError> {
1473 self.advance(state, version)?;
1474 let inner = self.read();
1475 let up = Upstream::new(&inner, engine.seen);
1476 let page = engine.render_from(Some(up), state, session, presence)?;
1477 engine.seen = inner.version;
1478 // Published outside this dataflow's write lock, and this is the whole reason a frontier is
1479 // an atomic: a render must not serialise against the other renders it is concurrent with.
1480 // Publishing it *after* the render is what makes it safe to compact against — a reader
1481 // whose frontier still reads older than it is retains more history than it needs, and a
1482 // reader that retains too little is the only way this could be wrong.
1483 if let Some(a) = &engine.attached {
1484 a.frontier.store(inner.version, Ordering::Relaxed);
1485 }
1486 Ok((page, inner.version))
1487 }
1488
1489 /// Bring the shared prefix up to `version`, if some other subscriber has not already.
1490 fn advance(&self, state: &Value, version: u64) -> Result<(), ExecError> {
1491 {
1492 let inner = self.read();
1493 if inner.started && inner.version >= version {
1494 return Ok(());
1495 }
1496 }
1497 let mut inner = self.write();
1498 // Checked again under the write lock: between the read above and here, another subscriber
1499 // may have done exactly this.
1500 if inner.started && inner.version >= version {
1501 return Ok(());
1502 }
1503 let from = inner.version;
1504 if let Err(e) = inner.engine.advance(state) {
1505 // The engine has already discarded its arrangements. The history describes a dataflow
1506 // that no longer exists, so it goes too, and every subscriber rebuilds.
1507 inner.history.clear();
1508 inner.started = false;
1509 inner.version = 0;
1510 return Err(e);
1511 }
1512 inner.started = true;
1513 self.advances.fetch_add(1, Ordering::Relaxed);
1514 let step = inner.engine.step(from, version);
1515 inner.history.push_back(step);
1516 inner.version = version;
1517 // Under the same write lock as the advance, so nothing is compacted away between a
1518 // subscriber deciding what it needs and reading it: a render holds the read lock for its
1519 // whole duration, and this cannot run until every render in flight has finished.
1520 let depth = self.retention.depth;
1521 inner.compact(depth);
1522 Ok(())
1523 }
1524
1525 /// The version the shared prefix currently reflects.
1526 pub fn version(&self) -> u64 {
1527 self.read().version
1528 }
1529
1530 /// How many times the shared prefix has been advanced since the process started.
1531 ///
1532 /// §5.3's claim is that a thousand subscribers of one view share one dataflow. This is the
1533 /// number that says so: it counts advances, not renders, so it stays flat as subscribers are
1534 /// added and moves only when the fold does.
1535 pub fn advances(&self) -> u64 {
1536 self.advances.load(Ordering::Relaxed)
1537 }
1538
1539 /// How many times the arrangements have been given up because nobody was reading them.
1540 ///
1541 /// The counterpart to [`SharedDataflow::advances`], and the number a deployment weighs against
1542 /// it: every release is a cold start charged to whichever subscriber reconnects first.
1543 pub fn releases(&self) -> u64 {
1544 self.releases.load(Ordering::Relaxed)
1545 }
1546
1547 /// How many subscribers are attached right now.
1548 pub fn readers(&self) -> usize {
1549 self.read().readers.len()
1550 }
1551
1552 /// How many versions of change history are being kept.
1553 ///
1554 /// Bounded above by [`Retention::depth`] and below by the oldest attached reader's frontier, so
1555 /// on a fanout whose subscribers all render at every version it is 1 rather than 64. This is
1556 /// the number that says the frontier discipline is doing something.
1557 pub fn retained(&self) -> usize {
1558 self.read().history.len()
1559 }
1560
1561 /// Entries across every shared arrangement — held once, however many subscribers there are.
1562 pub fn arranged(&self) -> u64 {
1563 self.read().engine.arranged()
1564 }
1565
1566 /// What the shared prefix retains beyond the accumulator — once, for every subscriber.
1567 pub fn footprint(&self, base: &Value) -> Footprint {
1568 self.read().engine.footprint(base)
1569 }
1570
1571 pub fn work(&self) -> Work {
1572 self.read().engine.work()
1573 }
1574
1575 fn read(&self) -> std::sync::RwLockReadGuard<'_, SharedInner> {
1576 self.inner
1577 .read()
1578 .unwrap_or_else(std::sync::PoisonError::into_inner)
1579 }
1580
1581 fn write(&self) -> std::sync::RwLockWriteGuard<'_, SharedInner> {
1582 self.inner
1583 .write()
1584 .unwrap_or_else(std::sync::PoisonError::into_inner)
1585 }
1586}
1587
1588/// A reader of a [`SharedDataflow`]'s arrangements that renders nothing.
1589///
1590/// The read model's half of §5.3's cut: the operators that do not read the session are exactly the
1591/// ones a client with no session can be shown ([`crate::read`]). Holding one keeps the arrangements
1592/// from being released; dropping it is how a SQL connection ends.
1593pub struct Reader {
1594 shared: Arc<SharedDataflow>,
1595 id: ReaderId,
1596}
1597
1598impl Drop for Reader {
1599 fn drop(&mut self) {
1600 self.shared.detach(self.id);
1601 }
1602}
1603
1604impl Reader {
1605 /// One shared operator's output, as the rows it stands for, at `version`.
1606 ///
1607 /// Advances the shared prefix first, by the same path a rendering subscriber takes — so a query
1608 /// issued after an ack sees that ack's event, and a query issued when nothing is subscribed
1609 /// pays for the advance nobody else has paid for. That is the read model's whole freshness
1610 /// story: there is no projection to lag behind.
1611 ///
1612 /// An arrangement answers its entries in key order, which is the order the plan gives it and
1613 /// therefore the order the page renders in. A value answers itself, once.
1614 pub fn read(&self, state: &Value, version: u64, id: OpId) -> Result<Vec<Value>, ExecError> {
1615 self.shared.advance(state, version)?;
1616 let inner = self.shared.read();
1617 if !inner.engine.owns.get(id).copied().unwrap_or(false) {
1618 return Err(ExecError::new(
1619 format!("operator {id} is not part of the shared dataflow"),
1620 Span::NONE,
1621 ));
1622 }
1623 Ok(match &inner.engine.cells[id].out {
1624 Out::Arr(a) => a.entries.values().cloned().collect(),
1625 Out::Val(v) => vec![v.clone()],
1626 })
1627 }
1628}
1629
1630impl Engine {
1631 /// What this engine's owned operators did in one advance, as a replayable step.
1632 fn step(&self, from: u64, to: u64) -> Step {
1633 let mut changed = BTreeSet::new();
1634 let mut rebuilt = BTreeSet::new();
1635 let mut changes = BTreeMap::new();
1636 for (id, cell) in self.cells.iter().enumerate() {
1637 if !self.owns[id] {
1638 continue;
1639 }
1640 if cell.changed {
1641 changed.insert(id);
1642 }
1643 if cell.rebuilt {
1644 rebuilt.insert(id);
1645 } else if !cell.changes.is_empty() {
1646 // From the slice, one copy: this runs under the shared dataflow's write lock.
1647 changes.insert(id, Arc::<[Change]>::from(cell.changes.as_slice()));
1648 }
1649 }
1650 Step {
1651 from,
1652 to,
1653 changed,
1654 rebuilt,
1655 changes,
1656 }
1657 }
1658}
1659
1660/// One subscriber's window onto the shared dataflow: its arrangements now, and everything that
1661/// moved since this subscriber last looked.
1662#[derive(Clone, Copy)]
1663struct Upstream<'a> {
1664 inner: &'a SharedInner,
1665 since: u64,
1666 /// Whether the history still covers `since`. When it does not, every upstream node reads as
1667 /// changed *and* rebuilt, so the subscriber re-reads the arrangements whole — slow, and right.
1668 resolvable: bool,
1669}
1670
1671impl<'a> Upstream<'a> {
1672 fn new(inner: &'a SharedInner, since: u64) -> Upstream<'a> {
1673 let resolvable = since == inner.version
1674 || inner
1675 .history
1676 .iter()
1677 .find(|s| s.to > since)
1678 .is_some_and(|s| s.from == since);
1679 Upstream {
1680 inner,
1681 since,
1682 resolvable,
1683 }
1684 }
1685
1686 fn out(&self, id: OpId) -> &'a Out {
1687 &self.inner.engine.cells[id].out
1688 }
1689
1690 fn window(&self) -> impl Iterator<Item = &'a Step> {
1691 let since = self.since;
1692 self.inner.history.iter().filter(move |s| s.to > since)
1693 }
1694
1695 fn changed(&self, id: OpId) -> bool {
1696 !self.resolvable || self.window().any(|s| s.changed.contains(&id))
1697 }
1698
1699 fn rebuilt(&self, id: OpId) -> bool {
1700 !self.resolvable || self.window().any(|s| s.rebuilt.contains(&id))
1701 }
1702
1703 /// Everything that moved at this node since `since`, in the order it moved.
1704 ///
1705 /// Concatenation rather than coalescing: a consumer applies changes in order, so a key that
1706 /// moved twice is applied twice and lands where the second one put it. Coalescing would save a
1707 /// consumer one application per repeat and cost a pass over the window; the window is a handful
1708 /// of deltas.
1709 fn changes(&self, id: OpId) -> Vec<Change> {
1710 self.window()
1711 .filter_map(|s| s.changes.get(&id))
1712 .flat_map(|c| c.iter().cloned())
1713 .collect()
1714 }
1715}
1716
1717/// One element of a flattened collection: the outer key, then the position within it.
1718fn inner_key(outer: &Key, i: usize) -> Key {
1719 let mut k: Vec<Value> = outer.to_vec();
1720 k.push(Value::Int(i as i64));
1721 Arc::from(k)
1722}
1723
1724/// A list, as an arrangement keyed by position.
1725fn list_entries(v: &Value) -> Vec<(Key, Value)> {
1726 match v {
1727 Value::List(xs) => xs
1728 .iter()
1729 .enumerate()
1730 .map(|(i, x)| (Arc::from(vec![Value::Int(i as i64)]), x.clone()))
1731 .collect(),
1732 Value::Map(m) => m
1733 .iter()
1734 .map(|(k, v)| (Arc::from(vec![k.clone()]), v.clone()))
1735 .collect(),
1736 _ => Vec::new(),
1737 }
1738}
1739
1740/// A conservative "did this value move" test: `true` only when it certainly did not.
1741///
1742/// Structural equality would be `O(size)`, and doing it once per operator per event would put back
1743/// the cost this engine removes. Collections and rendered trees therefore compare by *pointer*: two
1744/// equal-but-separately-built lists answer `false`, which costs one recompute that the old runtime
1745/// performed unconditionally. Records compare field by field, because that is how a program's own
1746/// small values — a `Summary`, a `Tally` — are built, and the whole point of a plan is that an
1747/// event which does not move the summary does not re-render the page below it.
1748// Not `pub`: this is a *conservative* changed-test — `Arc::ptr_eq` for lists and Html — and a
1749// caller reading it as equality would be misled.
1750fn same(a: &Value, b: &Value) -> bool {
1751 match (a, b) {
1752 (Value::Unit, Value::Unit) => true,
1753 (Value::Bool(x), Value::Bool(y)) => x == y,
1754 (Value::Int(x), Value::Int(y)) => x == y,
1755 (Value::Float(x), Value::Float(y)) => x == y,
1756 (Value::Str(x), Value::Str(y)) => x == y,
1757 (Value::List(x), Value::List(y)) => Arc::ptr_eq(x, y),
1758 (Value::Map(x), Value::Map(y)) => x.same_root(y),
1759 (Value::Html(x), Value::Html(y)) => Arc::ptr_eq(x, y),
1760 (Value::Attr(x), Value::Attr(y)) => Arc::ptr_eq(x, y),
1761 (Value::Data(a), Value::Data(b)) => {
1762 // One pointer now compares the whole record, where three fields used to be compared
1763 // one at a time — the shape `Value::Data(Arc<Record>)` was chosen for.
1764 if Arc::ptr_eq(a, b) {
1765 return true;
1766 }
1767 let (t1, v1, f1) = (&a.ty, &a.variant, &a.fields);
1768 let (t2, v2, f2) = (&b.ty, &b.variant, &b.fields);
1769 t1 == t2
1770 && v1 == v2
1771 && f1.len() == f2.len()
1772 && f1
1773 .iter()
1774 .zip(f2.iter())
1775 .all(|((n1, x), (n2, y))| n1 == n2 && same(x, y))
1776 }
1777 _ => false,
1778 }
1779}
1780
1781// -------------------------------------------------------------------------------------------
1782// Footprint
1783// -------------------------------------------------------------------------------------------
1784
1785/// A deterministic byte estimate for the memory a subscription's engine retains.
1786///
1787/// `docs/05-tier-lowering.md` §5.3 names per-session memory as one of three metrics to export,
1788/// and Phase 0's kill gate is written in kilobytes per idle session
1789/// (`docs/18-phase-0-report.md` §18.3). An engine per subscription is a memory-for-time trade,
1790/// so the number has to exist.
1791///
1792/// It is computed rather than sampled. A resident-set reading moves with the allocator's arena and
1793/// swung by 2× between runs of the same measurement; a counting allocator would be exact and needs
1794/// `unsafe`, which this workspace forbids. So this walks what is actually retained and adds up
1795/// `size_of` plus the bytes behind each allocation, **counting shared structure once**: a `Todo` an
1796/// arrangement holds is the same `Arc` the accumulator holds, and charging a subscription for it
1797/// would be the difference between "a handle per row" and "a row per row".
1798///
1799/// What it excludes, and therefore under-reports: allocator overhead per allocation, which for many
1800/// small allocations is substantial. It is a floor on the true cost, not a ceiling.
1801#[derive(Clone, Copy, Debug, Default)]
1802pub struct Footprint {
1803 /// Bytes retained by this engine's cells, arrangements and keys.
1804 pub bytes: u64,
1805 /// Of those, the ones in arrangements that do not read the session — what §5.3 says a thousand
1806 /// subscribers should hold once between them, and this engine holds once each.
1807 pub shared_bytes: u64,
1808 pub entries: u64,
1809}
1810
1811impl Engine {
1812 /// What this subscription retains **beyond** the accumulator it renders from.
1813 ///
1814 /// `base` is that accumulator, and walking it first is not a detail: an arrangement over
1815 /// `map_values(s.todos)` holds the *same* `Todo` records the fold holds, by `Arc`, so charging
1816 /// a subscription for them would report a row per row where the truth is a handle per row. What
1817 /// remains after the exclusion is what a thousand subscribers actually multiply.
1818 ///
1819 /// See [`Footprint`] for what the number does and does not include.
1820 pub fn footprint(&self, base: &Value) -> Footprint {
1821 let mut seen = BTreeSet::new();
1822 value_bytes(base, &mut seen);
1823 let mut acc = Footprint::default();
1824 self.footprint_into(&mut seen, &mut acc);
1825 acc
1826 }
1827
1828 /// The same walk, against an exclusion set some other engine has already contributed to.
1829 ///
1830 /// Separate from [`Engine::footprint`] because summing per-engine footprints across a fanout
1831 /// over-reports, and over-reports **exactly the thing this work is about**: with a shared
1832 /// dataflow, two subscribers' pages hold the same `ul` by `Arc`, and charging both of them for
1833 /// it would report the sharing as costing what it saves.
1834 fn footprint_into(&self, seen: &mut BTreeSet<usize>, acc: &mut Footprint) {
1835 let (mut bytes, mut shared_bytes, mut entries) = (0u64, 0u64, 0u64);
1836 for (i, cell) in self.cells.iter().enumerate() {
1837 // An operator this engine does not own costs it nothing: the shared dataflow holds it,
1838 // and `SharedDataflow::footprint` is where it is charged — once, not once per
1839 // subscriber, which is the whole point of the split.
1840 if !self.owns[i] {
1841 continue;
1842 }
1843 let mut here = std::mem::size_of::<Cell>() as u64;
1844 match &cell.out {
1845 Out::Val(v) => here += value_bytes(v, seen),
1846 Out::Arr(a) => {
1847 entries += a.entries.len() as u64;
1848 for (k, v) in &a.entries {
1849 // A `BTreeMap` node holds up to 11 entries plus links; charged per entry as
1850 // the pair plus a share of the node.
1851 here +=
1852 (std::mem::size_of::<Key>() + std::mem::size_of::<Value>() + 24) as u64;
1853 here += k.len() as u64 * std::mem::size_of::<Value>() as u64;
1854 here += value_bytes(v, seen);
1855 }
1856 if let Some(listed) = a.listed.get() {
1857 here += value_bytes(listed, seen);
1858 }
1859 }
1860 }
1861 for (k, v) in &cell.positions {
1862 here += (k.len() + v.len()) as u64 * std::mem::size_of::<Value>() as u64 + 24;
1863 }
1864 bytes += here;
1865 if !self.prepared.plan.nodes[i].per_session {
1866 shared_bytes += here;
1867 }
1868 }
1869 acc.bytes += bytes;
1870 acc.shared_bytes += shared_bytes;
1871 acc.entries += entries;
1872 }
1873}
1874
1875/// What a whole fanout retains: the accumulator once, the shared dataflow once, and each
1876/// subscriber's own operators — with every shared allocation counted **exactly once across all of
1877/// them**.
1878///
1879/// Summing [`Engine::footprint`] over the subscribers is the wrong number once there is a shared
1880/// dataflow, and wrong in the direction that flatters nothing: two subscribers' pages hold the same
1881/// `ul` by `Arc`, so charging both would report sharing as costing what it saves. This is the
1882/// number a fanout estimate should be built from, and `docs/26` is where it is.
1883pub fn fanout_footprint(
1884 base: &Value,
1885 shared: Option<&SharedDataflow>,
1886 engines: &[&Engine],
1887) -> Footprint {
1888 let mut seen = BTreeSet::new();
1889 value_bytes(base, &mut seen);
1890 let mut acc = Footprint::default();
1891 if let Some(shared) = shared {
1892 shared.read().engine.footprint_into(&mut seen, &mut acc);
1893 }
1894 for engine in engines {
1895 engine.footprint_into(&mut seen, &mut acc);
1896 }
1897 acc
1898}
1899
1900/// Bytes behind a value, counting each shared allocation once.
1901fn value_bytes(v: &Value, seen: &mut std::collections::BTreeSet<usize>) -> u64 {
1902 let mut fresh = |p: usize| seen.insert(p);
1903 match v {
1904 Value::Unit | Value::Bool(_) | Value::Int(_) | Value::Float(_) => 0,
1905 Value::Str(s) => {
1906 if fresh(s.as_ptr() as usize) {
1907 s.len() as u64
1908 } else {
1909 0
1910 }
1911 }
1912 Value::List(xs) => {
1913 if !fresh(Arc::as_ptr(xs) as usize) {
1914 return 0;
1915 }
1916 let mut n = (xs.len() * std::mem::size_of::<Value>()) as u64;
1917 for x in xs.iter() {
1918 n += value_bytes(x, seen);
1919 }
1920 n
1921 }
1922 Value::Map(m) => {
1923 let mut n = 0;
1924 for (k, v) in m.iter() {
1925 // A tree node: key, value, size and two links.
1926 n += (2 * std::mem::size_of::<Value>() + 24) as u64;
1927 n += value_bytes(k, seen) + value_bytes(v, seen);
1928 }
1929 n
1930 }
1931 Value::Data(d) => {
1932 if !fresh(Arc::as_ptr(d) as usize) {
1933 return 0;
1934 }
1935 let mut n = (d.fields.len() * (std::mem::size_of::<Value>() + 16 + 24)) as u64;
1936 for f in d.fields.values() {
1937 n += value_bytes(f, seen);
1938 }
1939 n
1940 }
1941 Value::Html(h) => {
1942 if !fresh(Arc::as_ptr(h) as usize) {
1943 return 0;
1944 }
1945 html_bytes(h)
1946 }
1947 Value::Attr(a) => {
1948 if fresh(Arc::as_ptr(a) as usize) {
1949 std::mem::size_of::<crate::core::AttrValue>() as u64
1950 } else {
1951 0
1952 }
1953 }
1954 Value::Closure(_) => 0,
1955 }
1956}
1957
1958/// The same estimate for a rendered page, so "what the engine added" has a baseline.
1959pub fn html_footprint(h: &crate::html::Html) -> u64 {
1960 html_bytes(h)
1961}
1962
1963fn html_bytes(h: &crate::html::Html) -> u64 {
1964 use crate::html::Html;
1965 match h {
1966 Html::Text { text, .. } => std::mem::size_of::<Html>() as u64 + text.len() as u64,
1967 Html::Element {
1968 tag,
1969 attrs,
1970 key,
1971 children,
1972 ..
1973 } => {
1974 let mut n = std::mem::size_of::<Html>() as u64 + tag.len() as u64;
1975 n += key.as_ref().map(|k| k.len()).unwrap_or(0) as u64;
1976 for (a, b) in attrs {
1977 n += (a.len() + b.len() + 48) as u64;
1978 }
1979 n += (children.len() * std::mem::size_of::<Html>()) as u64;
1980 for c in children {
1981 n += html_bytes(c);
1982 }
1983 n
1984 }
1985 }
1986}