Skip to main content

graphos_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)]
10pub enum PressureLevel {
11    /// < 70% used - Normal operation, no action needed.
12    Normal,
13    /// 70-85% used - Begin evicting cold data proactively.
14    Moderate,
15    /// 85-95% used - Aggressive eviction, trigger spilling.
16    High,
17    /// > 95% used - Critical, block new allocations until memory freed.
18    Critical,
19}
20
21impl PressureLevel {
22    /// Returns a human-readable description of this pressure level.
23    #[must_use]
24    pub const fn description(&self) -> &'static str {
25        match self {
26            Self::Normal => "Normal operation",
27            Self::Moderate => "Proactive eviction",
28            Self::High => "Aggressive eviction/spilling",
29            Self::Critical => "Blocking allocations",
30        }
31    }
32
33    /// Returns whether this level requires eviction action.
34    #[must_use]
35    pub const fn requires_eviction(&self) -> bool {
36        matches!(self, Self::Moderate | Self::High | Self::Critical)
37    }
38
39    /// Returns whether this level should trigger spilling.
40    #[must_use]
41    pub const fn should_spill(&self) -> bool {
42        matches!(self, Self::High | Self::Critical)
43    }
44
45    /// Returns whether allocations should be blocked at this level.
46    #[must_use]
47    pub const fn blocks_allocations(&self) -> bool {
48        matches!(self, Self::Critical)
49    }
50}
51
52impl std::fmt::Display for PressureLevel {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Self::Normal => write!(f, "Normal"),
56            Self::Moderate => write!(f, "Moderate"),
57            Self::High => write!(f, "High"),
58            Self::Critical => write!(f, "Critical"),
59        }
60    }
61}
62
63/// Statistics about buffer manager state.
64#[derive(Debug, Clone)]
65pub struct BufferStats {
66    /// Total memory budget in bytes.
67    pub budget: usize,
68    /// Total allocated bytes across all regions.
69    pub total_allocated: usize,
70    /// Per-region allocation in bytes.
71    pub region_allocated: [usize; 4],
72    /// Current pressure level.
73    pub pressure_level: PressureLevel,
74    /// Number of registered consumers.
75    pub consumer_count: usize,
76}
77
78impl BufferStats {
79    /// Returns the utilization as a fraction (0.0 to 1.0).
80    #[must_use]
81    pub fn utilization(&self) -> f64 {
82        if self.budget == 0 {
83            return 0.0;
84        }
85        self.total_allocated as f64 / self.budget as f64
86    }
87
88    /// Returns the utilization as a percentage (0 to 100).
89    #[must_use]
90    pub fn utilization_percent(&self) -> f64 {
91        self.utilization() * 100.0
92    }
93
94    /// Returns allocated bytes for a specific region.
95    #[must_use]
96    pub fn region_usage(&self, region: MemoryRegion) -> usize {
97        self.region_allocated[region.index()]
98    }
99
100    /// Returns available bytes (budget - allocated).
101    #[must_use]
102    pub fn available(&self) -> usize {
103        self.budget.saturating_sub(self.total_allocated)
104    }
105}
106
107impl Default for BufferStats {
108    fn default() -> Self {
109        Self {
110            budget: 0,
111            total_allocated: 0,
112            region_allocated: [0; 4],
113            pressure_level: PressureLevel::Normal,
114            consumer_count: 0,
115        }
116    }
117}
118
119impl std::fmt::Display for BufferStats {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        writeln!(f, "Buffer Manager Stats:")?;
122        writeln!(
123            f,
124            "  Budget: {} ({:.2}% used)",
125            format_bytes(self.budget),
126            self.utilization_percent()
127        )?;
128        writeln!(
129            f,
130            "  Allocated: {} / {}",
131            format_bytes(self.total_allocated),
132            format_bytes(self.budget)
133        )?;
134        writeln!(f, "  Pressure: {}", self.pressure_level)?;
135        writeln!(f, "  Consumers: {}", self.consumer_count)?;
136        writeln!(f, "  Per-region:")?;
137        for region in MemoryRegion::all() {
138            writeln!(
139                f,
140                "    {}: {}",
141                region.name(),
142                format_bytes(self.region_usage(region))
143            )?;
144        }
145        Ok(())
146    }
147}
148
149/// Formats bytes in human-readable form.
150fn format_bytes(bytes: usize) -> String {
151    const KB: usize = 1024;
152    const MB: usize = KB * 1024;
153    const GB: usize = MB * 1024;
154
155    if bytes >= GB {
156        format!("{:.2} GB", bytes as f64 / GB as f64)
157    } else if bytes >= MB {
158        format!("{:.2} MB", bytes as f64 / MB as f64)
159    } else if bytes >= KB {
160        format!("{:.2} KB", bytes as f64 / KB as f64)
161    } else {
162        format!("{bytes} B")
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_pressure_level_ordering() {
172        assert!(PressureLevel::Normal < PressureLevel::Moderate);
173        assert!(PressureLevel::Moderate < PressureLevel::High);
174        assert!(PressureLevel::High < PressureLevel::Critical);
175    }
176
177    #[test]
178    fn test_pressure_level_properties() {
179        assert!(!PressureLevel::Normal.requires_eviction());
180        assert!(PressureLevel::Moderate.requires_eviction());
181        assert!(PressureLevel::High.requires_eviction());
182        assert!(PressureLevel::Critical.requires_eviction());
183
184        assert!(!PressureLevel::Normal.should_spill());
185        assert!(!PressureLevel::Moderate.should_spill());
186        assert!(PressureLevel::High.should_spill());
187        assert!(PressureLevel::Critical.should_spill());
188
189        assert!(!PressureLevel::Normal.blocks_allocations());
190        assert!(!PressureLevel::High.blocks_allocations());
191        assert!(PressureLevel::Critical.blocks_allocations());
192    }
193
194    #[test]
195    fn test_buffer_stats_utilization() {
196        let stats = BufferStats {
197            budget: 1000,
198            total_allocated: 750,
199            region_allocated: [250, 250, 200, 50],
200            pressure_level: PressureLevel::Moderate,
201            consumer_count: 3,
202        };
203
204        assert!((stats.utilization() - 0.75).abs() < 0.001);
205        assert!((stats.utilization_percent() - 75.0).abs() < 0.1);
206        assert_eq!(stats.available(), 250);
207    }
208
209    #[test]
210    fn test_buffer_stats_region_usage() {
211        let stats = BufferStats {
212            budget: 1000,
213            total_allocated: 600,
214            region_allocated: [100, 200, 250, 50],
215            pressure_level: PressureLevel::Normal,
216            consumer_count: 2,
217        };
218
219        assert_eq!(stats.region_usage(MemoryRegion::GraphStorage), 100);
220        assert_eq!(stats.region_usage(MemoryRegion::IndexBuffers), 200);
221        assert_eq!(stats.region_usage(MemoryRegion::ExecutionBuffers), 250);
222        assert_eq!(stats.region_usage(MemoryRegion::SpillStaging), 50);
223    }
224
225    #[test]
226    fn test_format_bytes() {
227        assert_eq!(format_bytes(512), "512 B");
228        assert_eq!(format_bytes(1024), "1.00 KB");
229        assert_eq!(format_bytes(1536), "1.50 KB");
230        assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
231        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
232    }
233}