grafeo_common/memory/buffer/
stats.rs1use super::region::MemoryRegion;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[non_exhaustive]
11pub enum PressureLevel {
12 Normal,
14 Moderate,
16 High,
18 Critical,
20}
21
22impl PressureLevel {
23 #[must_use]
25 pub const fn description(&self) -> &'static str {
26 match self {
27 Self::Normal => "Normal operation",
28 Self::Moderate => "Proactive eviction",
29 Self::High => "Aggressive eviction/spilling",
30 Self::Critical => "Blocking allocations",
31 }
32 }
33
34 #[must_use]
36 pub const fn requires_eviction(&self) -> bool {
37 matches!(self, Self::Moderate | Self::High | Self::Critical)
38 }
39
40 #[must_use]
42 pub const fn should_spill(&self) -> bool {
43 matches!(self, Self::High | Self::Critical)
44 }
45
46 #[must_use]
48 pub const fn blocks_allocations(&self) -> bool {
49 matches!(self, Self::Critical)
50 }
51}
52
53impl std::fmt::Display for PressureLevel {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::Normal => write!(f, "Normal"),
57 Self::Moderate => write!(f, "Moderate"),
58 Self::High => write!(f, "High"),
59 Self::Critical => write!(f, "Critical"),
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct BufferStats {
67 pub budget: usize,
69 pub total_allocated: usize,
71 pub region_allocated: [usize; 4],
73 pub pressure_level: PressureLevel,
75 pub consumer_count: usize,
77}
78
79impl BufferStats {
80 #[must_use]
82 pub fn utilization(&self) -> f64 {
83 if self.budget == 0 {
84 return 0.0;
85 }
86 self.total_allocated as f64 / self.budget as f64
87 }
88
89 #[must_use]
91 pub fn utilization_percent(&self) -> f64 {
92 self.utilization() * 100.0
93 }
94
95 #[must_use]
97 pub fn region_usage(&self, region: MemoryRegion) -> usize {
98 self.region_allocated[region.index()]
99 }
100
101 #[must_use]
103 pub fn available(&self) -> usize {
104 self.budget.saturating_sub(self.total_allocated)
105 }
106}
107
108impl Default for BufferStats {
109 fn default() -> Self {
110 Self {
111 budget: 0,
112 total_allocated: 0,
113 region_allocated: [0; 4],
114 pressure_level: PressureLevel::Normal,
115 consumer_count: 0,
116 }
117 }
118}
119
120impl std::fmt::Display for BufferStats {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 writeln!(f, "Buffer Manager Stats:")?;
123 writeln!(
124 f,
125 " Budget: {} ({:.2}% used)",
126 format_bytes(self.budget),
127 self.utilization_percent()
128 )?;
129 writeln!(
130 f,
131 " Allocated: {} / {}",
132 format_bytes(self.total_allocated),
133 format_bytes(self.budget)
134 )?;
135 writeln!(f, " Pressure: {}", self.pressure_level)?;
136 writeln!(f, " Consumers: {}", self.consumer_count)?;
137 writeln!(f, " Per-region:")?;
138 for region in MemoryRegion::all() {
139 writeln!(
140 f,
141 " {}: {}",
142 region.name(),
143 format_bytes(self.region_usage(region))
144 )?;
145 }
146 Ok(())
147 }
148}
149
150fn format_bytes(bytes: usize) -> String {
152 const KB: usize = 1024;
153 const MB: usize = KB * 1024;
154 const GB: usize = MB * 1024;
155
156 if bytes >= GB {
157 format!("{:.2} GB", bytes as f64 / GB as f64)
158 } else if bytes >= MB {
159 format!("{:.2} MB", bytes as f64 / MB as f64)
160 } else if bytes >= KB {
161 format!("{:.2} KB", bytes as f64 / KB as f64)
162 } else {
163 format!("{bytes} B")
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_pressure_level_ordering() {
173 assert!(PressureLevel::Normal < PressureLevel::Moderate);
174 assert!(PressureLevel::Moderate < PressureLevel::High);
175 assert!(PressureLevel::High < PressureLevel::Critical);
176 }
177
178 #[test]
179 fn test_pressure_level_properties() {
180 assert!(!PressureLevel::Normal.requires_eviction());
181 assert!(PressureLevel::Moderate.requires_eviction());
182 assert!(PressureLevel::High.requires_eviction());
183 assert!(PressureLevel::Critical.requires_eviction());
184
185 assert!(!PressureLevel::Normal.should_spill());
186 assert!(!PressureLevel::Moderate.should_spill());
187 assert!(PressureLevel::High.should_spill());
188 assert!(PressureLevel::Critical.should_spill());
189
190 assert!(!PressureLevel::Normal.blocks_allocations());
191 assert!(!PressureLevel::High.blocks_allocations());
192 assert!(PressureLevel::Critical.blocks_allocations());
193 }
194
195 #[test]
196 fn test_buffer_stats_utilization() {
197 let stats = BufferStats {
198 budget: 1000,
199 total_allocated: 750,
200 region_allocated: [250, 250, 200, 50],
201 pressure_level: PressureLevel::Moderate,
202 consumer_count: 3,
203 };
204
205 assert!((stats.utilization() - 0.75).abs() < 0.001);
206 assert!((stats.utilization_percent() - 75.0).abs() < 0.1);
207 assert_eq!(stats.available(), 250);
208 }
209
210 #[test]
211 fn test_buffer_stats_region_usage() {
212 let stats = BufferStats {
213 budget: 1000,
214 total_allocated: 600,
215 region_allocated: [100, 200, 250, 50],
216 pressure_level: PressureLevel::Normal,
217 consumer_count: 2,
218 };
219
220 assert_eq!(stats.region_usage(MemoryRegion::GraphStorage), 100);
221 assert_eq!(stats.region_usage(MemoryRegion::IndexBuffers), 200);
222 assert_eq!(stats.region_usage(MemoryRegion::ExecutionBuffers), 250);
223 assert_eq!(stats.region_usage(MemoryRegion::SpillStaging), 50);
224 }
225
226 #[test]
227 fn test_format_bytes() {
228 assert_eq!(format_bytes(512), "512 B");
229 assert_eq!(format_bytes(1024), "1.00 KB");
230 assert_eq!(format_bytes(1536), "1.50 KB");
231 assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
232 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
233 }
234
235 #[test]
236 fn test_pressure_level_description() {
237 assert_eq!(PressureLevel::Normal.description(), "Normal operation");
238 assert_eq!(PressureLevel::Moderate.description(), "Proactive eviction");
239 assert_eq!(
240 PressureLevel::High.description(),
241 "Aggressive eviction/spilling"
242 );
243 assert_eq!(
244 PressureLevel::Critical.description(),
245 "Blocking allocations"
246 );
247 }
248
249 #[test]
250 fn test_pressure_level_display() {
251 assert_eq!(PressureLevel::Normal.to_string(), "Normal");
252 assert_eq!(PressureLevel::Moderate.to_string(), "Moderate");
253 assert_eq!(PressureLevel::High.to_string(), "High");
254 assert_eq!(PressureLevel::Critical.to_string(), "Critical");
255 }
256
257 #[test]
258 fn test_buffer_stats_zero_budget() {
259 let stats = BufferStats {
260 budget: 0,
261 total_allocated: 0,
262 ..Default::default()
263 };
264 assert_eq!(stats.utilization(), 0.0);
265 assert_eq!(stats.utilization_percent(), 0.0);
266 }
267
268 #[test]
269 fn test_buffer_stats_default() {
270 let stats = BufferStats::default();
271 assert_eq!(stats.budget, 0);
272 assert_eq!(stats.total_allocated, 0);
273 assert_eq!(stats.pressure_level, PressureLevel::Normal);
274 assert_eq!(stats.consumer_count, 0);
275 }
276
277 #[test]
278 fn test_buffer_stats_available_saturates() {
279 let stats = BufferStats {
280 budget: 100,
281 total_allocated: 150,
282 ..Default::default()
283 };
284 assert_eq!(stats.available(), 0);
285 }
286
287 #[test]
288 fn test_buffer_stats_display_contains_budget() {
289 let stats = BufferStats {
290 budget: 1024,
291 total_allocated: 512,
292 region_allocated: [128, 128, 128, 128],
293 pressure_level: PressureLevel::Normal,
294 consumer_count: 1,
295 };
296 let s = stats.to_string();
297 assert!(s.contains("Budget"));
298 assert!(s.contains("Pressure"));
299 assert!(s.contains("Normal"));
300 }
301}