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