use super::allocation::{AllocationRecord, AllocationStats, AllocationTracker};
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub struct LeakDetector {
old_threshold: Duration,
min_size: usize,
}
impl LeakDetector {
pub fn new() -> Self {
Self {
old_threshold: Duration::from_secs(60),
min_size: 64,
}
}
pub fn with_threshold(threshold: Duration) -> Self {
Self {
old_threshold: threshold,
min_size: 64,
}
}
pub fn set_old_threshold(&mut self, threshold: Duration) {
self.old_threshold = threshold;
}
pub fn set_min_size(&mut self, size: usize) {
self.min_size = size;
}
pub fn start(&self) -> LeakDetectorGuard<'_> {
super::enable_profiling();
LeakDetectorGuard {
detector: self,
start_time: Instant::now(),
initial_stats: super::GLOBAL_TRACKER.stats(),
}
}
pub fn analyze(&self, tracker: &AllocationTracker) -> LeakReport {
let stats = tracker.stats();
let old_allocations = tracker.old_allocations(self.old_threshold);
let mut by_size: HashMap<usize, Vec<&AllocationRecord>> = HashMap::new();
for alloc in &old_allocations {
by_size.entry(alloc.size).or_default().push(alloc);
}
let mut potential_leaks = Vec::new();
for (size, allocs) in &by_size {
if allocs.len() >= 3 {
potential_leaks.push(PotentialLeak {
pattern: LeakPattern::RepeatedSize {
size: *size,
count: allocs.len(),
},
severity: if allocs.len() > 10 {
LeakSeverity::High
} else if allocs.len() > 5 {
LeakSeverity::Medium
} else {
LeakSeverity::Low
},
total_bytes: size * allocs.len(),
oldest_age: allocs.iter().map(|a| a.age()).max().unwrap_or_default(),
#[cfg(debug_assertions)]
sample_backtrace: allocs.first().and_then(|a| a.backtrace.clone()),
#[cfg(not(debug_assertions))]
sample_backtrace: None,
});
}
}
for alloc in &old_allocations {
if alloc.age() > self.old_threshold * 5 {
potential_leaks.push(PotentialLeak {
pattern: LeakPattern::VeryOld {
age: alloc.age(),
size: alloc.size,
},
severity: LeakSeverity::High,
total_bytes: alloc.size,
oldest_age: alloc.age(),
#[cfg(debug_assertions)]
sample_backtrace: alloc.backtrace.clone(),
#[cfg(not(debug_assertions))]
sample_backtrace: None,
});
}
}
if stats.net_allocations() > 100 {
potential_leaks.push(PotentialLeak {
pattern: LeakPattern::GrowingCount {
net_allocations: stats.net_allocations(),
},
severity: if stats.net_allocations() > 1000 {
LeakSeverity::High
} else if stats.net_allocations() > 500 {
LeakSeverity::Medium
} else {
LeakSeverity::Low
},
total_bytes: stats.current_bytes,
oldest_age: Duration::default(),
sample_backtrace: None,
});
}
potential_leaks.sort_by_key(|leak| std::cmp::Reverse(leak.severity));
LeakReport {
session_duration: stats.uptime,
total_allocations: stats.total_allocations,
total_deallocations: stats.total_deallocations,
current_bytes: stats.current_bytes,
peak_bytes: stats.peak_bytes,
old_allocations_count: old_allocations.len(),
potential_leaks,
}
}
pub fn finish(&self) -> LeakReport {
self.analyze(&super::GLOBAL_TRACKER)
}
}
impl Default for LeakDetector {
fn default() -> Self {
Self::new()
}
}
pub struct LeakDetectorGuard<'a> {
detector: &'a LeakDetector,
start_time: Instant,
initial_stats: AllocationStats,
}
impl<'a> LeakDetectorGuard<'a> {
pub fn current_report(&self) -> LeakReport {
self.detector.analyze(&super::GLOBAL_TRACKER)
}
pub fn delta(&self) -> AllocationDelta {
let current = super::GLOBAL_TRACKER.stats();
AllocationDelta {
allocations_delta: current.total_allocations as i64
- self.initial_stats.total_allocations as i64,
deallocations_delta: current.total_deallocations as i64
- self.initial_stats.total_deallocations as i64,
bytes_delta: current.current_bytes as i64 - self.initial_stats.current_bytes as i64,
duration: self.start_time.elapsed(),
}
}
}
impl Drop for LeakDetectorGuard<'_> {
fn drop(&mut self) {
super::disable_profiling();
}
}
#[derive(Debug, Clone)]
pub struct LeakReport {
pub session_duration: Duration,
pub total_allocations: u64,
pub total_deallocations: u64,
pub current_bytes: usize,
pub peak_bytes: usize,
pub old_allocations_count: usize,
pub potential_leaks: Vec<PotentialLeak>,
}
impl LeakReport {
pub fn has_leaks(&self) -> bool {
!self.potential_leaks.is_empty()
}
pub fn has_high_severity_leaks(&self) -> bool {
self.potential_leaks
.iter()
.any(|l| l.severity == LeakSeverity::High)
}
pub fn total_leaked_bytes(&self) -> usize {
self.potential_leaks.iter().map(|l| l.total_bytes).sum()
}
pub fn summary(&self) -> String {
let mut s = String::new();
if self.potential_leaks.is_empty() {
s.push_str(" No potential leaks detected\n");
return s;
}
for (i, leak) in self.potential_leaks.iter().enumerate() {
s.push_str(&format!(
" {}. [{:?}] {}\n",
i + 1,
leak.severity,
leak.pattern.description()
));
s.push_str(&format!(
" Total bytes: {} ({:.2} KB)\n",
leak.total_bytes,
leak.total_bytes as f64 / 1024.0
));
if let Some(bt) = &leak.sample_backtrace {
s.push_str(" Sample backtrace:\n");
for line in bt.lines().take(10) {
s.push_str(&format!(" {}\n", line));
}
}
}
s
}
}
impl std::fmt::Display for LeakReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Leak Detection Report ===")?;
writeln!(f, "Session duration: {:?}", self.session_duration)?;
writeln!(
f,
"Allocations: {} / Deallocations: {}",
self.total_allocations, self.total_deallocations
)?;
writeln!(
f,
"Current bytes: {} / Peak bytes: {}",
self.current_bytes, self.peak_bytes
)?;
writeln!(f, "Old allocations: {}", self.old_allocations_count)?;
writeln!(f)?;
if self.has_leaks() {
writeln!(
f,
"⚠️ {} potential leak(s) detected:",
self.potential_leaks.len()
)?;
write!(f, "{}", self.summary())?;
} else {
writeln!(f, "✅ No potential leaks detected")?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PotentialLeak {
pub pattern: LeakPattern,
pub severity: LeakSeverity,
pub total_bytes: usize,
pub oldest_age: Duration,
pub sample_backtrace: Option<String>,
}
#[derive(Debug, Clone)]
pub enum LeakPattern {
RepeatedSize { size: usize, count: usize },
VeryOld { age: Duration, size: usize },
GrowingCount { net_allocations: i64 },
LargeOld { size: usize, age: Duration },
}
impl LeakPattern {
pub fn description(&self) -> String {
match self {
LeakPattern::RepeatedSize { size, count } => {
format!("{} allocations of {} bytes each", count, size)
}
LeakPattern::VeryOld { age, size } => {
format!("{} byte allocation held for {:?}", size, age)
}
LeakPattern::GrowingCount { net_allocations } => {
format!("{} net allocations (allocs - deallocs)", net_allocations)
}
LeakPattern::LargeOld { size, age } => {
format!("Large {} byte allocation held for {:?}", size, age)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LeakSeverity {
Low,
Medium,
High,
}
#[derive(Debug, Clone)]
pub struct AllocationDelta {
pub allocations_delta: i64,
pub deallocations_delta: i64,
pub bytes_delta: i64,
pub duration: Duration,
}
impl AllocationDelta {
pub fn memory_grew(&self) -> bool {
self.bytes_delta > 0
}
pub fn allocation_rate(&self) -> f64 {
if self.duration.as_secs_f64() > 0.0 {
self.allocations_delta as f64 / self.duration.as_secs_f64()
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_leak_detector_new() {
let detector = LeakDetector::new();
assert_eq!(detector.old_threshold, Duration::from_secs(60));
assert_eq!(detector.min_size, 64);
}
#[test]
fn test_leak_pattern_description() {
let pattern = LeakPattern::RepeatedSize {
size: 1024,
count: 10,
};
assert!(pattern.description().contains("10 allocations"));
assert!(pattern.description().contains("1024 bytes"));
let pattern = LeakPattern::VeryOld {
age: Duration::from_secs(300),
size: 2048,
};
assert!(pattern.description().contains("2048 byte"));
}
#[test]
fn test_leak_severity_ordering() {
assert!(LeakSeverity::High > LeakSeverity::Medium);
assert!(LeakSeverity::Medium > LeakSeverity::Low);
}
#[test]
fn test_leak_report_empty() {
let report = LeakReport {
session_duration: Duration::from_secs(10),
total_allocations: 100,
total_deallocations: 100,
current_bytes: 0,
peak_bytes: 1000,
old_allocations_count: 0,
potential_leaks: vec![],
};
assert!(!report.has_leaks());
assert!(!report.has_high_severity_leaks());
assert_eq!(report.total_leaked_bytes(), 0);
}
#[test]
fn test_leak_report_with_leaks() {
let report = LeakReport {
session_duration: Duration::from_secs(10),
total_allocations: 100,
total_deallocations: 50,
current_bytes: 5000,
peak_bytes: 6000,
old_allocations_count: 5,
potential_leaks: vec![
PotentialLeak {
pattern: LeakPattern::RepeatedSize {
size: 1024,
count: 5,
},
severity: LeakSeverity::Medium,
total_bytes: 5120,
oldest_age: Duration::from_secs(120),
sample_backtrace: None,
},
PotentialLeak {
pattern: LeakPattern::GrowingCount {
net_allocations: 50,
},
severity: LeakSeverity::High,
total_bytes: 5000,
oldest_age: Duration::default(),
sample_backtrace: None,
},
],
};
assert!(report.has_leaks());
assert!(report.has_high_severity_leaks());
assert_eq!(report.total_leaked_bytes(), 10120);
}
#[test]
fn test_allocation_delta() {
let delta = AllocationDelta {
allocations_delta: 100,
deallocations_delta: 50,
bytes_delta: 5000,
duration: Duration::from_secs(10),
};
assert!(delta.memory_grew());
assert_eq!(delta.allocation_rate(), 10.0);
}
}