1use super::allocation::{AllocationRecord, AllocationStats, AllocationTracker};
7use std::collections::HashMap;
8use std::time::{Duration, Instant};
9
10pub struct LeakDetector {
16 old_threshold: Duration,
18 min_size: usize,
20}
21
22impl LeakDetector {
23 pub fn new() -> Self {
25 Self {
26 old_threshold: Duration::from_secs(60),
27 min_size: 64,
28 }
29 }
30
31 pub fn with_threshold(threshold: Duration) -> Self {
33 Self {
34 old_threshold: threshold,
35 min_size: 64,
36 }
37 }
38
39 pub fn set_old_threshold(&mut self, threshold: Duration) {
41 self.old_threshold = threshold;
42 }
43
44 pub fn set_min_size(&mut self, size: usize) {
46 self.min_size = size;
47 }
48
49 pub fn start(&self) -> LeakDetectorGuard<'_> {
51 super::enable_profiling();
52 LeakDetectorGuard {
53 detector: self,
54 start_time: Instant::now(),
55 initial_stats: super::GLOBAL_TRACKER.stats(),
56 }
57 }
58
59 pub fn analyze(&self, tracker: &AllocationTracker) -> LeakReport {
61 let stats = tracker.stats();
62 let old_allocations = tracker.old_allocations(self.old_threshold);
63
64 let mut by_size: HashMap<usize, Vec<&AllocationRecord>> = HashMap::new();
66 for alloc in &old_allocations {
67 by_size.entry(alloc.size).or_default().push(alloc);
68 }
69
70 let mut potential_leaks = Vec::new();
72
73 for (size, allocs) in &by_size {
75 if allocs.len() >= 3 {
76 potential_leaks.push(PotentialLeak {
77 pattern: LeakPattern::RepeatedSize {
78 size: *size,
79 count: allocs.len(),
80 },
81 severity: if allocs.len() > 10 {
82 LeakSeverity::High
83 } else if allocs.len() > 5 {
84 LeakSeverity::Medium
85 } else {
86 LeakSeverity::Low
87 },
88 total_bytes: size * allocs.len(),
89 oldest_age: allocs.iter().map(|a| a.age()).max().unwrap_or_default(),
90 #[cfg(debug_assertions)]
91 sample_backtrace: allocs.first().and_then(|a| a.backtrace.clone()),
92 #[cfg(not(debug_assertions))]
93 sample_backtrace: None,
94 });
95 }
96 }
97
98 for alloc in &old_allocations {
100 if alloc.age() > self.old_threshold * 5 {
101 potential_leaks.push(PotentialLeak {
102 pattern: LeakPattern::VeryOld {
103 age: alloc.age(),
104 size: alloc.size,
105 },
106 severity: LeakSeverity::High,
107 total_bytes: alloc.size,
108 oldest_age: alloc.age(),
109 #[cfg(debug_assertions)]
110 sample_backtrace: alloc.backtrace.clone(),
111 #[cfg(not(debug_assertions))]
112 sample_backtrace: None,
113 });
114 }
115 }
116
117 if stats.net_allocations() > 100 {
119 potential_leaks.push(PotentialLeak {
120 pattern: LeakPattern::GrowingCount {
121 net_allocations: stats.net_allocations(),
122 },
123 severity: if stats.net_allocations() > 1000 {
124 LeakSeverity::High
125 } else if stats.net_allocations() > 500 {
126 LeakSeverity::Medium
127 } else {
128 LeakSeverity::Low
129 },
130 total_bytes: stats.current_bytes,
131 oldest_age: Duration::default(),
132 sample_backtrace: None,
133 });
134 }
135
136 potential_leaks.sort_by(|a, b| b.severity.cmp(&a.severity));
138
139 LeakReport {
140 session_duration: stats.uptime,
141 total_allocations: stats.total_allocations,
142 total_deallocations: stats.total_deallocations,
143 current_bytes: stats.current_bytes,
144 peak_bytes: stats.peak_bytes,
145 old_allocations_count: old_allocations.len(),
146 potential_leaks,
147 }
148 }
149
150 pub fn finish(&self) -> LeakReport {
152 self.analyze(&super::GLOBAL_TRACKER)
153 }
154}
155
156impl Default for LeakDetector {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162pub struct LeakDetectorGuard<'a> {
168 detector: &'a LeakDetector,
169 start_time: Instant,
170 initial_stats: AllocationStats,
171}
172
173impl<'a> LeakDetectorGuard<'a> {
174 pub fn current_report(&self) -> LeakReport {
176 self.detector.analyze(&super::GLOBAL_TRACKER)
177 }
178
179 pub fn delta(&self) -> AllocationDelta {
181 let current = super::GLOBAL_TRACKER.stats();
182 AllocationDelta {
183 allocations_delta: current.total_allocations as i64
184 - self.initial_stats.total_allocations as i64,
185 deallocations_delta: current.total_deallocations as i64
186 - self.initial_stats.total_deallocations as i64,
187 bytes_delta: current.current_bytes as i64 - self.initial_stats.current_bytes as i64,
188 duration: self.start_time.elapsed(),
189 }
190 }
191}
192
193impl Drop for LeakDetectorGuard<'_> {
194 fn drop(&mut self) {
195 super::disable_profiling();
196 }
197}
198
199#[derive(Debug, Clone)]
205pub struct LeakReport {
206 pub session_duration: Duration,
208 pub total_allocations: u64,
210 pub total_deallocations: u64,
212 pub current_bytes: usize,
214 pub peak_bytes: usize,
216 pub old_allocations_count: usize,
218 pub potential_leaks: Vec<PotentialLeak>,
220}
221
222impl LeakReport {
223 pub fn has_leaks(&self) -> bool {
225 !self.potential_leaks.is_empty()
226 }
227
228 pub fn has_high_severity_leaks(&self) -> bool {
230 self.potential_leaks
231 .iter()
232 .any(|l| l.severity == LeakSeverity::High)
233 }
234
235 pub fn total_leaked_bytes(&self) -> usize {
237 self.potential_leaks.iter().map(|l| l.total_bytes).sum()
238 }
239
240 pub fn summary(&self) -> String {
242 let mut s = String::new();
243
244 if self.potential_leaks.is_empty() {
245 s.push_str(" No potential leaks detected\n");
246 return s;
247 }
248
249 for (i, leak) in self.potential_leaks.iter().enumerate() {
250 s.push_str(&format!(
251 " {}. [{:?}] {}\n",
252 i + 1,
253 leak.severity,
254 leak.pattern.description()
255 ));
256 s.push_str(&format!(
257 " Total bytes: {} ({:.2} KB)\n",
258 leak.total_bytes,
259 leak.total_bytes as f64 / 1024.0
260 ));
261
262 if let Some(bt) = &leak.sample_backtrace {
263 s.push_str(" Sample backtrace:\n");
264 for line in bt.lines().take(10) {
266 s.push_str(&format!(" {}\n", line));
267 }
268 }
269 }
270
271 s
272 }
273}
274
275impl std::fmt::Display for LeakReport {
276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 writeln!(f, "=== Leak Detection Report ===")?;
278 writeln!(f, "Session duration: {:?}", self.session_duration)?;
279 writeln!(
280 f,
281 "Allocations: {} / Deallocations: {}",
282 self.total_allocations, self.total_deallocations
283 )?;
284 writeln!(
285 f,
286 "Current bytes: {} / Peak bytes: {}",
287 self.current_bytes, self.peak_bytes
288 )?;
289 writeln!(f, "Old allocations: {}", self.old_allocations_count)?;
290 writeln!(f)?;
291
292 if self.has_leaks() {
293 writeln!(
294 f,
295 "⚠️ {} potential leak(s) detected:",
296 self.potential_leaks.len()
297 )?;
298 write!(f, "{}", self.summary())?;
299 } else {
300 writeln!(f, "✅ No potential leaks detected")?;
301 }
302
303 Ok(())
304 }
305}
306
307#[derive(Debug, Clone)]
313pub struct PotentialLeak {
314 pub pattern: LeakPattern,
316 pub severity: LeakSeverity,
318 pub total_bytes: usize,
320 pub oldest_age: Duration,
322 pub sample_backtrace: Option<String>,
324}
325
326#[derive(Debug, Clone)]
332pub enum LeakPattern {
333 RepeatedSize { size: usize, count: usize },
335 VeryOld { age: Duration, size: usize },
337 GrowingCount { net_allocations: i64 },
339 LargeOld { size: usize, age: Duration },
341}
342
343impl LeakPattern {
344 pub fn description(&self) -> String {
346 match self {
347 LeakPattern::RepeatedSize { size, count } => {
348 format!("{} allocations of {} bytes each", count, size)
349 }
350 LeakPattern::VeryOld { age, size } => {
351 format!("{} byte allocation held for {:?}", size, age)
352 }
353 LeakPattern::GrowingCount { net_allocations } => {
354 format!("{} net allocations (allocs - deallocs)", net_allocations)
355 }
356 LeakPattern::LargeOld { size, age } => {
357 format!("Large {} byte allocation held for {:?}", size, age)
358 }
359 }
360 }
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
369pub enum LeakSeverity {
370 Low,
372 Medium,
374 High,
376}
377
378#[derive(Debug, Clone)]
384pub struct AllocationDelta {
385 pub allocations_delta: i64,
387 pub deallocations_delta: i64,
389 pub bytes_delta: i64,
391 pub duration: Duration,
393}
394
395impl AllocationDelta {
396 pub fn memory_grew(&self) -> bool {
398 self.bytes_delta > 0
399 }
400
401 pub fn allocation_rate(&self) -> f64 {
403 if self.duration.as_secs_f64() > 0.0 {
404 self.allocations_delta as f64 / self.duration.as_secs_f64()
405 } else {
406 0.0
407 }
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_leak_detector_new() {
417 let detector = LeakDetector::new();
418 assert_eq!(detector.old_threshold, Duration::from_secs(60));
419 assert_eq!(detector.min_size, 64);
420 }
421
422 #[test]
423 fn test_leak_pattern_description() {
424 let pattern = LeakPattern::RepeatedSize {
425 size: 1024,
426 count: 10,
427 };
428 assert!(pattern.description().contains("10 allocations"));
429 assert!(pattern.description().contains("1024 bytes"));
430
431 let pattern = LeakPattern::VeryOld {
432 age: Duration::from_secs(300),
433 size: 2048,
434 };
435 assert!(pattern.description().contains("2048 byte"));
436 }
437
438 #[test]
439 fn test_leak_severity_ordering() {
440 assert!(LeakSeverity::High > LeakSeverity::Medium);
441 assert!(LeakSeverity::Medium > LeakSeverity::Low);
442 }
443
444 #[test]
445 fn test_leak_report_empty() {
446 let report = LeakReport {
447 session_duration: Duration::from_secs(10),
448 total_allocations: 100,
449 total_deallocations: 100,
450 current_bytes: 0,
451 peak_bytes: 1000,
452 old_allocations_count: 0,
453 potential_leaks: vec![],
454 };
455
456 assert!(!report.has_leaks());
457 assert!(!report.has_high_severity_leaks());
458 assert_eq!(report.total_leaked_bytes(), 0);
459 }
460
461 #[test]
462 fn test_leak_report_with_leaks() {
463 let report = LeakReport {
464 session_duration: Duration::from_secs(10),
465 total_allocations: 100,
466 total_deallocations: 50,
467 current_bytes: 5000,
468 peak_bytes: 6000,
469 old_allocations_count: 5,
470 potential_leaks: vec![
471 PotentialLeak {
472 pattern: LeakPattern::RepeatedSize {
473 size: 1024,
474 count: 5,
475 },
476 severity: LeakSeverity::Medium,
477 total_bytes: 5120,
478 oldest_age: Duration::from_secs(120),
479 sample_backtrace: None,
480 },
481 PotentialLeak {
482 pattern: LeakPattern::GrowingCount {
483 net_allocations: 50,
484 },
485 severity: LeakSeverity::High,
486 total_bytes: 5000,
487 oldest_age: Duration::default(),
488 sample_backtrace: None,
489 },
490 ],
491 };
492
493 assert!(report.has_leaks());
494 assert!(report.has_high_severity_leaks());
495 assert_eq!(report.total_leaked_bytes(), 10120);
496 }
497
498 #[test]
499 fn test_allocation_delta() {
500 let delta = AllocationDelta {
501 allocations_delta: 100,
502 deallocations_delta: 50,
503 bytes_delta: 5000,
504 duration: Duration::from_secs(10),
505 };
506
507 assert!(delta.memory_grew());
508 assert_eq!(delta.allocation_rate(), 10.0);
509 }
510}
511