linkrs_storage/edge/
fragmentation_stats.rs1pub const GROUP_FRAGMENTATION_THRESHOLD: f32 = 0.5;
21
22#[derive(Debug, Clone, Copy)]
23pub struct FragmentationStats {
24 pub total_capacity: usize,
26 pub reachable_edges: usize,
28 pub dead_entries: usize,
31 pub wasted_capacity: usize,
33}
34
35impl FragmentationStats {
36 pub fn with_dead_info(
38 total_capacity: usize,
39 reachable_edges: usize,
40 dead_entries: usize,
41 wasted_capacity: usize,
42 ) -> Self {
43 Self {
44 total_capacity,
45 reachable_edges,
46 dead_entries,
47 wasted_capacity,
48 }
49 }
50
51 pub fn fragmentation_ratio(&self) -> f32 {
60 if self.total_capacity == 0 {
61 0.0
62 } else {
63 self.wasted_capacity as f32 / self.total_capacity as f32
64 }
65 }
66
67 pub fn measured_bytes_per_edge(used_bytes: usize, edges: u64) -> usize {
72 if edges == 0 {
73 return 0;
74 }
75 let bpe = used_bytes / edges as usize;
76 if bpe == 0 {
77 log::debug!(
78 "bytes_per_edge: measured zero ({} bytes / {} edges), reporting zero",
79 used_bytes,
80 edges,
81 );
82 }
83 bpe
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct VertexFragmentation {
93 pub vertex: u32,
95 pub live_edges: usize,
97 pub dead_entries: usize,
99 pub capacity: usize,
101 pub reclaimable: usize,
103}
104
105impl VertexFragmentation {
106 pub fn waste(&self) -> usize {
108 self.capacity.saturating_sub(self.live_edges)
109 }
110
111 pub fn density(&self) -> f32 {
113 if self.capacity == 0 {
114 1.0
115 } else {
116 self.live_edges as f32 / self.capacity as f32
117 }
118 }
119
120 pub fn needs_compact(&self) -> bool {
122 self.reclaimable > 0
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_fragmentation_stats_no_fragmentation() {
132 let stats = FragmentationStats {
133 total_capacity: 100,
134 reachable_edges: 100,
135 dead_entries: 0,
136 wasted_capacity: 0,
137 };
138
139 assert_eq!(stats.fragmentation_ratio(), 0.0);
140 assert!(stats.fragmentation_ratio() < GROUP_FRAGMENTATION_THRESHOLD);
141 }
142
143 #[test]
144 fn test_fragmentation_stats_with_fragmentation() {
145 let stats = FragmentationStats {
146 total_capacity: 100,
147 reachable_edges: 30,
148 dead_entries: 2,
149 wasted_capacity: 50,
150 };
151
152 assert_eq!(stats.fragmentation_ratio(), 0.5);
153 assert!(stats.fragmentation_ratio() >= GROUP_FRAGMENTATION_THRESHOLD);
154 }
155
156 #[test]
157 fn test_fragmentation_stats_severe_fragmentation() {
158 let stats = FragmentationStats {
159 total_capacity: 100,
160 reachable_edges: 20,
161 dead_entries: 3,
162 wasted_capacity: 80,
163 };
164
165 assert_eq!(stats.fragmentation_ratio(), 0.8);
166 assert!(stats.fragmentation_ratio() >= GROUP_FRAGMENTATION_THRESHOLD);
167 }
168
169 #[test]
170 fn test_fragmentation_stats_empty() {
171 let stats = FragmentationStats::with_dead_info(0, 0, 0, 0);
172
173 assert_eq!(stats.fragmentation_ratio(), 0.0);
174 }
175
176 #[test]
177 fn test_fragmentation_ratio_threshold() {
178 let stats = FragmentationStats {
179 total_capacity: 100,
180 reachable_edges: 50,
181 dead_entries: 1,
182 wasted_capacity: 60,
183 };
184
185 assert!(stats.fragmentation_ratio() >= GROUP_FRAGMENTATION_THRESHOLD);
186 assert!(stats.fragmentation_ratio() <= 1.0);
187 }
188
189 #[test]
190 fn test_measured_bytes_per_edge() {
191 assert_eq!(FragmentationStats::measured_bytes_per_edge(0, 0), 0);
192 assert_eq!(FragmentationStats::measured_bytes_per_edge(100, 0), 0);
193 assert_eq!(FragmentationStats::measured_bytes_per_edge(100, 10), 10);
194 assert_eq!(FragmentationStats::measured_bytes_per_edge(5, 10), 0);
195 }
196
197 #[test]
198 fn test_vertex_fragmentation_view() {
199 let view = VertexFragmentation {
200 vertex: 3,
201 live_edges: 4,
202 dead_entries: 1,
203 capacity: 8,
204 reclaimable: 1,
205 };
206 assert_eq!(view.waste(), 4);
207 assert_eq!(view.density(), 0.5);
208 assert!(view.needs_compact());
209
210 let clean = VertexFragmentation {
211 vertex: 4,
212 live_edges: 4,
213 dead_entries: 0,
214 capacity: 8,
215 reclaimable: 0,
216 };
217 assert!(!clean.needs_compact());
218 }
219}