Skip to main content

linkrs_storage/edge/csr_with_properties/
maintenance.rs

1use super::{CsrWithProperties, RowVisibility};
2use crate::edge::csr_shared::EdgeIdBitSet;
3use crate::edge::property_schema::PropertySchema;
4use linkrs_core::types::{EdgeId, Timestamp};
5
6impl CsrWithProperties {
7    /// Pre-allocate row and column capacity for an incoming batch.
8    ///
9    /// Sizes visibility, reverse indexes and every property column once so a
10    /// batch of `additional` edges lands in reserved gaps instead of growing
11    /// row by row. Inline stubs keep no rows and skip reservation.
12    pub fn reserve_for_batch(&mut self, additional: usize) {
13        if additional == 0 || self.is_inline_stub() {
14            return;
15        }
16        self.visibility.reserve(additional);
17        self.row_to_edge.reserve(additional);
18        self.free_list.reserve(additional);
19        for col in &mut self.property_columns {
20            col.reserve(additional);
21        }
22    }
23
24    pub fn compaction_stats(&self) -> crate::edge::property_schema::PropertyCompactionStats {
25        let tombstone_count = self
26            .visibility
27            .iter()
28            .filter(|v| v.delete_ts.is_some())
29            .count();
30        let live_records = self.visibility.iter().filter(|v| v.is_live()).count();
31        let mut reclaimable_bytes = 0usize;
32        for v in &self.visibility {
33            if v.delete_ts.is_some() {
34                reclaimable_bytes += 32 * self.property_schema.len();
35            }
36        }
37        crate::edge::property_schema::PropertyCompactionStats {
38            tombstone_count,
39            total_records: self.visibility.len(),
40            live_records,
41            reclaimable_bytes,
42        }
43    }
44
45    /// Garbage-collect property version-chain entries that no active snapshot
46    /// can observe. Returns the total number of before-images removed.
47    pub fn gc_property_versions(&mut self, min_active_snapshot_ts: Timestamp) -> usize {
48        self.property_columns
49            .iter_mut()
50            .map(|col| col.gc_versions(min_active_snapshot_ts))
51            .sum()
52    }
53
54    pub fn used_memory_size(&self) -> usize {
55        let mut total = std::mem::size_of::<Self>();
56        total += self.visibility.capacity() * std::mem::size_of::<RowVisibility>();
57        total += self.edge_map_memory_bytes();
58        total += self.free_list.capacity() * std::mem::size_of::<u32>();
59        total += self.row_to_edge.capacity() * std::mem::size_of::<Option<EdgeId>>();
60        total += self.column_index.len()
61            * (std::mem::size_of::<String>() + std::mem::size_of::<usize>());
62        for col in &self.property_columns {
63            total += col.memory_size();
64        }
65        total += self.property_schema.len() * std::mem::size_of::<PropertySchema>();
66        total
67    }
68
69    /// Split property memory into mapping bytes (edge map plus reverse index
70    /// plus free list) and row bytes (visibility plus columns). Caliber for
71    /// per-edge accounting without walking rows.
72    pub fn mapping_row_bytes(&self) -> (usize, usize) {
73        let mapping = self.edge_map_memory_bytes()
74            + self.row_to_edge.capacity() * std::mem::size_of::<Option<EdgeId>>()
75            + self.free_list.capacity() * std::mem::size_of::<u32>();
76        let mut rows = self.visibility.capacity() * std::mem::size_of::<RowVisibility>();
77        for col in &self.property_columns {
78            rows += col.memory_size();
79        }
80        (mapping, rows)
81    }
82
83    pub(crate) fn reclaim_slots(
84        &mut self,
85        valid_edge_ids: &EdgeIdBitSet,
86        retention_bound: Timestamp,
87    ) -> usize {
88        if retention_bound == Timestamp::MAX {
89            return 0;
90        }
91        let mut to_reclaim = Vec::new();
92        for (idx, vis) in self.visibility.iter().enumerate() {
93            if vis.is_virgin() {
94                continue;
95            }
96            // O(1) ownership check via the reverse index instead of scanning
97            // the full edge map for every row.
98            let has_live_edge = self
99                .row_to_edge
100                .get(idx)
101                .and_then(|slot| *slot)
102                .is_some_and(|eid| valid_edge_ids.contains(&eid));
103            if has_live_edge {
104                continue;
105            }
106            if let Some(del_ts) = vis.delete_ts {
107                // Exclusive waterfront: deletable exactly when invisible to
108                // every snapshot at or past the cutoff.
109                if crate::mvcc_visibility::Visibility::is_gc_eligible(del_ts, retention_bound) {
110                    to_reclaim.push(idx);
111                }
112            }
113        }
114        if to_reclaim.is_empty() {
115            return 0;
116        }
117        for &idx in &to_reclaim {
118            self.visibility[idx].create_ts = 0;
119            self.visibility[idx].delete_ts = None;
120            if let Some(taken) = self.row_to_edge.get_mut(idx).and_then(|slot| slot.take()) {
121                self.map_remove(taken);
122            }
123            // Reclaimed rows are virgin after the stamp clear, so the free
124            // list admits each of them exactly once with no membership set.
125            self.free_list.push(idx as u32);
126            self.row_count = self.row_count.saturating_sub(1);
127        }
128        to_reclaim.len()
129    }
130
131    /// Recompute persisted per-column statistics from flush buffers.
132    ///
133    /// Called before the property file is serialized so statistics follow the
134    /// checkpoint instead of drifting. Only columns marked dirty are
135    /// recomputed; clean columns keep their persisted statistics. Columns
136    /// that fail to compute keep their previous statistics.
137    pub fn refresh_column_stats(&mut self) {
138        if self.dirty_columns.is_empty() {
139            for col in &mut self.property_columns {
140                if let Ok(stats) = col.compute_stats() {
141                    col.set_stats(stats);
142                }
143            }
144            return;
145        }
146        // Dirt marks stay until the checkpoint clears them: stats refresh
147        // must not consume the marks that drive dirty-column persistence.
148        let dirty: Vec<usize> = self.dirty_columns.iter().copied().collect();
149        for idx in dirty {
150            if let Some(col) = self.property_columns.get_mut(idx) {
151                if let Ok(stats) = col.compute_stats() {
152                    col.set_stats(stats);
153                }
154            }
155        }
156    }
157
158    /// Refresh statistics for one column only. Used by column-level
159    /// checkpoint follow-up when only a subset changed.
160    pub fn refresh_column_stats_for(&mut self, column: &str) {
161        if let Some(&idx) = self.column_index.get(column) {
162            if let Some(col) = self.property_columns.get_mut(idx) {
163                if let Ok(stats) = col.compute_stats() {
164                    col.set_stats(stats);
165                }
166            }
167        }
168    }
169}