Skip to main content

linkrs_storage/edge/mutable_csr/
overflow.rs

1use super::super::{csr_shared::OverflowTable, ColdStamps, HotNbr, Nbr};
2
3/// Single overflow chunk: hot topology halves plus cold stamp halves.
4///
5/// The two vectors grow in lockstep; index `i` of both addresses one slot.
6/// Scans that only need topology (edge-id lookup) or only liveness walk one
7/// vector, keeping the other half out of cache.
8#[derive(Debug, Clone, Default)]
9pub struct OverflowChunk {
10    hot: Vec<HotNbr>,
11    cold: Vec<ColdStamps>,
12}
13
14impl OverflowChunk {
15    pub fn with_capacity(cap: usize) -> Self {
16        Self {
17            hot: Vec::with_capacity(cap),
18            cold: Vec::with_capacity(cap),
19        }
20    }
21
22    #[inline]
23    pub fn len(&self) -> usize {
24        self.hot.len()
25    }
26
27    #[inline]
28    pub fn is_empty(&self) -> bool {
29        self.hot.is_empty()
30    }
31
32    /// Reserved slots of this chunk (hot and cold grow together).
33    #[inline]
34    pub fn capacity(&self) -> usize {
35        self.hot.capacity()
36    }
37
38    /// Assembled slot copy at `index`.
39    #[inline]
40    pub fn slot_at(&self, index: usize) -> Option<Nbr> {
41        Some(Nbr::from_parts(
42            *self.hot.get(index)?,
43            *self.cold.get(index)?,
44        ))
45    }
46
47    /// Hot half at `index` without touching the stamp lines.
48    #[inline]
49    pub fn hot_at(&self, index: usize) -> Option<HotNbr> {
50        self.hot.get(index).copied()
51    }
52
53    /// Cold half at `index` without touching the topology lines.
54    #[inline]
55    pub fn cold_at(&self, index: usize) -> Option<ColdStamps> {
56        self.cold.get(index).copied()
57    }
58
59    /// Hot slice for topology-only scans (edge-id lookup, key matching).
60    #[inline]
61    pub fn hot_slice(&self) -> &[HotNbr] {
62        &self.hot
63    }
64
65    /// Cold slice for liveness-only scans.
66    #[inline]
67    pub fn cold_slice(&self) -> &[ColdStamps] {
68        &self.cold
69    }
70
71    /// Mutable cold half for in-place stamping.
72    #[inline]
73    pub fn cold_at_mut(&mut self, index: usize) -> Option<&mut ColdStamps> {
74        self.cold.get_mut(index)
75    }
76
77    /// Append one assembled record, splitting it into the halves.
78    #[inline]
79    pub fn push(&mut self, nbr: Nbr) {
80        self.hot.push(nbr.hot());
81        self.cold.push(nbr.cold());
82        debug_assert_eq!(self.hot.len(), self.cold.len());
83    }
84
85    /// Remove and return the assembled record at `index`.
86    ///
87    /// Overflow order is unspecified; swap removal avoids the linear shift.
88    /// Both halves swap the same index so hot/cold alignment is preserved.
89    #[inline]
90    pub fn remove(&mut self, index: usize) -> Nbr {
91        let hot = self.hot.swap_remove(index);
92        let cold = self.cold.swap_remove(index);
93        Nbr::from_parts(hot, cold)
94    }
95
96    /// Extend the chunk from an assembled slice.
97    pub fn extend_from_slice(&mut self, slots: &[Nbr]) {
98        self.hot.reserve(slots.len());
99        self.cold.reserve(slots.len());
100        for nbr in slots {
101            self.hot.push(nbr.hot());
102            self.cold.push(nbr.cold());
103        }
104    }
105
106    /// Single contiguous chunk holding `slots`.
107    ///
108    /// Consolidation target for the repack passes: a repacked row reads as
109    /// its primary block plus at most one overflow block instead of a chain
110    /// of graded blocks. Built from the default (empty) chunk so the halves
111    /// reserve exactly the staged length with no graded over-reservation.
112    pub fn consolidated(slots: &[Nbr]) -> Self {
113        let mut chunk = OverflowChunk::default();
114        chunk.extend_from_slice(slots);
115        chunk
116    }
117
118    /// Single contiguous chunk holding `slots` plus reserved spare capacity.
119    ///
120    /// High-degree consolidation target: the spare keeps the next burst of
121    /// appends inside the same dedicated block instead of allocating a fresh
122    /// graded tail chunk per burst. Spare is capped so one huge row cannot
123    /// pin unbounded reservation; later bursts beyond the spare grow fresh
124    /// tail chunks until the next consolidation.
125    pub fn consolidated_with_reserve(slots: &[Nbr], spare: usize) -> Self {
126        let mut chunk = OverflowChunk::default();
127        chunk.hot.reserve(slots.len().saturating_add(spare));
128        chunk.cold.reserve(slots.len().saturating_add(spare));
129        chunk.extend_from_slice(slots);
130        chunk
131    }
132}
133
134/// Single benchmarked per-vertex overflow bound. Past this many chunks a row
135/// is repacked on the write path into one contiguous chunk; a row holding
136/// only live entries is merged the same way so skewed rows cannot grow
137/// unbounded chains. Region and full compactions still handle watermark
138/// reclaim separately.
139///
140/// Source: 8 chunks bound the pointer-chase depth of the widest rows while
141/// keeping the amortized repack cost off the insert hot path (covered by
142/// `test_supernode_overflow_consolidates_repack_into_single_block`).
143/// Recommended range 4..=16; lower repacks too eagerly on skewed inserts,
144/// higher lets chains degrade point lookups. Retuning requires rerunning
145/// the supernode benchmark first.
146pub(crate) const OVERFLOW_REPACK_CHUNKS_PER_VERTEX: usize = 8;
147
148impl super::super::csr_shared::OverflowChunkSpec for OverflowChunk {
149    type Slot = Nbr;
150
151    fn with_capacity(cap: usize) -> Self {
152        OverflowChunk::with_capacity(cap)
153    }
154
155    #[inline]
156    fn len(&self) -> usize {
157        self.len()
158    }
159
160    #[inline]
161    fn capacity(&self) -> usize {
162        self.capacity()
163    }
164
165    #[inline]
166    fn push_slot(&mut self, slot: Nbr) {
167        self.push(slot);
168    }
169}
170
171/// Mutable-CSR overflow storage: the shared overflow table over hot/cold
172/// stamp chunks. See [`OverflowTable`](super::super::csr_shared::OverflowTable).
173pub type OverflowStorage = OverflowTable<OverflowChunk>;
174
175#[cfg(test)]
176mod tests {
177    use super::super::super::csr_shared::SEGMENT_SIZE;
178    use super::*;
179    use linkrs_core::types::EdgeId;
180
181    fn chunks_with_first_endpoint(endpoint: u32) -> Vec<OverflowChunk> {
182        let mut chunk = OverflowChunk::default();
183        chunk.push(Nbr::new(endpoint, 0, EdgeId::new(1)));
184        vec![chunk]
185    }
186
187    #[test]
188    fn empty_vertices_cost_only_pointers() {
189        let mut storage = OverflowStorage::new();
190        storage.ensure_capacity(1_000_000);
191        assert!(storage.is_empty());
192        // One million empty rows: only the segment pointer table, well under
193        // the hundreds of megabytes a dense slot array would reserve.
194        assert!(storage.index_bytes() < 1_000_000);
195        assert!(storage.get(999_999).is_none());
196    }
197
198    #[test]
199    fn routes_across_segment_boundary() {
200        let mut storage = OverflowStorage::new();
201        let lo = SEGMENT_SIZE as u32 - 1;
202        let hi = SEGMENT_SIZE as u32;
203        let far = 2 * SEGMENT_SIZE as u32 + 5;
204        storage.insert(lo, chunks_with_first_endpoint(10));
205        storage.insert(hi, chunks_with_first_endpoint(20));
206        storage.insert(far, chunks_with_first_endpoint(30));
207        assert_eq!(storage.len(), 3);
208        for (vid, endpoint) in [(lo, 10), (hi, 20), (far, 30)] {
209            let chunks = storage.get(vid).expect("row present");
210            assert_eq!(chunks[0].hot_at(0).expect("slot").endpoint, endpoint);
211        }
212        let taken = storage.remove(hi).expect("row removed");
213        assert_eq!(taken[0].hot_at(0).expect("slot").endpoint, 20);
214        assert_eq!(storage.len(), 2);
215        assert!(storage.get(hi).is_none());
216    }
217
218    #[test]
219    fn iter_walks_rows_in_vertex_order() {
220        let mut storage = OverflowStorage::new();
221        let far = 3 * SEGMENT_SIZE as u32 + 7;
222        let near = 5u32;
223        storage.insert(far, chunks_with_first_endpoint(30));
224        storage.insert(near, chunks_with_first_endpoint(10));
225        storage.insert(SEGMENT_SIZE as u32, chunks_with_first_endpoint(20));
226        let order: Vec<u32> = storage.iter().map(|(vid, _)| vid).collect();
227        assert_eq!(order, vec![near, SEGMENT_SIZE as u32, far]);
228    }
229
230    #[test]
231    fn clear_empties_allocated_segments() {
232        let mut storage = OverflowStorage::new();
233        storage.insert(SEGMENT_SIZE as u32 + 1, chunks_with_first_endpoint(1));
234        storage.clear();
235        assert!(storage.is_empty());
236        assert_eq!(storage.len(), 0);
237        assert!(storage.get(SEGMENT_SIZE as u32 + 1).is_none());
238        storage.insert(3, chunks_with_first_endpoint(2));
239        assert_eq!(storage.len(), 1);
240    }
241}