Skip to main content

linkrs_storage/edge/immutable_csr/
persistence.rs

1use super::super::csr_trait::CsrBase;
2use super::super::mutable_csr::serialization::{
3    decode_topology_i64_column, decode_topology_u32_column, decode_topology_u64_column,
4    encode_topology_i64_column, encode_topology_u32_column, encode_topology_u64_column,
5    TopologyColumnEncoding,
6};
7use super::super::{ColdStamps, EdgeId, HotNbr, INVALID_EDGE_ID};
8use super::ImmutableCsr;
9use crate::persistence::read_u64_le;
10use bitvec::order::Lsb0;
11use bitvec::vec::BitVec;
12use linkrs_core::{StorageError, StorageResult};
13
14impl ImmutableCsr {
15    /// Approximate memory usage in bytes.
16    pub fn used_memory_size(&self) -> usize {
17        self.hot_entries
18            .len()
19            .saturating_mul(std::mem::size_of::<HotNbr>())
20            .saturating_add(
21                self.cold_entries
22                    .len()
23                    .saturating_mul(std::mem::size_of::<ColdStamps>()),
24            )
25            .saturating_add(
26                self.degrees
27                    .len()
28                    .saturating_mul(std::mem::size_of::<u32>()),
29            )
30            .saturating_add(
31                self.offsets
32                    .len()
33                    .saturating_mul(std::mem::size_of::<u32>()),
34            )
35            .saturating_add(self.values.len().saturating_mul(std::mem::size_of::<u64>()))
36            .saturating_add(self.packed_valid_bytes().len())
37            .saturating_add(std::mem::size_of::<Self>())
38    }
39
40    /// Encoding report for the persisted neighbor columns.
41    pub fn topology_encoding_report(&self) -> Vec<(String, TopologyColumnEncoding, usize, usize)> {
42        let endpoints: Vec<u32> = self.hot_entries.iter().map(|hot| hot.endpoint).collect();
43        let edge_ids: Vec<u64> = self.hot_entries.iter().map(|hot| hot.edge_id.0).collect();
44        let (endpoint_choice, _) = encode_topology_u32_column(&endpoints);
45        let (edge_id_choice, _) = encode_topology_u64_column(&edge_ids);
46        let (degrees_choice, _) = encode_topology_u32_column(&self.degrees);
47        let mut report = vec![
48            (
49                "neighbor".to_string(),
50                endpoint_choice.encoding,
51                endpoint_choice.plain_bytes,
52                endpoint_choice.encoded_bytes,
53            ),
54            (
55                "edge_id".to_string(),
56                edge_id_choice.encoding,
57                edge_id_choice.plain_bytes,
58                edge_id_choice.encoded_bytes,
59            ),
60            (
61                "lengths".to_string(),
62                degrees_choice.encoding,
63                degrees_choice.plain_bytes,
64                degrees_choice.encoded_bytes,
65            ),
66        ];
67        if self.has_valued_entries() {
68            let (values_choice, _) = encode_topology_u64_column(&self.values);
69            report.push((
70                "values".to_string(),
71                values_choice.encoding,
72                values_choice.plain_bytes,
73                values_choice.encoded_bytes,
74            ));
75        }
76        report
77    }
78
79    /// Dump to bytes.
80    ///
81    /// Format:
82    /// - rows (u64)
83    /// - edge_count (u64)
84    /// - entries_len (u64)
85    /// - encoded degrees column
86    /// - encoded endpoints column
87    /// - encoded ranks column
88    /// - encoded edge ids column
89    /// - encoded delete stamps column
90    /// - has_values flag (u64, 0 or 1)
91    /// - encoded value words column (present only when the flag is set)
92    /// - validity byte count (u64) plus raw validity bytes (flag set only)
93    ///
94    /// Groups packed from non-bundled sources carry no value columns and
95    /// store a zero flag. Row offsets are memory-only and never persisted;
96    /// load rebuilds them.
97    pub fn dump(&self) -> Vec<u8> {
98        let mut result = Vec::new();
99        self.dump_into(&mut result);
100        result
101    }
102
103    /// Borrow-based dump into `out` without an intermediate owned buffer.
104    pub fn dump_into(&self, out: &mut Vec<u8>) {
105        let mut scratch = super::super::mutable_csr::persistence::CsrDumpScratch::new();
106        self.dump_into_with_scratch(out, &mut scratch);
107    }
108
109    /// Dump reusing caller-owned column buffers.
110    ///
111    /// Same bytes as [`Self::dump_into`]; a checkpoint over many groups pays
112    /// one allocation per column instead of one per group. The scratch holds
113    /// no state between calls.
114    pub fn dump_into_with_scratch(
115        &self,
116        out: &mut Vec<u8>,
117        scratch: &mut super::super::mutable_csr::persistence::CsrDumpScratch,
118    ) {
119        let start = out.len();
120        out.extend_from_slice(&(self.degrees.len() as u64).to_le_bytes());
121        out.extend_from_slice(&self.edge_count.to_le_bytes());
122        out.extend_from_slice(&(self.hot_entries.len() as u64).to_le_bytes());
123        let (_, degrees_payload) = encode_topology_u32_column(&self.degrees);
124        out.extend_from_slice(&degrees_payload);
125        scratch.fill_from_split(&self.hot_entries, &self.cold_entries);
126        let (_, endpoints_payload) = encode_topology_u32_column(scratch.endpoints());
127        out.extend_from_slice(&endpoints_payload);
128        let (_, ranks_payload) = encode_topology_i64_column(scratch.ranks());
129        out.extend_from_slice(&ranks_payload);
130        let (_, edge_ids_payload) = encode_topology_u64_column(scratch.edge_ids());
131        out.extend_from_slice(&edge_ids_payload);
132        let (_, delete_payload) = encode_topology_u64_column(scratch.deletes());
133        out.extend_from_slice(&delete_payload);
134        let valued = self.has_valued_entries();
135        out.extend_from_slice(&(u64::from(valued)).to_le_bytes());
136        if valued {
137            let (_, values_payload) = encode_topology_u64_column(&self.values);
138            out.extend_from_slice(&values_payload);
139            let valid_bytes = self.packed_valid_bytes();
140            out.extend_from_slice(&(valid_bytes.len() as u64).to_le_bytes());
141            out.extend_from_slice(valid_bytes);
142        }
143        let crc = crc32fast::hash(&out[start..]);
144        out.extend_from_slice(&crc.to_le_bytes());
145    }
146
147    /// Load from bytes.
148    ///
149    /// Rejects short headers, CRC mismatches, column
150    /// length mismatches, out-of-range row windows, edge count mismatches
151    /// and trailing bytes.
152    pub fn load(&mut self, data: &[u8]) -> StorageResult<()> {
153        if data.len() < 32 {
154            return Err(StorageError::deserialize_error(
155                "frozen CSR data too short for header",
156            ));
157        }
158        let (body, trailer) = data.split_at(data.len() - 4);
159        let mut stored_bytes = [0u8; 4];
160        stored_bytes.copy_from_slice(trailer);
161        let stored = u32::from_le_bytes(stored_bytes);
162        let computed = crc32fast::hash(body);
163        if stored != computed {
164            return Err(StorageError::deserialize_error(format!(
165                "frozen CSR dump CRC mismatch: stored={:#x} computed={:#x}",
166                stored, computed
167            )));
168        }
169        let data = body;
170        let mut offset = 0usize;
171        let rows = read_u64_le(data, &mut offset)? as usize;
172        let edge_count = read_u64_le(data, &mut offset)?;
173        let entries_len = read_u64_le(data, &mut offset)? as usize;
174        let degrees = decode_topology_u32_column(data, &mut offset)?;
175        if degrees.len() != rows {
176            return Err(StorageError::deserialize_error(
177                "frozen CSR degree column length mismatch",
178            ));
179        }
180        let endpoints = decode_topology_u32_column(data, &mut offset)?;
181        let ranks = decode_topology_i64_column(data, &mut offset)?;
182        let edge_ids = decode_topology_u64_column(data, &mut offset)?;
183        let delete_stamps = decode_topology_u64_column(data, &mut offset)?;
184        if endpoints.len() != entries_len
185            || ranks.len() != entries_len
186            || edge_ids.len() != entries_len
187            || delete_stamps.len() != entries_len
188        {
189            return Err(StorageError::deserialize_error(
190                "frozen CSR neighbor column length mismatch",
191            ));
192        }
193        let mut hot_entries = Vec::with_capacity(entries_len);
194        let mut cold_entries = Vec::with_capacity(entries_len);
195        let mut recomputed: u64 = 0;
196        for index in 0..entries_len {
197            let hot = HotNbr {
198                endpoint: endpoints[index],
199                rank: ranks[index],
200                edge_id: EdgeId(edge_ids[index]),
201            };
202            let cold = ColdStamps {
203                delete_ts: delete_stamps[index],
204            };
205            if hot.edge_id != INVALID_EDGE_ID && cold.is_live() {
206                recomputed += 1;
207            }
208            hot_entries.push(hot);
209            cold_entries.push(cold);
210        }
211        let mut covered = 0usize;
212        for degree in &degrees {
213            covered = covered.saturating_add(*degree as usize);
214        }
215        if covered != entries_len {
216            return Err(StorageError::deserialize_error(format!(
217                "frozen CSR row window mismatch: degrees cover {} entries, payload holds {}",
218                covered, entries_len
219            )));
220        }
221        if recomputed != edge_count {
222            return Err(StorageError::deserialize_error(format!(
223                "frozen CSR edge count mismatch: stored={}, recomputed={}",
224                edge_count, recomputed
225            )));
226        }
227        let has_values = read_u64_le(data, &mut offset)?;
228        let (values, valid) = match has_values {
229            0 => (Vec::new(), BitVec::new()),
230            1 => {
231                let words = decode_topology_u64_column(data, &mut offset)?;
232                if words.len() != entries_len {
233                    return Err(StorageError::deserialize_error(
234                        "frozen CSR value column length mismatch",
235                    ));
236                }
237                let valid_len = read_u64_le(data, &mut offset)? as usize;
238                let want_valid = entries_len.div_ceil(8);
239                if valid_len != want_valid {
240                    return Err(StorageError::deserialize_error(format!(
241                        "frozen CSR validity length mismatch: holds {} bytes, layout needs {}",
242                        valid_len, want_valid
243                    )));
244                }
245                let valid_bytes = data.get(offset..offset + valid_len).ok_or_else(|| {
246                    StorageError::deserialize_error("frozen CSR validity bytes truncated")
247                })?;
248                offset += valid_len;
249                let mut bits = BitVec::<u8, Lsb0>::from_slice(valid_bytes);
250                bits.truncate(entries_len);
251                (words, bits)
252            }
253            other => {
254                return Err(StorageError::deserialize_error(format!(
255                    "frozen CSR value flag out of range: {other}"
256                )));
257            }
258        };
259        if offset != data.len() {
260            return Err(StorageError::deserialize_error(
261                "unexpected trailing data in frozen CSR payload",
262            ));
263        }
264        self.hot_entries = hot_entries;
265        self.cold_entries = cold_entries;
266        self.degrees = degrees;
267        self.edge_count = edge_count;
268        self.values = values;
269        self.valid = valid;
270        self.rebuild_offsets();
271        Ok(())
272    }
273}
274
275impl CsrBase for ImmutableCsr {
276    fn vertex_capacity(&self) -> usize {
277        self.degrees.len()
278    }
279
280    fn edge_count(&self) -> u64 {
281        self.edge_count
282    }
283
284    fn dump(&self) -> Vec<u8> {
285        ImmutableCsr::dump(self)
286    }
287
288    fn dump_into(&self, out: &mut Vec<u8>) {
289        ImmutableCsr::dump_into(self, out);
290    }
291
292    fn load(&mut self, data: &[u8]) -> StorageResult<()> {
293        ImmutableCsr::load(self, data)
294    }
295}