1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use sysinfo::{System, ProcessesToUpdate, Pid};
12
13#[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#[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 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 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 if self.samples.len() > 100 {
72 self.samples.remove(0);
73 }
74
75 self.analyze();
76 }
77
78 fn analyze(&mut self) {
80 if self.samples.len() < 5 {
81 return;
82 }
83
84 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; 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 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 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; 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 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 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 } else if self.growth_rate_mb_per_hour > 50.0 || self.growth_percent() > 200.0 {
156 2 } else {
158 1 }
160 }
161}
162
163#[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
177pub 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 pub fn set_sample_interval(&mut self, secs: u64) {
202 self.sample_interval_secs = secs;
203 }
204
205 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 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 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 pub fn should_sample(&self) -> bool {
241 self.last_sample.elapsed().as_secs() >= self.sample_interval_secs
242 }
243
244 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 leaks.sort_by(|a, b| b.severity.cmp(&a.severity));
265
266 leaks
267 }
268
269 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 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 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 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 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#[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}