Skip to main content

ruvector_memopt/apps/
leaks.rs

1//! Memory leak detection
2//!
3//! Monitors processes over time to detect potential memory leaks:
4//! - Tracks memory usage history
5//! - Detects consistent memory growth
6//! - Identifies processes with abnormal memory patterns
7//! - Provides recommendations
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use sysinfo::{System, ProcessesToUpdate, Pid};
12
13/// Memory sample for a process
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct MemorySample {
16    pub timestamp: u64,
17    pub memory_mb: f64,
18    pub cpu_percent: f32,
19}
20
21/// Process memory history
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ProcessHistory {
24    pub pid: u32,
25    pub name: String,
26    pub samples: Vec<MemorySample>,
27    pub start_memory_mb: f64,
28    pub current_memory_mb: f64,
29    pub peak_memory_mb: f64,
30    pub growth_rate_mb_per_hour: f64,
31    pub is_likely_leak: bool,
32    pub confidence: f64,
33}
34
35impl ProcessHistory {
36    /// Create new process history
37    pub fn new(pid: u32, name: String, initial_memory_mb: f64) -> Self {
38        Self {
39            pid,
40            name,
41            samples: vec![MemorySample {
42                timestamp: current_timestamp(),
43                memory_mb: initial_memory_mb,
44                cpu_percent: 0.0,
45            }],
46            start_memory_mb: initial_memory_mb,
47            current_memory_mb: initial_memory_mb,
48            peak_memory_mb: initial_memory_mb,
49            growth_rate_mb_per_hour: 0.0,
50            is_likely_leak: false,
51            confidence: 0.0,
52        }
53    }
54
55    /// Add a memory sample
56    pub fn add_sample(&mut self, memory_mb: f64, cpu_percent: f32) {
57        let sample = MemorySample {
58            timestamp: current_timestamp(),
59            memory_mb,
60            cpu_percent,
61        };
62
63        self.samples.push(sample);
64        self.current_memory_mb = memory_mb;
65
66        if memory_mb > self.peak_memory_mb {
67            self.peak_memory_mb = memory_mb;
68        }
69
70        // Keep only last 100 samples
71        if self.samples.len() > 100 {
72            self.samples.remove(0);
73        }
74
75        self.analyze();
76    }
77
78    /// Analyze memory pattern for leaks
79    fn analyze(&mut self) {
80        if self.samples.len() < 5 {
81            return;
82        }
83
84        // Calculate growth rate using linear regression
85        let n = self.samples.len() as f64;
86        let mut sum_x = 0.0;
87        let mut sum_y = 0.0;
88        let mut sum_xy = 0.0;
89        let mut sum_xx = 0.0;
90
91        let base_time = self.samples[0].timestamp;
92
93        for sample in &self.samples {
94            let x = (sample.timestamp - base_time) as f64 / 3600.0; // Hours
95            let y = sample.memory_mb;
96
97            sum_x += x;
98            sum_y += y;
99            sum_xy += x * y;
100            sum_xx += x * x;
101        }
102
103        let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
104        self.growth_rate_mb_per_hour = slope;
105
106        // Calculate R-squared for confidence
107        let mean_y = sum_y / n;
108        let mut ss_tot = 0.0;
109        let mut ss_res = 0.0;
110
111        for sample in &self.samples {
112            let x = (sample.timestamp - base_time) as f64 / 3600.0;
113            let y = sample.memory_mb;
114            let predicted = self.samples[0].memory_mb + slope * x;
115
116            ss_tot += (y - mean_y).powi(2);
117            ss_res += (y - predicted).powi(2);
118        }
119
120        let r_squared = if ss_tot > 0.0 {
121            1.0 - (ss_res / ss_tot)
122        } else {
123            0.0
124        };
125
126        self.confidence = r_squared.max(0.0).min(1.0);
127
128        // Determine if likely leak
129        // Criteria: consistent growth (high R²), significant rate, not just startup
130        let memory_doubled = self.current_memory_mb > self.start_memory_mb * 2.0;
131        let significant_growth = self.growth_rate_mb_per_hour > 10.0; // >10 MB/hour
132        let consistent = self.confidence > 0.7;
133        let enough_samples = self.samples.len() >= 10;
134
135        self.is_likely_leak = (memory_doubled || significant_growth) && consistent && enough_samples;
136    }
137
138    /// Get memory growth percentage
139    pub fn growth_percent(&self) -> f64 {
140        if self.start_memory_mb > 0.0 {
141            ((self.current_memory_mb - self.start_memory_mb) / self.start_memory_mb) * 100.0
142        } else {
143            0.0
144        }
145    }
146
147    /// Get severity level (0-3)
148    pub fn severity(&self) -> u8 {
149        if !self.is_likely_leak {
150            return 0;
151        }
152
153        if self.growth_rate_mb_per_hour > 100.0 || self.growth_percent() > 500.0 {
154            3 // Critical
155        } else if self.growth_rate_mb_per_hour > 50.0 || self.growth_percent() > 200.0 {
156            2 // High
157        } else {
158            1 // Medium
159        }
160    }
161}
162
163/// Leak detection result
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct LeakReport {
166    pub process_name: String,
167    pub pid: u32,
168    pub current_memory_mb: f64,
169    pub start_memory_mb: f64,
170    pub growth_rate_mb_per_hour: f64,
171    pub growth_percent: f64,
172    pub confidence: f64,
173    pub severity: u8,
174    pub recommendation: String,
175}
176
177/// Memory leak detector
178pub struct LeakDetector {
179    system: System,
180    process_history: HashMap<u32, ProcessHistory>,
181    monitoring_duration_secs: u64,
182    sample_interval_secs: u64,
183    last_sample: std::time::Instant,
184}
185
186impl LeakDetector {
187    pub fn new() -> Self {
188        let mut system = System::new_all();
189        system.refresh_processes(ProcessesToUpdate::All, true);
190
191        Self {
192            system,
193            process_history: HashMap::new(),
194            monitoring_duration_secs: 0,
195            sample_interval_secs: 30,
196            last_sample: std::time::Instant::now(),
197        }
198    }
199
200    /// Set sample interval
201    pub fn set_sample_interval(&mut self, secs: u64) {
202        self.sample_interval_secs = secs;
203    }
204
205    /// Take a sample of all processes
206    pub fn sample(&mut self) {
207        self.system.refresh_processes(ProcessesToUpdate::All, true);
208
209        let mut seen_pids = Vec::new();
210
211        for (pid, process) in self.system.processes() {
212            let pid_u32 = pid.as_u32();
213            let name = process.name().to_string_lossy().to_string();
214            let memory_mb = process.memory() as f64 / (1024.0 * 1024.0);
215            let cpu_percent = process.cpu_usage();
216
217            seen_pids.push(pid_u32);
218
219            if let Some(history) = self.process_history.get_mut(&pid_u32) {
220                history.add_sample(memory_mb, cpu_percent);
221            } else {
222                // Only track processes using >50MB
223                if memory_mb > 50.0 {
224                    self.process_history.insert(
225                        pid_u32,
226                        ProcessHistory::new(pid_u32, name, memory_mb),
227                    );
228                }
229            }
230        }
231
232        // Remove dead processes
233        self.process_history.retain(|pid, _| seen_pids.contains(pid));
234
235        self.last_sample = std::time::Instant::now();
236        self.monitoring_duration_secs += self.sample_interval_secs;
237    }
238
239    /// Check if enough time has passed for next sample
240    pub fn should_sample(&self) -> bool {
241        self.last_sample.elapsed().as_secs() >= self.sample_interval_secs
242    }
243
244    /// Get processes with likely memory leaks
245    pub fn get_leaks(&self) -> Vec<LeakReport> {
246        let mut leaks: Vec<LeakReport> = self
247            .process_history
248            .values()
249            .filter(|h| h.is_likely_leak)
250            .map(|h| LeakReport {
251                process_name: h.name.clone(),
252                pid: h.pid,
253                current_memory_mb: h.current_memory_mb,
254                start_memory_mb: h.start_memory_mb,
255                growth_rate_mb_per_hour: h.growth_rate_mb_per_hour,
256                growth_percent: h.growth_percent(),
257                confidence: h.confidence,
258                severity: h.severity(),
259                recommendation: self.get_recommendation(h),
260            })
261            .collect();
262
263        // Sort by severity (highest first)
264        leaks.sort_by(|a, b| b.severity.cmp(&a.severity));
265
266        leaks
267    }
268
269    /// Get all monitored processes sorted by memory growth
270    pub fn get_all_monitored(&self) -> Vec<&ProcessHistory> {
271        let mut procs: Vec<_> = self.process_history.values().collect();
272        procs.sort_by(|a, b| {
273            b.growth_rate_mb_per_hour
274                .partial_cmp(&a.growth_rate_mb_per_hour)
275                .unwrap()
276        });
277        procs
278    }
279
280    /// Get top memory growing processes
281    pub fn get_top_growing(&self, count: usize) -> Vec<&ProcessHistory> {
282        let mut procs: Vec<_> = self
283            .process_history
284            .values()
285            .filter(|h| h.samples.len() >= 3 && h.growth_rate_mb_per_hour > 0.0)
286            .collect();
287
288        procs.sort_by(|a, b| {
289            b.growth_rate_mb_per_hour
290                .partial_cmp(&a.growth_rate_mb_per_hour)
291                .unwrap()
292        });
293
294        procs.into_iter().take(count).collect()
295    }
296
297    /// Get recommendation for a process
298    fn get_recommendation(&self, history: &ProcessHistory) -> String {
299        match history.severity() {
300            3 => format!(
301                "CRITICAL: {} is growing at {:.0} MB/hour. Restart immediately!",
302                history.name, history.growth_rate_mb_per_hour
303            ),
304            2 => format!(
305                "HIGH: {} has grown {:.0}%. Consider restarting soon.",
306                history.name, history.growth_percent()
307            ),
308            1 => format!(
309                "MEDIUM: {} shows gradual memory growth. Monitor closely.",
310                history.name
311            ),
312            _ => String::from("No action needed"),
313        }
314    }
315
316    /// Get monitoring stats
317    pub fn stats(&self) -> LeakDetectorStats {
318        let total_processes = self.process_history.len();
319        let leaking = self.process_history.values().filter(|h| h.is_likely_leak).count();
320        let growing = self
321            .process_history
322            .values()
323            .filter(|h| h.growth_rate_mb_per_hour > 1.0)
324            .count();
325
326        LeakDetectorStats {
327            total_processes,
328            leaking_processes: leaking,
329            growing_processes: growing,
330            monitoring_duration_secs: self.monitoring_duration_secs,
331            sample_count: self
332                .process_history
333                .values()
334                .map(|h| h.samples.len())
335                .max()
336                .unwrap_or(0),
337        }
338    }
339
340    /// Print leak detection summary
341    pub fn print_summary(&self) {
342        let stats = self.stats();
343        let leaks = self.get_leaks();
344        let top_growing = self.get_top_growing(5);
345
346        println!("\n🔍 Memory Leak Detection\n");
347        println!(
348            "Monitoring {} processes for {} minutes ({} samples)\n",
349            stats.total_processes,
350            stats.monitoring_duration_secs / 60,
351            stats.sample_count
352        );
353
354        if !leaks.is_empty() {
355            println!("⚠️  DETECTED MEMORY LEAKS:\n");
356            println!("┌──────────────────────┬───────────┬───────────┬──────────┬──────────┐");
357            println!("│ Process              │ Current   │ Growth/hr │ Growth % │ Severity │");
358            println!("├──────────────────────┼───────────┼───────────┼──────────┼──────────┤");
359
360            for leak in &leaks {
361                let severity_icon = match leak.severity {
362                    3 => "🔴 Crit",
363                    2 => "🟠 High",
364                    1 => "🟡 Med",
365                    _ => "🟢 Low",
366                };
367
368                println!(
369                    "│ {:20} │ {:>7.0} MB │ {:>+7.0} MB │ {:>+7.0}% │ {:8} │",
370                    truncate(&leak.process_name, 20),
371                    leak.current_memory_mb,
372                    leak.growth_rate_mb_per_hour,
373                    leak.growth_percent,
374                    severity_icon
375                );
376            }
377
378            println!("└──────────────────────┴───────────┴───────────┴──────────┴──────────┘");
379
380            println!("\n💡 Recommendations:");
381            for leak in leaks.iter().take(3) {
382                println!("   • {}", leak.recommendation);
383            }
384        } else if !top_growing.is_empty() {
385            println!("No confirmed leaks detected, but monitoring these growing processes:\n");
386            println!("┌──────────────────────┬───────────┬───────────┬──────────────┐");
387            println!("│ Process              │ Current   │ Growth/hr │ Confidence   │");
388            println!("├──────────────────────┼───────────┼───────────┼──────────────┤");
389
390            for proc in &top_growing {
391                println!(
392                    "│ {:20} │ {:>7.0} MB │ {:>+7.1} MB │ {:>10.0}%  │",
393                    truncate(&proc.name, 20),
394                    proc.current_memory_mb,
395                    proc.growth_rate_mb_per_hour,
396                    proc.confidence * 100.0
397                );
398            }
399
400            println!("└──────────────────────┴───────────┴───────────┴──────────────┘");
401        } else {
402            println!("✅ No memory leaks or unusual growth patterns detected.");
403        }
404
405        println!(
406            "\nTip: Run with longer duration for better detection accuracy."
407        );
408    }
409}
410
411impl Default for LeakDetector {
412    fn default() -> Self {
413        Self::new()
414    }
415}
416
417/// Leak detector statistics
418#[derive(Debug, Clone)]
419pub struct LeakDetectorStats {
420    pub total_processes: usize,
421    pub leaking_processes: usize,
422    pub growing_processes: usize,
423    pub monitoring_duration_secs: u64,
424    pub sample_count: usize,
425}
426
427fn current_timestamp() -> u64 {
428    std::time::SystemTime::now()
429        .duration_since(std::time::UNIX_EPOCH)
430        .unwrap()
431        .as_secs()
432}
433
434fn truncate(s: &str, max: usize) -> String {
435    if s.len() <= max {
436        format!("{:width$}", s, width = max)
437    } else {
438        format!("{}...", &s[..max - 3])
439    }
440}