vtcode_core/memory/
mod.rs1use 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#[derive(Debug, Error)]
21pub enum MemoryError {
22 #[error("failed to read memory info: {0}")]
24 Io(#[from] std::io::Error),
25
26 #[error("failed to parse memory value: {0}")]
28 Parse(String),
29
30 #[error("memory monitoring not supported on this platform")]
32 Unsupported,
33}
34
35#[derive(Clone)]
37pub struct MemoryMonitor {
38 state: Arc<Mutex<MemoryMonitorState>>,
39}
40
41struct MemoryMonitorState {
42 checkpoints: VecDeque<RssCheckpoint>,
44 last_rss_bytes: usize,
46 last_check_timestamp: u64,
48}
49
50#[derive(Debug, Clone)]
52pub struct RssCheckpoint {
53 pub timestamp: u64,
55 pub rss_bytes: usize,
57 pub label: String,
59}
60
61#[derive(Debug, Clone)]
63pub struct MemoryReport {
64 pub current_rss_mb: f64,
66 pub soft_limit_mb: f64,
68 pub hard_limit_mb: f64,
70 pub pressure: MemoryPressure,
72 pub usage_percent: f64,
74 pub recent_checkpoints: Vec<RssCheckpoint>,
76}
77
78impl MemoryMonitor {
79 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 #[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); }
108 }
109 }
110
111 Err(MemoryError::Parse("VmRSS not found in /proc/self/status".to_string()))
112 }
113
114 #[cfg(target_os = "macos")]
116 pub fn get_rss_bytes() -> Result<usize, MemoryError> {
117 use std::process::Command;
118
119 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) }
133
134 #[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 pub fn check_pressure(&self) -> Result<MemoryPressure, MemoryError> {
142 let rss = Self::get_rss_bytes()?;
143 let pressure = MemoryPressure::from_rss(rss);
144
145 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 pub fn record_checkpoint(&self, label: String) -> Result<(), MemoryError> {
156 let rss = Self::get_rss_bytes()?;
157
158 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 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 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 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 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 pub fn clear_checkpoints(&self) {
227 if let Ok(mut state) = self.state.lock() {
228 state.checkpoints.clear();
229 }
230 }
231
232 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 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 }
266 }
267 }
268
269 #[test]
270 fn test_check_pressure() {
271 let monitor = MemoryMonitor::new();
272 match monitor.check_pressure() {
273 Ok(pressure) => {
274 let _ = format!("{pressure:?}");
276 }
277 Err(e) => {
278 println!("Warning: Could not check pressure: {e}");
279 }
281 }
282 }
283
284 #[test]
285 fn test_record_checkpoint() {
286 let monitor = MemoryMonitor::new();
287 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 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 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 }
322 }
323 }
324}