Skip to main content

vtcode_core/memory/
mod.rs

1//! Memory monitoring and pressure detection system.
2//!
3//! This module provides real-time memory usage tracking for VT Code, including:
4//! - RSS-based memory monitoring
5//! - Memory pressure classification (Normal, Warning, Critical)
6//! - Memory checkpoints for debugging
7//! - Adaptive TTL based on memory pressure
8
9use std::collections::VecDeque;
10use std::sync::{Arc, Mutex};
11use vtcode_commons::utils::current_timestamp;
12
13pub use self::pressure::MemoryPressure;
14
15mod pressure;
16
17/// Memory monitor for tracking system memory usage
18#[derive(Clone)]
19pub struct MemoryMonitor {
20    state: Arc<Mutex<MemoryMonitorState>>,
21}
22
23struct MemoryMonitorState {
24    /// Memory checkpoint history for debugging
25    checkpoints: VecDeque<MemoryCheckpoint>,
26    /// Last recorded RSS in bytes
27    last_rss_bytes: usize,
28    /// Timestamp of last check
29    last_check_timestamp: u64,
30}
31
32/// Memory checkpoint for debugging memory spikes
33#[derive(Debug, Clone)]
34pub struct MemoryCheckpoint {
35    /// Timestamp when checkpoint was recorded
36    pub timestamp: u64,
37    /// RSS memory at checkpoint (bytes)
38    pub rss_bytes: usize,
39    /// Label/context for this checkpoint
40    pub label: String,
41}
42
43/// Memory report for user visibility
44#[derive(Debug, Clone)]
45pub struct MemoryReport {
46    /// Current RSS in MB
47    pub current_rss_mb: f64,
48    /// Soft limit in MB
49    pub soft_limit_mb: f64,
50    /// Hard limit in MB
51    pub hard_limit_mb: f64,
52    /// Current memory pressure
53    pub pressure: MemoryPressure,
54    /// Usage percentage (0-100)
55    pub usage_percent: f64,
56    /// Recent memory checkpoints
57    pub recent_checkpoints: Vec<MemoryCheckpoint>,
58}
59
60impl MemoryMonitor {
61    /// Create a new memory monitor
62    pub fn new() -> Self {
63        Self {
64            state: Arc::new(Mutex::new(MemoryMonitorState {
65                checkpoints: VecDeque::with_capacity(
66                    vtcode_config::constants::memory::MAX_CHECKPOINT_HISTORY,
67                ),
68                last_rss_bytes: 0,
69                last_check_timestamp: 0,
70            })),
71        }
72    }
73
74    /// Get current RSS in bytes using platform-specific methods
75    #[cfg(target_os = "linux")]
76    pub fn get_rss_bytes() -> Result<usize, String> {
77        use std::fs;
78        use std::io::Read;
79
80        let mut status = String::new();
81        fs::File::open("/proc/self/status")
82            .and_then(|mut f| f.read_to_string(&mut status))
83            .map_err(|e| format!("Failed to read /proc/self/status: {}", e))?;
84
85        for line in status.lines() {
86            if line.starts_with("VmRSS:") {
87                let parts: Vec<&str> = line.split_whitespace().collect();
88                if parts.len() >= 2 {
89                    let kb: usize = parts[1]
90                        .parse()
91                        .map_err(|_| "Failed to parse VmRSS value".to_string())?;
92                    return Ok(kb * 1024); // Convert KB to bytes
93                }
94            }
95        }
96
97        Err("VmRSS not found in /proc/self/status".to_string())
98    }
99
100    /// Get current RSS in bytes on macOS using /proc/self/stat or sysctl
101    #[cfg(target_os = "macos")]
102    pub fn get_rss_bytes() -> Result<usize, String> {
103        use std::process::Command;
104
105        // Use `ps` command to get RSS in kilobytes
106        let output = Command::new("ps")
107            .args(["-o", "rss=", "-p"])
108            .arg(std::process::id().to_string())
109            .output()
110            .map_err(|e| format!("Failed to run ps command: {}", e))?;
111
112        let rss_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
113
114        let kb: usize = rss_str
115            .parse()
116            .map_err(|_| format!("Failed to parse ps output: {}", rss_str))?;
117
118        Ok(kb * 1024) // Convert KB to bytes
119    }
120
121    /// Get current RSS in bytes (fallback for unsupported platforms)
122    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
123    pub fn get_rss_bytes() -> Result<usize, String> {
124        Err("Memory monitoring not supported on this platform".to_string())
125    }
126
127    /// Check current memory pressure
128    pub fn check_pressure(&self) -> Result<MemoryPressure, String> {
129        let rss = Self::get_rss_bytes()?;
130        let pressure = MemoryPressure::from_rss(rss);
131
132        // Update last known RSS
133        if let Ok(mut state) = self.state.lock() {
134            state.last_rss_bytes = rss;
135            state.last_check_timestamp = current_timestamp();
136        }
137
138        Ok(pressure)
139    }
140
141    /// Record a memory checkpoint for debugging
142    pub fn record_checkpoint(&self, label: String) -> Result<(), String> {
143        let rss = Self::get_rss_bytes()?;
144
145        // Only record if change is significant (> 1 MB)
146        let min_threshold = vtcode_config::constants::memory::MIN_RSS_CHECKPOINT_BYTES;
147        if let Ok(state) = self.state.lock() {
148            let diff = (rss as i64 - state.last_rss_bytes as i64).unsigned_abs() as usize;
149            if diff < min_threshold {
150                return Ok(());
151            }
152        }
153
154        let checkpoint = MemoryCheckpoint {
155            timestamp: current_timestamp(),
156            rss_bytes: rss,
157            label,
158        };
159
160        if let Ok(mut state) = self.state.lock() {
161            state.checkpoints.push_back(checkpoint);
162
163            // Enforce max checkpoint history
164            let max_history = vtcode_config::constants::memory::MAX_CHECKPOINT_HISTORY;
165            while state.checkpoints.len() > max_history {
166                state.checkpoints.pop_front();
167            }
168        }
169
170        Ok(())
171    }
172
173    /// Get memory report for user visibility
174    pub fn get_report(&self) -> Result<MemoryReport, String> {
175        let rss_bytes = Self::get_rss_bytes()?;
176        let pressure = MemoryPressure::from_rss(rss_bytes);
177
178        let soft_limit =
179            vtcode_config::constants::memory::SOFT_LIMIT_BYTES as f64 / (1024.0 * 1024.0);
180        let hard_limit =
181            vtcode_config::constants::memory::HARD_LIMIT_BYTES as f64 / (1024.0 * 1024.0);
182        let current_rss_mb = rss_bytes as f64 / (1024.0 * 1024.0);
183
184        // Use hard limit for percentage calculation (worst case)
185        let usage_percent =
186            (rss_bytes as f64 / vtcode_config::constants::memory::HARD_LIMIT_BYTES as f64) * 100.0;
187
188        let recent_checkpoints = if let Ok(state) = self.state.lock() {
189            state.checkpoints.iter().cloned().collect()
190        } else {
191            Vec::new()
192        };
193
194        Ok(MemoryReport {
195            current_rss_mb,
196            soft_limit_mb: soft_limit,
197            hard_limit_mb: hard_limit,
198            pressure,
199            usage_percent,
200            recent_checkpoints,
201        })
202    }
203
204    /// Get adaptive TTL factor based on current memory pressure
205    pub fn adaptive_ttl_factor(&self) -> f64 {
206        match self.check_pressure() {
207            Ok(MemoryPressure::Normal) => 1.0,
208            Ok(MemoryPressure::Warning) => {
209                vtcode_config::constants::memory::WARNING_TTL_REDUCTION_FACTOR
210            }
211            Ok(MemoryPressure::Critical) => {
212                vtcode_config::constants::memory::CRITICAL_TTL_REDUCTION_FACTOR
213            }
214            Err(_) => 1.0, // Assume normal if we can't check
215        }
216    }
217
218    /// Clear checkpoint history (for testing)
219    pub fn clear_checkpoints(&self) {
220        if let Ok(mut state) = self.state.lock() {
221            state.checkpoints.clear();
222        }
223    }
224
225    /// Get number of recorded checkpoints
226    pub fn checkpoint_count(&self) -> usize {
227        self.state
228            .lock()
229            .map(|state| state.checkpoints.len())
230            .unwrap_or(0)
231    }
232}
233
234impl Default for MemoryMonitor {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_memory_monitor_creation() {
246        let monitor = MemoryMonitor::new();
247        assert_eq!(monitor.checkpoint_count(), 0);
248    }
249
250    #[test]
251    fn test_get_rss_bytes() {
252        match MemoryMonitor::get_rss_bytes() {
253            Ok(rss) => {
254                // RSS should be reasonable (> 1MB and < 10GB)
255                assert!(rss > 1024 * 1024, "RSS should be > 1MB");
256                assert!(rss < 10 * 1024 * 1024 * 1024, "RSS should be < 10GB");
257            }
258            Err(e) => {
259                println!("Warning: Could not get RSS: {}", e);
260                // This is acceptable on unsupported platforms
261            }
262        }
263    }
264
265    #[test]
266    fn test_check_pressure() {
267        let monitor = MemoryMonitor::new();
268        match monitor.check_pressure() {
269            Ok(pressure) => {
270                // Should always return a valid pressure level
271                let _ = format!("{:?}", pressure);
272            }
273            Err(e) => {
274                println!("Warning: Could not check pressure: {}", e);
275                // Acceptable on unsupported platforms
276            }
277        }
278    }
279
280    #[test]
281    fn test_record_checkpoint() {
282        let monitor = MemoryMonitor::new();
283        // Try to record checkpoints (may fail on unsupported platforms)
284        let _result = monitor.record_checkpoint("test_checkpoint".to_string());
285    }
286
287    #[test]
288    fn test_clear_checkpoints() {
289        let monitor = MemoryMonitor::new();
290        monitor.clear_checkpoints();
291        assert_eq!(monitor.checkpoint_count(), 0);
292    }
293
294    #[test]
295    fn test_adaptive_ttl_factor() {
296        let monitor = MemoryMonitor::new();
297        let factor = monitor.adaptive_ttl_factor();
298
299        // TTL factor should be between 0.1 and 1.0
300        assert!(factor > 0.0);
301        assert!(factor <= 1.0);
302    }
303
304    #[test]
305    fn test_memory_report() {
306        let monitor = MemoryMonitor::new();
307        match monitor.get_report() {
308            Ok(report) => {
309                // Report should have reasonable values
310                assert!(report.usage_percent >= 0.0);
311                assert!(report.soft_limit_mb > 0.0);
312                assert!(report.hard_limit_mb > report.soft_limit_mb);
313            }
314            Err(e) => {
315                println!("Warning: Could not generate report: {}", e);
316                // Acceptable on unsupported platforms
317            }
318        }
319    }
320}