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