Skip to main content

sim_incremental_core/
snapshot.rs

1//! Bounded graph snapshot records.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6    BudgetKind, FingerprintValue, IncrementalEngine, IncrementalError, Observation,
7    ObservationKind, QueryResult, Revision, SnapshotBudgets, SnapshotError, state::Node,
8};
9
10/// A deterministic snapshot of memoized graph state.
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct GraphSnapshot<K, V> {
13    /// Snapshot nodes in stable key order.
14    pub nodes: Vec<SnapshotNode<K, V>>,
15}
16
17impl<K, V> GraphSnapshot<K, V> {
18    /// Creates a graph snapshot from already-ordered nodes.
19    #[must_use]
20    pub fn new(nodes: Vec<SnapshotNode<K, V>>) -> Self {
21        Self { nodes }
22    }
23}
24
25/// One memo node inside a graph snapshot.
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct SnapshotNode<K, V> {
28    /// The query key.
29    pub key: K,
30    /// The memo revision.
31    pub revision: Revision,
32    /// Whether the memo needs verification before reuse.
33    pub dirty: bool,
34    /// The memoized value, when one exists.
35    pub value: Option<V>,
36    /// Dependency observations captured during the last execution, without
37    /// process-local cutoff fingerprints.
38    pub dependencies: Vec<SnapshotObservation<K>>,
39}
40
41/// Durable dependency observation with no process-local fingerprint authority.
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct SnapshotObservation<K> {
44    /// Observed dependency key.
45    pub key: K,
46    /// Kind of observation made.
47    pub kind: ObservationKind,
48    /// Revision observed at snapshot time.
49    pub revision: Revision,
50}
51
52/// Summary of a snapshot restore operation.
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54pub struct RestoreReport {
55    /// Number of nodes restored.
56    pub nodes: usize,
57    /// Number of nodes marked dirty to recover from partial or stale snapshot
58    /// contents.
59    pub recovered_dirty: usize,
60}
61
62impl<K, V> IncrementalEngine<K, V>
63where
64    K: Ord + Clone,
65    V: Clone + Eq + FingerprintValue,
66{
67    /// Exports memo graph state reachable from `roots`.
68    pub fn snapshot<I>(
69        &mut self,
70        roots: I,
71        budgets: SnapshotBudgets,
72    ) -> QueryResult<K, GraphSnapshot<K, V>>
73    where
74        I: IntoIterator<Item = K>,
75    {
76        let mut pending = roots.into_iter().collect::<BTreeSet<_>>();
77        let Some(root) = pending.iter().next().cloned() else {
78            return Ok(GraphSnapshot::new(Vec::new()));
79        };
80        let mut seen = BTreeSet::new();
81        let mut nodes = Vec::new();
82        let mut edges = 0_usize;
83        while let Some(key) = pending.iter().next().cloned() {
84            pending.remove(&key);
85            if !seen.insert(key.clone()) {
86                continue;
87            }
88            if nodes.len().saturating_add(1) > budgets.max_nodes {
89                let token = self.alloc_continuation(root);
90                return Err(IncrementalError::BudgetExceeded {
91                    kind: BudgetKind::Output,
92                    limit: budgets.max_nodes,
93                    consumed: nodes.len().saturating_add(1),
94                    continuation: Some(token),
95                });
96            }
97            let node = self
98                .nodes
99                .get(&key)
100                .ok_or_else(|| IncrementalError::UnknownQuery { key: key.clone() })?;
101            edges = edges.saturating_add(node.dependencies.len());
102            if edges > budgets.max_edges {
103                let token = self.alloc_continuation(root);
104                return Err(IncrementalError::BudgetExceeded {
105                    kind: BudgetKind::Output,
106                    limit: budgets.max_edges,
107                    consumed: edges,
108                    continuation: Some(token),
109                });
110            }
111            for observation in &node.dependencies {
112                if matches!(observation.kind(), ObservationKind::Read) {
113                    pending.insert(observation.key().clone());
114                }
115            }
116            nodes.push(SnapshotNode {
117                key,
118                revision: node.revision,
119                dirty: node.dirty,
120                value: node.value.clone(),
121                dependencies: node
122                    .dependencies
123                    .iter()
124                    .map(|observation| SnapshotObservation {
125                        key: observation.key().clone(),
126                        kind: observation.kind().clone(),
127                        revision: observation.revision(),
128                    })
129                    .collect(),
130            });
131        }
132        Ok(GraphSnapshot::new(nodes))
133    }
134
135    /// Restores memo graph state and rebuilds reverse dependency edges.
136    pub fn restore_snapshot(
137        &mut self,
138        snapshot: GraphSnapshot<K, V>,
139    ) -> Result<RestoreReport, SnapshotError<K>> {
140        let mut keys = BTreeSet::new();
141        for node in &snapshot.nodes {
142            if !keys.insert(node.key.clone()) {
143                return Err(SnapshotError::DuplicateNode {
144                    key: node.key.clone(),
145                });
146            }
147        }
148
149        self.nodes.clear();
150        self.reverse.clear();
151        let mut recovered_dirty = 0_usize;
152        let mut max_revision = self.next_revision.saturating_sub(1);
153        let restored_fingerprints = snapshot
154            .nodes
155            .iter()
156            .filter_map(|node| {
157                node.value
158                    .as_ref()
159                    .map(|value| (node.key.clone(), value.incremental_fingerprint()))
160            })
161            .collect::<BTreeMap<_, _>>();
162        for snapshot_node in snapshot.nodes {
163            max_revision = max_revision.max(snapshot_node.revision.get());
164            // Snapshot revisions are historical hints. Recheck every restored
165            // value through its registered query before it can be reused.
166            let dirty = true;
167            let recovered = !snapshot_node.dirty;
168            let fingerprint = snapshot_node
169                .value
170                .as_ref()
171                .map(FingerprintValue::incremental_fingerprint);
172            if recovered {
173                recovered_dirty += 1;
174            }
175            restore_external_revisions(&mut self.source_revisions, &snapshot_node);
176            let dependencies = snapshot_node
177                .dependencies
178                .into_iter()
179                .map(|observation| match observation.kind {
180                    ObservationKind::Read => Observation::read(
181                        observation.key.clone(),
182                        observation.revision,
183                        restored_fingerprints
184                            .get(&observation.key)
185                            .copied()
186                            .unwrap_or_else(|| crate::ValueFingerprint::new(0)),
187                    ),
188                    kind => Observation::new(observation.key, kind, observation.revision, None),
189                })
190                .collect();
191            self.nodes.insert(
192                snapshot_node.key,
193                Node {
194                    revision: snapshot_node.revision,
195                    dirty,
196                    value: snapshot_node.value,
197                    fingerprint,
198                    dependencies,
199                },
200            );
201        }
202        self.next_revision = self.next_revision.max(max_revision.saturating_add(1));
203        self.rebuild_reverse();
204        Ok(RestoreReport {
205            nodes: self.nodes.len(),
206            recovered_dirty,
207        })
208    }
209
210    fn rebuild_reverse(&mut self) {
211        self.reverse.clear();
212        for (key, node) in &self.nodes {
213            for observation in &node.dependencies {
214                self.reverse
215                    .entry(observation.key().clone())
216                    .or_default()
217                    .insert(key.clone());
218            }
219        }
220    }
221}
222
223fn restore_external_revisions<K, V>(
224    source_revisions: &mut BTreeMap<K, Revision>,
225    snapshot_node: &SnapshotNode<K, V>,
226) where
227    K: Ord + Clone,
228{
229    for observation in &snapshot_node.dependencies {
230        if matches!(observation.kind, ObservationKind::Read) {
231            continue;
232        }
233        source_revisions
234            .entry(observation.key.clone())
235            .and_modify(|revision| {
236                if observation.revision > *revision {
237                    *revision = observation.revision;
238                }
239            })
240            .or_insert(observation.revision);
241    }
242}