Skip to main content

scirs2_core/memory/
leak_detection.rs

1//! # Memory Leak Detection System
2//!
3//! Production-grade memory leak detection and monitoring system for `SciRS2` Core
4//! providing comprehensive memory profiling, leak testing, and integration with
5//! memory profiling tools for enterprise deployments.
6//!
7//! ## Features
8//!
9//! - Real-time memory leak detection with configurable thresholds
10//! - Integration with valgrind, AddressSanitizer, and other profiling tools
11//! - Automatic leak testing for continuous integration
12//! - Memory pattern analysis and anomaly detection
13//! - Production-safe monitoring with minimal overhead
14//! - Detailed leak reports with call stack information
15//! - Integration with existing memory metrics system
16//! - Support for custom allocation tracking
17//!
18//! ## Example
19//!
20//! ```rust
21//! use scirs2_core::memory::leak_detection::{LeakDetector, LeakDetectionConfig};
22//!
23//! // Configure leak detection
24//! let config = LeakDetectionConfig::default()
25//!     .with_threshold_mb(100)
26//!     .with_samplingrate(0.1);
27//!
28//! let mut detector = LeakDetector::new(config)?;
29//!
30//! // Start monitoring a function
31//! let checkpoint = detector.create_checkpoint("matrix_operations")?;
32//!
33//! // Perform operations that might leak memory
34//! fn performmatrix_calculations() {
35//!     // Example computation that could potentially leak memory
36//!     let matrix: Vec<Vec<f64>> = (0..100).map(|i| vec![i as f64; 100]).collect();
37//! }
38//! performmatrix_calculations();
39//!
40//! // Check for leaks
41//! let report = detector.check_leaks(&checkpoint)?;
42//! if report.has_leaks() {
43//!     println!("Memory leaks detected: {}", report.summary());
44//! }
45//! # Ok::<(), Box<dyn std::error::Error>>(())
46//! ```
47
48use crate::error::CoreError;
49use std::collections::HashMap;
50use std::sync::{Arc, Mutex, RwLock};
51use std::time::Duration;
52use uuid::Uuid;
53
54#[cfg(feature = "serialization")]
55use serde::{Deserialize, Serialize};
56
57/// Memory leak detection configuration
58#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
59#[derive(Debug, Clone)]
60pub struct LeakDetectionConfig {
61    /// Enable real-time leak detection
62    pub enabled: bool,
63    /// Memory growth threshold in bytes
64    pub growth_threshold_bytes: u64,
65    /// Time window for leak detection
66    pub detection_window: Duration,
67    /// Sampling rate for profiling (0.0 to 1.0)
68    pub samplingrate: f64,
69    /// Enable call stack collection
70    pub collect_call_stacks: bool,
71    /// Maximum number of tracked allocations
72    pub max_tracked_allocations: usize,
73    /// Enable integration with external profilers
74    pub enable_external_profilers: bool,
75    /// Profiler tools to use
76    pub profiler_tools: Vec<ProfilerTool>,
77    /// Enable periodic leak checks
78    pub enable_periodic_checks: bool,
79    /// Periodic check interval
80    pub check_interval: Duration,
81    /// Enable production monitoring mode
82    pub production_mode: bool,
83}
84
85impl Default for LeakDetectionConfig {
86    fn default() -> Self {
87        Self {
88            enabled: true,
89            growth_threshold_bytes: 100 * 1024 * 1024, // 100MB
90            detection_window: Duration::from_secs(300), // 5 minutes
91            samplingrate: 0.01,                        // 1% sampling in production
92            collect_call_stacks: false,                // Expensive in production
93            max_tracked_allocations: 10000,
94            enable_external_profilers: false,
95            profiler_tools: Vec::new(),
96            enable_periodic_checks: true,
97            check_interval: Duration::from_secs(60), // 1 minute
98            production_mode: true,
99        }
100    }
101}
102
103impl LeakDetectionConfig {
104    /// Set memory threshold in megabytes
105    pub fn with_threshold_mb(mut self, mb: u64) -> Self {
106        self.growth_threshold_bytes = mb * 1024 * 1024;
107        self
108    }
109
110    /// Set sampling rate
111    pub fn with_samplingrate(mut self, rate: f64) -> Self {
112        self.samplingrate = rate.clamp(0.0, 1.0);
113        self
114    }
115
116    /// Enable call stack collection
117    pub fn with_call_stacks(mut self, enable: bool) -> Self {
118        self.collect_call_stacks = enable;
119        self
120    }
121
122    /// Enable development mode (more detailed tracking)
123    pub fn development_mode(mut self) -> Self {
124        self.production_mode = false;
125        self.samplingrate = 1.0;
126        self.collect_call_stacks = true;
127        self.max_tracked_allocations = 100000;
128        self
129    }
130}
131
132/// External profiler tools
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub enum ProfilerTool {
135    /// Valgrind memcheck
136    Valgrind,
137    /// AddressSanitizer
138    AddressSanitizer,
139    /// Heaptrack
140    Heaptrack,
141    /// Massif (part of Valgrind)
142    Massif,
143    /// jemalloc profiling
144    Jemalloc,
145    /// Custom profiler
146    Custom(String),
147}
148
149impl ProfilerTool {
150    /// Get the tool name
151    pub fn name(&self) -> &str {
152        match self {
153            ProfilerTool::Valgrind => "valgrind",
154            ProfilerTool::AddressSanitizer => "asan",
155            ProfilerTool::Heaptrack => "heaptrack",
156            ProfilerTool::Massif => "massif",
157            ProfilerTool::Jemalloc => "jemalloc",
158            ProfilerTool::Custom(name) => name,
159        }
160    }
161}
162
163/// Memory checkpoint for leak detection
164#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
165#[derive(Debug, Clone)]
166pub struct MemoryCheckpoint {
167    /// Unique checkpoint identifier
168    pub id: Uuid,
169    /// Checkpoint name/label
170    pub name: String,
171    /// Timestamp when checkpoint was created
172    pub timestamp: chrono::DateTime<chrono::Utc>,
173    /// Memory usage at checkpoint creation
174    pub memory_usage: MemoryUsage,
175    /// Active allocations at checkpoint
176    pub active_allocations: u64,
177    /// Call stack when checkpoint was created
178    pub call_stack: Option<CallStack>,
179    /// Thread ID
180    pub thread_id: u64,
181    /// Process ID
182    pub process_id: u32,
183}
184
185/// Memory usage snapshot
186#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
187#[derive(Debug, Clone)]
188pub struct MemoryUsage {
189    /// Resident set size (RSS) in bytes
190    pub rss_bytes: u64,
191    /// Virtual memory size in bytes
192    pub virtual_bytes: u64,
193    /// Heap size in bytes
194    pub heap_bytes: Option<u64>,
195    /// Stack size in bytes
196    pub stack_bytes: Option<u64>,
197    /// Number of memory mappings
198    pub mappings_count: Option<u64>,
199    /// Peak memory usage since start
200    pub peak_bytes: Option<u64>,
201}
202
203/// Call stack information
204#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
205#[derive(Debug, Clone)]
206pub struct CallStack {
207    /// Stack frames
208    pub frames: Vec<StackFrame>,
209    /// Maximum depth captured
210    pub max_depth: usize,
211    /// Whether stack was truncated
212    pub truncated: bool,
213}
214
215/// Individual stack frame
216#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
217#[derive(Debug, Clone)]
218pub struct StackFrame {
219    /// Function name
220    pub function: Option<String>,
221    /// File name
222    pub file: Option<String>,
223    /// Line number
224    pub line: Option<u32>,
225    /// Memory address
226    pub address: u64,
227    /// Module/library name
228    pub module: Option<String>,
229}
230
231/// Memory leak report
232#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
233#[derive(Debug, Clone)]
234pub struct LeakReport {
235    /// Checkpoint information
236    pub checkpoint: MemoryCheckpoint,
237    /// Current memory usage
238    pub current_usage: MemoryUsage,
239    /// Memory growth since checkpoint
240    pub memory_growth: i64,
241    /// Detected leaks
242    pub leaks: Vec<MemoryLeak>,
243    /// Analysis summary
244    pub summary: LeakSummary,
245    /// Recommendations
246    pub recommendations: Vec<String>,
247    /// Detection timestamp
248    pub detection_time: chrono::DateTime<chrono::Utc>,
249}
250
251impl LeakReport {
252    /// Check if any leaks were detected
253    pub fn has_leaks(&self) -> bool {
254        !self.leaks.is_empty()
255    }
256
257    /// Get total leaked bytes
258    pub fn total_leaked_bytes(&self) -> u64 {
259        self.leaks.iter().map(|leak| leak.size_bytes).sum()
260    }
261
262    /// Get summary string
263    pub fn summary(&self) -> String {
264        if self.has_leaks() {
265            format!(
266                "{} leaks detected totaling {} bytes",
267                self.leaks.len(),
268                self.total_leaked_bytes()
269            )
270        } else {
271            "No memory leaks detected".to_string()
272        }
273    }
274}
275
276/// Individual memory leak
277#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
278#[derive(Debug, Clone)]
279pub struct MemoryLeak {
280    /// Leak identifier
281    pub id: Uuid,
282    /// Size of the leak in bytes
283    pub size_bytes: u64,
284    /// Allocation site call stack
285    pub allocation_stack: Option<CallStack>,
286    /// Leak type
287    pub leak_type: LeakType,
288    /// Confidence level
289    pub confidence: f64,
290    /// First detected timestamp
291    pub first_detected: chrono::DateTime<chrono::Utc>,
292    /// Last seen timestamp
293    pub last_seen: chrono::DateTime<chrono::Utc>,
294    /// Allocation count
295    pub allocation_count: u64,
296}
297
298/// Types of memory leaks
299#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum LeakType {
302    /// Definite leak - memory definitely lost
303    Definite,
304    /// Indirect leak - leak caused by other leaks
305    Indirect,
306    /// Possible leak - might be intentionally retained
307    Possible,
308    /// Reachable leak - still reachable but suspicious
309    Reachable,
310    /// Growth pattern - suspicious growth pattern
311    GrowthPattern,
312}
313
314impl LeakType {
315    /// Get the severity score (0-10)
316    pub fn severity(&self) -> u8 {
317        match self {
318            LeakType::Definite => 10,
319            LeakType::Indirect => 8,
320            LeakType::Possible => 5,
321            LeakType::Reachable => 3,
322            LeakType::GrowthPattern => 7,
323        }
324    }
325}
326
327/// Leak detection summary
328#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
329#[derive(Debug, Clone)]
330pub struct LeakSummary {
331    /// Total number of leaks
332    pub total_leaks: usize,
333    /// Total leaked bytes
334    pub total_leaked_bytes: u64,
335    /// Leaks by type
336    pub leaks_by_type: HashMap<String, usize>,
337    /// Highest severity leak
338    pub max_severity: u8,
339    /// Average confidence
340    pub average_confidence: f64,
341    /// Memory growth rate (bytes per second)
342    pub growth_rate: f64,
343}
344
345/// Allocation tracking information
346#[derive(Debug, Clone)]
347struct AllocationInfo {
348    /// Allocation size
349    size: u64,
350    /// Allocation timestamp
351    timestamp: chrono::DateTime<chrono::Utc>,
352    /// Call stack at allocation
353    call_stack: Option<CallStack>,
354    /// Thread ID
355    #[allow(dead_code)]
356    thread_id: u64,
357    /// Allocation ID
358    #[allow(dead_code)]
359    id: u64,
360}
361
362/// Memory leak detector implementation
363pub struct LeakDetector {
364    /// Configuration
365    config: LeakDetectionConfig,
366    /// Tracked allocations
367    allocations: Arc<RwLock<HashMap<u64, AllocationInfo>>>,
368    /// Address to allocation ID mapping
369    address_to_id: Arc<RwLock<HashMap<u64, u64>>>,
370    /// Checkpoints
371    checkpoints: Arc<Mutex<HashMap<Uuid, MemoryCheckpoint>>>,
372    /// Detection results
373    reports: Arc<Mutex<Vec<LeakReport>>>,
374    /// Background monitoring
375    monitoring_active: Arc<Mutex<bool>>,
376    /// Allocation counter
377    allocation_counter: Arc<Mutex<u64>>,
378    /// External profiler integrations
379    profiler_integrations: Vec<Box<dyn ProfilerIntegration + Send + Sync>>,
380}
381
382impl LeakDetector {
383    /// Create a new leak detector
384    pub fn new(config: LeakDetectionConfig) -> Result<Self, CoreError> {
385        let detector = Self {
386            config,
387            allocations: Arc::new(RwLock::new(HashMap::new())),
388            address_to_id: Arc::new(RwLock::new(HashMap::new())),
389            checkpoints: Arc::new(Mutex::new(HashMap::new())),
390            reports: Arc::new(Mutex::new(Vec::new())),
391            monitoring_active: Arc::new(Mutex::new(false)),
392            allocation_counter: Arc::new(Mutex::new(0)),
393            profiler_integrations: Vec::new(),
394        };
395
396        Ok(detector)
397    }
398
399    /// Start background monitoring
400    pub fn startmonitoring(&self) -> Result<(), CoreError> {
401        if !self.config.enabled || !self.config.enable_periodic_checks {
402            return Ok(());
403        }
404
405        let mut monitoring = self.monitoring_active.lock().map_err(|_| {
406            CoreError::ComputationError(crate::error::ErrorContext::new(
407                "Failed to acquire monitoring lock".to_string(),
408            ))
409        })?;
410
411        if *monitoring {
412            return Ok(()); // Already monitoring
413        }
414
415        *monitoring = true;
416
417        // In a real implementation, you'd spawn a background thread here
418        // For now, we'll just mark monitoring as active
419        Ok(())
420    }
421
422    /// Stop background monitoring
423    pub fn stopmonitoring(&self) -> Result<(), CoreError> {
424        let mut monitoring = self.monitoring_active.lock().map_err(|_| {
425            CoreError::ComputationError(crate::error::ErrorContext::new(
426                "Failed to acquire monitoring lock".to_string(),
427            ))
428        })?;
429        *monitoring = false;
430        Ok(())
431    }
432
433    /// Create a memory checkpoint
434    pub fn create_checkpoint(&self, name: &str) -> Result<MemoryCheckpoint, CoreError> {
435        let memory_usage = self.get_current_memory_usage()?;
436        let call_stack = if self.config.collect_call_stacks {
437            Some(self.capture_call_stack()?)
438        } else {
439            None
440        };
441
442        let checkpoint = MemoryCheckpoint {
443            id: Uuid::new_v4(),
444            name: name.to_string(),
445            timestamp: chrono::Utc::now(),
446            memory_usage,
447            active_allocations: self.get_active_allocation_count()?,
448            call_stack,
449            thread_id: self.get_thread_id(),
450            process_id: std::process::id(),
451        };
452
453        let mut checkpoints = self.checkpoints.lock().map_err(|_| {
454            CoreError::ComputationError(crate::error::ErrorContext::new(
455                "Failed to acquire checkpoints lock".to_string(),
456            ))
457        })?;
458        checkpoints.insert(checkpoint.id, checkpoint.clone());
459
460        Ok(checkpoint)
461    }
462
463    /// Check for memory leaks since a checkpoint
464    pub fn check_leaks(&self, checkpoint: &MemoryCheckpoint) -> Result<LeakReport, CoreError> {
465        let current_usage = self.get_current_memory_usage()?;
466        let memory_growth =
467            current_usage.rss_bytes as i64 - checkpoint.memory_usage.rss_bytes as i64;
468
469        let mut leaks = Vec::new();
470
471        // Check for threshold violations
472        if memory_growth > self.config.growth_threshold_bytes as i64 {
473            let leak = MemoryLeak {
474                id: Uuid::new_v4(),
475                size_bytes: memory_growth as u64,
476                allocation_stack: checkpoint.call_stack.clone(),
477                leak_type: LeakType::GrowthPattern,
478                confidence: 0.8,
479                first_detected: checkpoint.timestamp,
480                last_seen: chrono::Utc::now(),
481                allocation_count: 1,
482            };
483            leaks.push(leak);
484        }
485
486        // Analyze allocation patterns
487        leaks.extend(self.analyze_allocation_patterns(checkpoint)?);
488
489        // Run external profiler checks
490        leaks.extend(self.run_external_profiler_checks()?);
491
492        let summary = self.create_leak_summary(&leaks, memory_growth);
493        let recommendations = self.generate_recommendations(&leaks, memory_growth);
494
495        let report = LeakReport {
496            checkpoint: checkpoint.clone(),
497            current_usage,
498            memory_growth,
499            leaks,
500            summary,
501            recommendations,
502            detection_time: chrono::Utc::now(),
503        };
504
505        // Store report
506        let mut reports = self.reports.lock().map_err(|_| {
507            CoreError::ComputationError(crate::error::ErrorContext::new(
508                "Failed to acquire reports lock".to_string(),
509            ))
510        })?;
511        reports.push(report.clone());
512
513        Ok(report)
514    }
515
516    /// Track a memory allocation
517    pub fn track_allocation(&self, size: u64, address: u64) -> Result<(), CoreError> {
518        if !self.config.enabled {
519            return Ok(());
520        }
521
522        // Apply sampling
523        if self.config.samplingrate < 1.0 {
524            use rand::RngExt;
525            let mut rng = rand::rng();
526            if rng.random::<f64>() > self.config.samplingrate {
527                return Ok(());
528            }
529        }
530
531        let mut counter = self.allocation_counter.lock().map_err(|_| {
532            CoreError::ComputationError(crate::error::ErrorContext::new(
533                "Failed to acquire allocation counter".to_string(),
534            ))
535        })?;
536        *counter += 1;
537        let allocation_id = *counter;
538
539        let call_stack = if self.config.collect_call_stacks {
540            Some(self.capture_call_stack()?)
541        } else {
542            None
543        };
544
545        let allocation_info = AllocationInfo {
546            size,
547            timestamp: chrono::Utc::now(),
548            call_stack,
549            thread_id: self.get_thread_id(),
550            id: allocation_id,
551        };
552
553        let mut allocations = self.allocations.write().map_err(|_| {
554            CoreError::ComputationError(crate::error::ErrorContext::new(
555                "Failed to acquire allocations lock".to_string(),
556            ))
557        })?;
558
559        // Prevent memory usage from growing too much
560        if allocations.len() >= self.config.max_tracked_allocations {
561            // Remove oldest allocation
562            if let Some((oldest_id, oldest_info)) = allocations
563                .iter()
564                .min_by_key(|(_, info)| info.timestamp)
565                .map(|(id, info)| (*id, info.clone()))
566            {
567                allocations.remove(&oldest_id);
568
569                // Also remove from address_to_id mapping
570                // Note: We don't have the address stored in AllocationInfo, so we'd need to
571                // iterate through address_to_id to find it. In a real implementation,
572                // AllocationInfo should store the address.
573            }
574        }
575
576        allocations.insert(allocation_id, allocation_info);
577
578        // Store address to ID mapping
579        let mut address_to_id = self.address_to_id.write().map_err(|_| {
580            CoreError::ComputationError(crate::error::ErrorContext::new(
581                "Failed to acquire address_to_id lock".to_string(),
582            ))
583        })?;
584        address_to_id.insert(address, allocation_id);
585
586        Ok(())
587    }
588
589    /// Track memory deallocation
590    pub fn track_deallocation(&self, address: u64) -> Result<(), CoreError> {
591        if !self.config.enabled {
592            return Ok(());
593        }
594
595        // Look up the allocation ID for this address
596        let allocation_id = {
597            let mut address_to_id = self.address_to_id.write().map_err(|_| {
598                CoreError::ComputationError(crate::error::ErrorContext::new(
599                    "Failed to acquire address_to_id lock".to_string(),
600                ))
601            })?;
602            address_to_id.remove(&address)
603        };
604
605        if let Some(id) = allocation_id {
606            let mut allocations = self.allocations.write().map_err(|_| {
607                CoreError::ComputationError(crate::error::ErrorContext::new(
608                    "Failed to acquire allocations lock".to_string(),
609                ))
610            })?;
611            allocations.remove(&id);
612        }
613
614        Ok(())
615    }
616
617    /// Get current memory usage of this process.
618    ///
619    /// Resident and virtual sizes are read from the operating system. Heap
620    /// usage is only reported when a real source is available (it is left as
621    /// `None` otherwise rather than being fabricated, since the standard
622    /// allocator exposes no portable heap-usage query).
623    fn get_current_memory_usage(&self) -> Result<MemoryUsage, CoreError> {
624        Ok(MemoryUsage {
625            rss_bytes: self.get_rss_memory()?,
626            virtual_bytes: self.get_virtual_memory()?,
627            heap_bytes: self.get_heap_memory(),
628            stack_bytes: None,
629            mappings_count: None,
630            peak_bytes: None,
631        })
632    }
633
634    /// Get resident set size (RSS) memory in bytes.
635    ///
636    /// Reads the real value from the OS. On Linux this parses
637    /// `/proc/self/status`; on other platforms it uses `sysinfo` when that
638    /// feature is enabled. If no real source is available it returns an honest
639    /// error instead of a fabricated number.
640    fn get_rss_memory(&self) -> Result<u64, CoreError> {
641        #[cfg(target_os = "linux")]
642        {
643            if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
644                for line in status.lines() {
645                    if line.starts_with("VmRSS:") {
646                        if let Some(kb_str) = line.split_whitespace().nth(1) {
647                            if let Ok(kb) = kb_str.parse::<u64>() {
648                                return Ok(kb * 1024); // Convert KB to bytes
649                            }
650                        }
651                    }
652                }
653            }
654        }
655
656        #[cfg(feature = "sysinfo")]
657        {
658            use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
659            let pid = Pid::from_u32(std::process::id());
660            let mut system = System::new();
661            system.refresh_processes_specifics(
662                ProcessesToUpdate::Some(&[pid]),
663                true,
664                ProcessRefreshKind::nothing().with_memory(),
665            );
666            if let Some(process) = system.process(pid) {
667                return Ok(process.memory());
668            }
669        }
670
671        Err(CoreError::ComputationError(crate::error::ErrorContext::new(
672            "Resident memory size is unavailable on this platform (enable the `sysinfo` feature for non-Linux support)".to_string(),
673        )))
674    }
675
676    /// Get virtual memory size in bytes.
677    ///
678    /// Reads the real value from the OS (Linux `/proc/self/status`, or
679    /// `sysinfo` when enabled). Returns an honest error when no real source is
680    /// available rather than fabricating a value.
681    fn get_virtual_memory(&self) -> Result<u64, CoreError> {
682        #[cfg(target_os = "linux")]
683        {
684            if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
685                for line in status.lines() {
686                    if line.starts_with("VmSize:") {
687                        if let Some(kb_str) = line.split_whitespace().nth(1) {
688                            if let Ok(kb) = kb_str.parse::<u64>() {
689                                return Ok(kb * 1024); // Convert KB to bytes
690                            }
691                        }
692                    }
693                }
694            }
695        }
696
697        #[cfg(feature = "sysinfo")]
698        {
699            use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
700            let pid = Pid::from_u32(std::process::id());
701            let mut system = System::new();
702            system.refresh_processes_specifics(
703                ProcessesToUpdate::Some(&[pid]),
704                true,
705                ProcessRefreshKind::nothing().with_memory(),
706            );
707            if let Some(process) = system.process(pid) {
708                return Ok(process.virtual_memory());
709            }
710        }
711
712        Err(CoreError::ComputationError(crate::error::ErrorContext::new(
713            "Virtual memory size is unavailable on this platform (enable the `sysinfo` feature for non-Linux support)".to_string(),
714        )))
715    }
716
717    /// Get heap memory usage in bytes, if a real source is available.
718    ///
719    /// The Rust standard allocator does not expose a portable heap-usage query,
720    /// so this returns `None` unless a real source can be read. Returning a
721    /// fabricated value here would corrupt downstream leak accounting, so the
722    /// absence of data is reported honestly as `None`.
723    fn get_heap_memory(&self) -> Option<u64> {
724        // No portable real heap-usage source is wired up. When integrated with
725        // an allocator that exposes statistics (e.g. jemalloc's `stats.allocated`),
726        // this is where that value would be returned.
727        None
728    }
729
730    /// Get active allocation count
731    fn get_active_allocation_count(&self) -> Result<u64, CoreError> {
732        let allocations = self.allocations.read().map_err(|_| {
733            CoreError::ComputationError(crate::error::ErrorContext::new(
734                "Failed to acquire allocations lock".to_string(),
735            ))
736        })?;
737        Ok(allocations.len() as u64)
738    }
739
740    /// Capture the current call stack.
741    ///
742    /// Uses `std::backtrace::Backtrace`, which is the only backtrace facility
743    /// available without pulling in the `backtrace` crate. The standard library
744    /// exposes the backtrace as formatted text rather than structured frames, so
745    /// each captured line is stored in the frame's `function` field; numeric
746    /// addresses are not recoverable through `std` and are reported as `0`
747    /// rather than being fabricated.
748    ///
749    /// If backtrace capture is disabled (e.g. `RUST_BACKTRACE` is unset) or
750    /// unsupported on the platform, an empty (but honest) call stack is
751    /// returned instead of inventing frames.
752    fn capture_call_stack(&self) -> Result<CallStack, CoreError> {
753        use std::backtrace::{Backtrace, BacktraceStatus};
754
755        let backtrace = Backtrace::capture();
756
757        if backtrace.status() != BacktraceStatus::Captured {
758            // Capturing is disabled or unsupported: report no frames honestly.
759            return Ok(CallStack {
760                frames: Vec::new(),
761                max_depth: 0,
762                truncated: false,
763            });
764        }
765
766        let rendered = backtrace.to_string();
767        let mut frames = Vec::new();
768        for line in rendered.lines() {
769            let trimmed = line.trim();
770            if trimmed.is_empty() {
771                continue;
772            }
773            frames.push(StackFrame {
774                function: Some(trimmed.to_string()),
775                file: None,
776                line: None,
777                // std does not expose resolved instruction addresses; use 0
778                // rather than a fabricated value.
779                address: 0,
780                module: None,
781            });
782        }
783
784        let max_depth = frames.len();
785        Ok(CallStack {
786            frames,
787            max_depth,
788            truncated: false,
789        })
790    }
791
792    /// Get current thread ID
793    fn get_thread_id(&self) -> u64 {
794        // Simplified implementation
795        use std::thread;
796        format!("{:?}", thread::current().id())
797            .chars()
798            .filter_map(|c| c.to_digit(10))
799            .map(|d| d as u64)
800            .fold(0, |acc, d| acc * 10 + d)
801    }
802
803    /// Analyze allocation patterns for leaks
804    fn analyze_allocation_patterns(
805        &self,
806        _checkpoint: &MemoryCheckpoint,
807    ) -> Result<Vec<MemoryLeak>, CoreError> {
808        let mut leaks = Vec::new();
809
810        let allocations = self.allocations.read().map_err(|_| {
811            CoreError::ComputationError(crate::error::ErrorContext::new(
812                "Failed to acquire allocations lock".to_string(),
813            ))
814        })?;
815
816        // Look for suspicious patterns
817        let now = chrono::Utc::now();
818        let old_threshold = now - Duration::from_secs(3600); // 1 hour
819
820        for (_id, allocation) in allocations.iter() {
821            if allocation.timestamp < old_threshold && allocation.size > 1024 * 1024 {
822                // Large allocation that's been around for a while
823                leaks.push(MemoryLeak {
824                    id: Uuid::new_v4(),
825                    size_bytes: allocation.size,
826                    allocation_stack: allocation.call_stack.clone(),
827                    leak_type: LeakType::Possible,
828                    confidence: 0.6,
829                    first_detected: allocation.timestamp,
830                    last_seen: now,
831                    allocation_count: 1,
832                });
833            }
834        }
835
836        Ok(leaks)
837    }
838
839    /// Run external profiler checks
840    fn run_external_profiler_checks(&self) -> Result<Vec<MemoryLeak>, CoreError> {
841        let mut leaks = Vec::new();
842
843        for integration in &self.profiler_integrations {
844            leaks.extend(integration.check_leaks()?);
845        }
846
847        Ok(leaks)
848    }
849
850    /// Create leak summary
851    fn create_leak_summary(&self, leaks: &[MemoryLeak], memorygrowth: i64) -> LeakSummary {
852        let total_leaks = leaks.len();
853        let total_leaked_bytes = leaks.iter().map(|leak| leak.size_bytes).sum();
854
855        let mut leaks_by_type = HashMap::new();
856        for leak in leaks {
857            let type_name = format!("{0:?}", leak.leak_type);
858            *leaks_by_type.entry(type_name).or_insert(0) += 1;
859        }
860
861        let max_severity = leaks
862            .iter()
863            .map(|leak| leak.leak_type.severity())
864            .max()
865            .unwrap_or(0);
866
867        let average_confidence = if total_leaks > 0 {
868            leaks.iter().map(|leak| leak.confidence).sum::<f64>() / total_leaks as f64
869        } else {
870            0.0
871        };
872
873        let growth_rate = memorygrowth as f64 / 60.0; // bytes per second (assuming 1 minute window)
874
875        LeakSummary {
876            total_leaks,
877            total_leaked_bytes,
878            leaks_by_type,
879            max_severity,
880            average_confidence,
881            growth_rate,
882        }
883    }
884
885    /// Generate recommendations based on detected leaks
886    fn generate_recommendations(&self, leaks: &[MemoryLeak], memorygrowth: i64) -> Vec<String> {
887        let mut recommendations = Vec::new();
888
889        if leaks.is_empty() && memorygrowth < 1024 * 1024 {
890            recommendations.push("No significant memory issues detected".to_string());
891            return recommendations;
892        }
893
894        if memorygrowth > self.config.growth_threshold_bytes as i64 {
895            recommendations.push(format!(
896                "Memory growth of {memorygrowth} bytes exceeds threshold, investigate allocation patterns"
897            ));
898        }
899
900        let definite_leaks: Vec<_> = leaks
901            .iter()
902            .filter(|leak| leak.leak_type == LeakType::Definite)
903            .collect();
904
905        if !definite_leaks.is_empty() {
906            recommendations.push(format!(
907                "{} definite leaks detected - fix these immediately",
908                definite_leaks.len()
909            ));
910        }
911
912        let large_leaks: Vec<_> = leaks.iter()
913            .filter(|leak| leak.size_bytes > 10 * 1024 * 1024) // > 10MB
914            .collect();
915
916        if !large_leaks.is_empty() {
917            recommendations.push(format!(
918                "{} large leaks (>10MB) detected - prioritize fixing these",
919                large_leaks.len()
920            ));
921        }
922
923        if leaks.iter().any(|leak| leak.confidence > 0.8) {
924            recommendations.push("High confidence leaks detected - likely real issues".to_string());
925        }
926
927        recommendations.push(
928            "Consider running with valgrind or AddressSanitizer for detailed analysis".to_string(),
929        );
930
931        recommendations
932    }
933
934    /// Get all reports
935    pub fn get_reports(&self) -> Result<Vec<LeakReport>, CoreError> {
936        let reports = self.reports.lock().map_err(|_| {
937            CoreError::ComputationError(crate::error::ErrorContext::new(
938                "Failed to acquire reports lock".to_string(),
939            ))
940        })?;
941        Ok(reports.clone())
942    }
943
944    /// Clear old reports
945    pub fn clear_old_reports(&self, maxage: Duration) -> Result<usize, CoreError> {
946        let mut reports = self.reports.lock().map_err(|_| {
947            CoreError::ComputationError(crate::error::ErrorContext::new(
948                "Failed to acquire reports lock".to_string(),
949            ))
950        })?;
951
952        let cutoff = chrono::Utc::now()
953            - chrono::Duration::from_std(maxage).map_err(|e| {
954                CoreError::ComputationError(crate::error::ErrorContext::new(format!(
955                    "Invalid duration: {e}"
956                )))
957            })?;
958
959        let initial_len = reports.len();
960        reports.retain(|report| report.detection_time > cutoff);
961
962        Ok(initial_len - reports.len())
963    }
964}
965
966/// Trait for external profiler integrations
967pub trait ProfilerIntegration {
968    /// Check for leaks using the external profiler
969    fn check_leaks(&self) -> Result<Vec<MemoryLeak>, CoreError>;
970
971    /// Get profiler name
972    fn name(&self) -> &str;
973
974    /// Check if profiler is available
975    fn is_available(&self) -> bool;
976}
977
978/// Valgrind integration
979pub struct ValgrindIntegration {
980    enabled: bool,
981}
982
983impl ValgrindIntegration {
984    pub fn new() -> Self {
985        Self {
986            enabled: Self::check_valgrind_available(),
987        }
988    }
989
990    fn check_valgrind_available() -> bool {
991        // Check if running under valgrind
992        std::env::var("VALGRIND_OPTS").is_ok() || std::env::var("RUNNING_ON_VALGRIND").is_ok()
993    }
994}
995
996impl ProfilerIntegration for ValgrindIntegration {
997    fn check_leaks(&self) -> Result<Vec<MemoryLeak>, CoreError> {
998        if !self.enabled {
999            // Not running under valgrind: there is genuinely nothing to report.
1000            return Ok(Vec::new());
1001        }
1002
1003        // We detected a valgrind environment, but parsing valgrind's leak report
1004        // (e.g. via the Valgrind client request API or its XML output) is not
1005        // implemented. Returning an empty list here would falsely claim "no
1006        // leaks", so report honestly that the integration cannot perform the
1007        // check rather than fabricating a clean result.
1008        Err(CoreError::ComputationError(
1009            crate::error::ErrorContext::new(
1010                "Valgrind leak parsing is not implemented; cannot report leak results".to_string(),
1011            ),
1012        ))
1013    }
1014
1015    fn name(&self) -> &str {
1016        "valgrind"
1017    }
1018
1019    fn is_available(&self) -> bool {
1020        self.enabled
1021    }
1022}
1023
1024impl Default for ValgrindIntegration {
1025    fn default() -> Self {
1026        Self::new()
1027    }
1028}
1029
1030/// RAII guard for automatic leak checking
1031pub struct LeakCheckGuard<'a> {
1032    detector: &'a LeakDetector,
1033    checkpoint: MemoryCheckpoint,
1034    check_on_drop: bool,
1035}
1036
1037impl<'a> LeakCheckGuard<'a> {
1038    /// Create a new leak check guard
1039    pub fn new(detector: &'a LeakDetector, name: &str) -> Result<Self, CoreError> {
1040        let checkpoint = detector.create_checkpoint(name)?;
1041        Ok(Self {
1042            detector,
1043            checkpoint,
1044            check_on_drop: true,
1045        })
1046    }
1047
1048    /// Disable automatic check on drop
1049    pub fn disable_auto_check(mut self) -> Self {
1050        self.check_on_drop = false;
1051        self
1052    }
1053
1054    /// Manually check for leaks
1055    pub fn check_leaks(&self) -> Result<LeakReport, CoreError> {
1056        self.detector.check_leaks(&self.checkpoint)
1057    }
1058}
1059
1060impl Drop for LeakCheckGuard<'_> {
1061    fn drop(&mut self) {
1062        if self.check_on_drop {
1063            if let Ok(report) = self.detector.check_leaks(&self.checkpoint) {
1064                if report.has_leaks() {
1065                    eprintln!(
1066                        "Memory leaks detected in {}: {}",
1067                        self.checkpoint.name,
1068                        report.summary()
1069                    );
1070                }
1071            }
1072        }
1073    }
1074}
1075
1076/// Convenience macro for leak checking
1077#[macro_export]
1078macro_rules! check_leaks {
1079    ($detector:expr, $name:expr, $block:block) => {{
1080        let guard = $crate::memory::leak_detection::LeakCheckGuard::new($detector, $name)?;
1081        $block
1082    }};
1083}
1084
1085/// Global leak detector instance
1086static GLOBAL_DETECTOR: std::sync::OnceLock<Arc<Mutex<LeakDetector>>> = std::sync::OnceLock::new();
1087
1088/// Get the global leak detector
1089#[allow(dead_code)]
1090pub fn global_leak_detector() -> Arc<Mutex<LeakDetector>> {
1091    GLOBAL_DETECTOR
1092        .get_or_init(|| {
1093            let config = LeakDetectionConfig::default();
1094            Arc::new(Mutex::new(
1095                LeakDetector::new(config).expect("Failed to create global leak detector"),
1096            ))
1097        })
1098        .clone()
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103    use super::*;
1104
1105    #[test]
1106    fn test_leak_detector_creation() {
1107        let config = LeakDetectionConfig::default();
1108        let detector = LeakDetector::new(config).expect("Operation failed");
1109
1110        assert!(!*detector.monitoring_active.lock().expect("Operation failed"));
1111    }
1112
1113    // Requires a real RSS source: Linux (/proc/self/status) or the `sysinfo` feature on other platforms.
1114    #[cfg(any(target_os = "linux", feature = "sysinfo"))]
1115    #[test]
1116    fn test_checkpoint_creation() {
1117        let config = LeakDetectionConfig::default();
1118        let detector = LeakDetector::new(config).expect("Operation failed");
1119
1120        let checkpoint = detector
1121            .create_checkpoint("test")
1122            .expect("Operation failed");
1123        assert_eq!(checkpoint.name, "test");
1124        assert!(checkpoint.memory_usage.rss_bytes > 0);
1125    }
1126
1127    #[test]
1128    fn test_allocation_tracking() {
1129        let config = LeakDetectionConfig::default().development_mode();
1130        let detector = LeakDetector::new(config).expect("Operation failed");
1131
1132        detector
1133            .track_allocation(1024, 0x12345678)
1134            .expect("Operation failed");
1135        detector
1136            .track_allocation(2048, 0x87654321)
1137            .expect("Operation failed");
1138
1139        let count = detector
1140            .get_active_allocation_count()
1141            .expect("Operation failed");
1142        assert_eq!(count, 2);
1143
1144        detector
1145            .track_deallocation(0x12345678)
1146            .expect("Operation failed");
1147        let count = detector
1148            .get_active_allocation_count()
1149            .expect("Operation failed");
1150        assert_eq!(count, 1);
1151    }
1152
1153    // Requires a real RSS source: Linux (/proc/self/status) or the `sysinfo` feature on other platforms.
1154    #[cfg(any(target_os = "linux", feature = "sysinfo"))]
1155    #[test]
1156    fn test_leak_check_guard() {
1157        let config = LeakDetectionConfig::default();
1158        let detector = LeakDetector::new(config).expect("Operation failed");
1159
1160        {
1161            let guard = LeakCheckGuard::new(&detector, "test_guard").expect("Operation failed");
1162            // Simulate some work that might leak memory
1163            detector
1164                .track_allocation(1024 * 1024, 0x12345678)
1165                .expect("Operation failed");
1166        } // Guard drops here and checks for leaks
1167    }
1168
1169    #[test]
1170    fn test_leak_types() {
1171        assert_eq!(LeakType::Definite.severity(), 10);
1172        assert_eq!(LeakType::Possible.severity(), 5);
1173        assert!(LeakType::Definite.severity() > LeakType::Possible.severity());
1174    }
1175
1176    #[test]
1177    fn test_config_builder() {
1178        let config = LeakDetectionConfig::default()
1179            .with_threshold_mb(50)
1180            .with_samplingrate(0.5)
1181            .with_call_stacks(true);
1182
1183        assert_eq!(config.growth_threshold_bytes, 50 * 1024 * 1024);
1184        assert_eq!(config.samplingrate, 0.5);
1185        assert!(config.collect_call_stacks);
1186    }
1187
1188    #[test]
1189    fn test_profiler_tools() {
1190        assert_eq!(ProfilerTool::Valgrind.name(), "valgrind");
1191        assert_eq!(ProfilerTool::AddressSanitizer.name(), "asan");
1192        assert_eq!(ProfilerTool::Custom("custom".to_string()).name(), "custom");
1193    }
1194
1195    #[test]
1196    fn test_memory_usage() {
1197        let usage = MemoryUsage {
1198            rss_bytes: 64 * 1024 * 1024,
1199            virtual_bytes: 256 * 1024 * 1024,
1200            heap_bytes: Some(32 * 1024 * 1024),
1201            stack_bytes: None,
1202            mappings_count: None,
1203            peak_bytes: None,
1204        };
1205
1206        assert_eq!(usage.rss_bytes, 64 * 1024 * 1024);
1207        assert!(usage.heap_bytes.is_some());
1208    }
1209}