Skip to main content

nodedb_graph/csr/
compaction.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! CSR compaction: merges the mutable write buffer into dense arrays.
4
5use super::index::CsrIndex;
6use crate::GraphError;
7
8impl CsrIndex {
9    /// Merge the mutable buffer into the dense CSR arrays.
10    ///
11    /// Called during idle periods. Rebuilds the contiguous offset/target/label
12    /// (and weight) arrays from scratch (buffer + surviving dense edges).
13    /// The old arrays are dropped, freeing memory. O(E) where E = total edges.
14    ///
15    /// # Errors
16    ///
17    /// Returns [`GraphError::MemoryBudget`] if a memory governor is installed
18    /// and the dense-array allocation would exceed the `Graph` engine budget.
19    pub fn compact(&mut self) -> Result<(), GraphError> {
20        let n = self.id_to_node.len();
21        let mut new_out_edges: Vec<Vec<(u32, u32)>> = vec![Vec::new(); n];
22        let mut new_in_edges: Vec<Vec<(u32, u32)>> = vec![Vec::new(); n];
23        // Collection ids parallel to `new_out_edges` / `new_in_edges`.
24        let mut new_out_collections: Vec<Vec<u32>> = vec![Vec::new(); n];
25        let mut new_in_collections: Vec<Vec<u32>> = vec![Vec::new(); n];
26        let mut new_out_weights: Vec<Vec<f64>> = if self.has_weights {
27            vec![Vec::new(); n]
28        } else {
29            Vec::new()
30        };
31        let mut new_in_weights: Vec<Vec<f64>> = if self.has_weights {
32            vec![Vec::new(); n]
33        } else {
34            Vec::new()
35        };
36
37        // Collect surviving dense edges.
38        for node in 0..n {
39            let node_id = node as u32;
40            let idx = node_id as usize;
41
42            // Outbound dense edges.
43            if idx + 1 < self.out_offsets.len() {
44                let start = self.out_offsets[idx] as usize;
45                let end = self.out_offsets[idx + 1] as usize;
46                for i in start..end {
47                    let lid = self.out_labels[i];
48                    let dst = self.out_targets[i];
49                    let coll = self.out_collections.get(i).copied().unwrap_or(0);
50                    if !self.deleted_edges.contains(&(node_id, lid, dst, coll)) {
51                        new_out_edges[node].push((lid, dst));
52                        new_out_collections[node].push(coll);
53                        if self.has_weights {
54                            let w = self
55                                .out_weights
56                                .as_ref()
57                                .map_or(1.0, |ws| ws.get(i).copied().unwrap_or(1.0));
58                            new_out_weights[node].push(w);
59                        }
60                    }
61                }
62            }
63
64            // Inbound dense edges.
65            if idx + 1 < self.in_offsets.len() {
66                let start = self.in_offsets[idx] as usize;
67                let end = self.in_offsets[idx + 1] as usize;
68                for i in start..end {
69                    let lid = self.in_labels[i];
70                    let src = self.in_targets[i];
71                    let coll = self.in_collections.get(i).copied().unwrap_or(0);
72                    if !self.deleted_edges.contains(&(src, lid, node_id, coll)) {
73                        new_in_edges[node].push((lid, src));
74                        new_in_collections[node].push(coll);
75                        if self.has_weights {
76                            let w = self
77                                .in_weights
78                                .as_ref()
79                                .map_or(1.0, |ws| ws.get(i).copied().unwrap_or(1.0));
80                            new_in_weights[node].push(w);
81                        }
82                    }
83                }
84            }
85        }
86
87        // Merge buffer edges. Dedup is collection-aware: an identical
88        // `(label, dst)` under a different collection is a distinct edge and
89        // must survive alongside the existing one.
90        for node in 0..n {
91            for (buf_idx, &(lid, dst)) in self.buffer_out[node].iter().enumerate() {
92                let coll = self.buffer_out_collections[node]
93                    .get(buf_idx)
94                    .copied()
95                    .unwrap_or(0);
96                if !new_out_edges[node]
97                    .iter()
98                    .zip(new_out_collections[node].iter())
99                    .any(|(&(l, d), &c)| l == lid && d == dst && c == coll)
100                {
101                    new_out_edges[node].push((lid, dst));
102                    new_out_collections[node].push(coll);
103                    if self.has_weights {
104                        let w = self.buffer_out_weights[node]
105                            .get(buf_idx)
106                            .copied()
107                            .unwrap_or(1.0);
108                        new_out_weights[node].push(w);
109                    }
110                }
111            }
112            for (buf_idx, &(lid, src)) in self.buffer_in[node].iter().enumerate() {
113                let coll = self.buffer_in_collections[node]
114                    .get(buf_idx)
115                    .copied()
116                    .unwrap_or(0);
117                if !new_in_edges[node]
118                    .iter()
119                    .zip(new_in_collections[node].iter())
120                    .any(|(&(l, s), &c)| l == lid && s == src && c == coll)
121                {
122                    new_in_edges[node].push((lid, src));
123                    new_in_collections[node].push(coll);
124                    if self.has_weights {
125                        let w = self.buffer_in_weights[node]
126                            .get(buf_idx)
127                            .copied()
128                            .unwrap_or(1.0);
129                        new_in_weights[node].push(w);
130                    }
131                }
132            }
133        }
134
135        // Build new dense arrays.
136        let memory = &self.memory;
137        let out = Self::build_dense(&new_out_edges, &new_out_collections, memory)?;
138        let in_ = Self::build_dense(&new_in_edges, &new_in_collections, memory)?;
139
140        self.out_offsets = out.offsets;
141        self.out_targets = out.targets.into();
142        self.out_labels = out.labels.into();
143        self.out_collections = out.collections;
144        self.in_offsets = in_.offsets;
145        self.in_targets = in_.targets.into();
146        self.in_labels = in_.labels.into();
147        self.in_collections = in_.collections;
148
149        // Build weight arrays (flatten per-node vecs into contiguous array).
150        if self.has_weights {
151            self.out_weights = Some(
152                new_out_weights
153                    .into_iter()
154                    .flatten()
155                    .collect::<Vec<_>>()
156                    .into(),
157            );
158            self.in_weights = Some(
159                new_in_weights
160                    .into_iter()
161                    .flatten()
162                    .collect::<Vec<_>>()
163                    .into(),
164            );
165        }
166
167        // Clear buffer and deleted set.
168        for buf in &mut self.buffer_out {
169            buf.clear();
170        }
171        for buf in &mut self.buffer_in {
172            buf.clear();
173        }
174        for buf in &mut self.buffer_out_collections {
175            buf.clear();
176        }
177        for buf in &mut self.buffer_in_collections {
178            buf.clear();
179        }
180        if self.has_weights {
181            for buf in &mut self.buffer_out_weights {
182                buf.clear();
183            }
184            for buf in &mut self.buffer_in_weights {
185                buf.clear();
186            }
187        }
188        self.deleted_edges.clear();
189        Ok(())
190    }
191}