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}