prax_query/profiling/
leak_detector.rs

1//! Memory leak detection.
2//!
3//! Provides utilities for identifying potential memory leaks through
4//! allocation tracking, age analysis, and pattern detection.
5
6use super::allocation::{AllocationRecord, AllocationStats, AllocationTracker};
7use std::collections::HashMap;
8use std::time::{Duration, Instant};
9
10// ============================================================================
11// Leak Detector
12// ============================================================================
13
14/// Memory leak detector.
15pub struct LeakDetector {
16    /// Threshold age for considering an allocation "old".
17    old_threshold: Duration,
18    /// Minimum size to track.
19    min_size: usize,
20}
21
22impl LeakDetector {
23    /// Create a new leak detector with default settings.
24    pub fn new() -> Self {
25        Self {
26            old_threshold: Duration::from_secs(60),
27            min_size: 64,
28        }
29    }
30
31    /// Create a leak detector with custom threshold.
32    pub fn with_threshold(threshold: Duration) -> Self {
33        Self {
34            old_threshold: threshold,
35            min_size: 64,
36        }
37    }
38
39    /// Set the age threshold for old allocations.
40    pub fn set_old_threshold(&mut self, threshold: Duration) {
41        self.old_threshold = threshold;
42    }
43
44    /// Set minimum allocation size to track.
45    pub fn set_min_size(&mut self, size: usize) {
46        self.min_size = size;
47    }
48
49    /// Start a leak detection session.
50    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    /// Analyze allocations for potential leaks.
60    pub fn analyze(&self, tracker: &AllocationTracker) -> LeakReport {
61        let stats = tracker.stats();
62        let old_allocations = tracker.old_allocations(self.old_threshold);
63
64        // Group by size for pattern detection
65        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        // Identify potential leaks
71        let mut potential_leaks = Vec::new();
72
73        // Check for many allocations of same size (common leak pattern)
74        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        // Check for very old allocations
99        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        // Check for growing allocation count
118        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        // Sort by severity
137        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    /// Finish a leak detection session and return the report.
151    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
162// ============================================================================
163// Leak Detector Guard
164// ============================================================================
165
166/// RAII guard for leak detection sessions.
167pub struct LeakDetectorGuard<'a> {
168    detector: &'a LeakDetector,
169    start_time: Instant,
170    initial_stats: AllocationStats,
171}
172
173impl<'a> LeakDetectorGuard<'a> {
174    /// Get the current leak report.
175    pub fn current_report(&self) -> LeakReport {
176        self.detector.analyze(&super::GLOBAL_TRACKER)
177    }
178
179    /// Get the delta from start.
180    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// ============================================================================
200// Leak Report
201// ============================================================================
202
203/// Report from leak detection analysis.
204#[derive(Debug, Clone)]
205pub struct LeakReport {
206    /// Duration of the detection session.
207    pub session_duration: Duration,
208    /// Total allocations during session.
209    pub total_allocations: u64,
210    /// Total deallocations during session.
211    pub total_deallocations: u64,
212    /// Current bytes allocated.
213    pub current_bytes: usize,
214    /// Peak bytes allocated.
215    pub peak_bytes: usize,
216    /// Number of old allocations found.
217    pub old_allocations_count: usize,
218    /// Identified potential leaks.
219    pub potential_leaks: Vec<PotentialLeak>,
220}
221
222impl LeakReport {
223    /// Check if any potential leaks were detected.
224    pub fn has_leaks(&self) -> bool {
225        !self.potential_leaks.is_empty()
226    }
227
228    /// Check if any high-severity leaks were detected.
229    pub fn has_high_severity_leaks(&self) -> bool {
230        self.potential_leaks
231            .iter()
232            .any(|l| l.severity == LeakSeverity::High)
233    }
234
235    /// Get total bytes potentially leaked.
236    pub fn total_leaked_bytes(&self) -> usize {
237        self.potential_leaks.iter().map(|l| l.total_bytes).sum()
238    }
239
240    /// Generate a summary string.
241    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                // Show first few frames
265                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// ============================================================================
308// Potential Leak
309// ============================================================================
310
311/// A potential memory leak.
312#[derive(Debug, Clone)]
313pub struct PotentialLeak {
314    /// Pattern that indicates this leak.
315    pub pattern: LeakPattern,
316    /// Severity assessment.
317    pub severity: LeakSeverity,
318    /// Total bytes involved.
319    pub total_bytes: usize,
320    /// Age of oldest allocation.
321    pub oldest_age: Duration,
322    /// Sample backtrace (if available).
323    pub sample_backtrace: Option<String>,
324}
325
326// ============================================================================
327// Leak Pattern
328// ============================================================================
329
330/// Pattern indicating a potential leak.
331#[derive(Debug, Clone)]
332pub enum LeakPattern {
333    /// Many allocations of the same size.
334    RepeatedSize { size: usize, count: usize },
335    /// Very old allocation that was never freed.
336    VeryOld { age: Duration, size: usize },
337    /// Growing count of allocations over time.
338    GrowingCount { net_allocations: i64 },
339    /// Large single allocation held too long.
340    LargeOld { size: usize, age: Duration },
341}
342
343impl LeakPattern {
344    /// Get a description of the pattern.
345    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// ============================================================================
364// Leak Severity
365// ============================================================================
366
367/// Severity of a potential leak.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
369pub enum LeakSeverity {
370    /// Low severity - may be intentional caching.
371    Low,
372    /// Medium severity - warrants investigation.
373    Medium,
374    /// High severity - likely a leak.
375    High,
376}
377
378// ============================================================================
379// Allocation Delta
380// ============================================================================
381
382/// Change in allocations over time.
383#[derive(Debug, Clone)]
384pub struct AllocationDelta {
385    /// Change in allocation count.
386    pub allocations_delta: i64,
387    /// Change in deallocation count.
388    pub deallocations_delta: i64,
389    /// Change in bytes allocated.
390    pub bytes_delta: i64,
391    /// Duration of the delta period.
392    pub duration: Duration,
393}
394
395impl AllocationDelta {
396    /// Check if memory grew.
397    pub fn memory_grew(&self) -> bool {
398        self.bytes_delta > 0
399    }
400
401    /// Get allocation rate per second.
402    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