Skip to main content

grafeo_common/memory/buffer/
stats.rs

1//! Buffer manager statistics and pressure levels.
2
3use super::region::MemoryRegion;
4
5/// Memory pressure level thresholds.
6///
7/// The buffer manager uses these levels to determine when to
8/// trigger eviction and spilling.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[non_exhaustive]
11pub enum PressureLevel {
12    /// < 70% used - Normal operation, no action needed.
13    Normal,
14    /// 70-85% used - Begin evicting cold data proactively.
15    Moderate,
16    /// 85-95% used - Aggressive eviction, trigger spilling.
17    High,
18    /// > 95% used - Critical, block new allocations until memory freed.
19    Critical,
20}
21
22impl PressureLevel {
23    /// Returns a human-readable description of this pressure level.
24    #[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    /// Returns whether this level requires eviction action.
35    #[must_use]
36    pub const fn requires_eviction(&self) -> bool {
37        matches!(self, Self::Moderate | Self::High | Self::Critical)
38    }
39
40    /// Returns whether this level should trigger spilling.
41    #[must_use]
42    pub const fn should_spill(&self) -> bool {
43        matches!(self, Self::High | Self::Critical)
44    }
45
46    /// Returns whether allocations should be blocked at this level.
47    #[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/// Statistics about buffer manager state.
65#[derive(Debug, Clone)]
66pub struct BufferStats {
67    /// Total memory budget in bytes.
68    pub budget: usize,
69    /// Total allocated bytes across all regions.
70    pub total_allocated: usize,
71    /// Per-region allocation in bytes.
72    pub region_allocated: [usize; 4],
73    /// Current pressure level.
74    pub pressure_level: PressureLevel,
75    /// Number of registered consumers.
76    pub consumer_count: usize,
77}
78
79impl BufferStats {
80    /// Returns the utilization as a fraction (0.0 to 1.0).
81    #[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    /// Returns the utilization as a percentage (0 to 100).
90    #[must_use]
91    pub fn utilization_percent(&self) -> f64 {
92        self.utilization() * 100.0
93    }
94
95    /// Returns allocated bytes for a specific region.
96    #[must_use]
97    pub fn region_usage(&self, region: MemoryRegion) -> usize {
98        self.region_allocated[region.index()]
99    }
100
101    /// Returns available bytes (budget - allocated).
102    #[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
150/// Formats bytes in human-readable form.
151fn 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}