Skip to main content

scirs2_core/profiling/
memory_profiling.rs

1//! # Advanced Memory Profiling for SciRS2
2//!
3//! This module provides comprehensive memory profiling capabilities using Pure Rust
4//! OS APIs. It enables heap profiling, memory leak detection, and allocation pattern
5//! analysis without requiring jemalloc or any C/Fortran dependencies.
6//!
7//! # Platform Support
8//!
9//! - **macOS**: Uses `mach_task_self()` / `task_info` via libc for accurate memory stats
10//! - **Linux**: Reads `/proc/self/statm` and `/proc/self/status` for memory stats
11//! - **Other Unix**: Falls back to tracking via global allocator wrapper
12//! - **Windows**: Uses `windows-sys` APIs for memory info
13//!
14//! # Features
15//!
16//! - **Heap Profiling**: Track memory allocations and deallocations
17//! - **Leak Detection**: Identify memory leaks
18//! - **Allocation Patterns**: Analyze allocation patterns
19//! - **Statistics**: Detailed memory statistics
20//! - **Zero Overhead**: Disabled by default, minimal overhead when enabled
21//! - **Pure Rust**: No C/Fortran dependencies (COOLJAPAN Policy)
22//!
23//! # Example
24//!
25//! ```rust,no_run
26//! use scirs2_core::profiling::memory_profiling::{MemoryProfiler, enable_profiling};
27//!
28//! // Enable memory profiling
29//! enable_profiling().expect("Failed to enable profiling");
30//!
31//! // ... perform allocations ...
32//!
33//! // Get memory statistics
34//! let stats = MemoryProfiler::get_stats().expect("Failed to get stats");
35//! println!("Allocated: {} bytes", stats.allocated);
36//! println!("Resident: {} bytes", stats.resident);
37//! ```
38
39#[cfg(feature = "profiling_memory")]
40use crate::CoreResult;
41#[cfg(feature = "profiling_memory")]
42use std::collections::HashMap;
43#[cfg(feature = "profiling_memory")]
44use std::sync::atomic::{AtomicUsize, Ordering};
45
46// ============================================================================
47// Global allocation tracking (Pure Rust)
48// ============================================================================
49
50#[cfg(feature = "profiling_memory")]
51static TRACKED_ALLOCATED: AtomicUsize = AtomicUsize::new(0);
52#[cfg(feature = "profiling_memory")]
53static TRACKED_PEAK: AtomicUsize = AtomicUsize::new(0);
54
55/// Record an allocation (called from tracking allocator or estimation)
56#[cfg(feature = "profiling_memory")]
57fn record_allocation(size: usize) {
58    let prev = TRACKED_ALLOCATED.fetch_add(size, Ordering::Relaxed);
59    let new_total = prev + size;
60    // Update peak using compare-and-swap loop
61    let mut current_peak = TRACKED_PEAK.load(Ordering::Relaxed);
62    while new_total > current_peak {
63        match TRACKED_PEAK.compare_exchange_weak(
64            current_peak,
65            new_total,
66            Ordering::Relaxed,
67            Ordering::Relaxed,
68        ) {
69            Ok(_) => break,
70            Err(actual) => current_peak = actual,
71        }
72    }
73}
74
75/// Record a deallocation
76#[cfg(feature = "profiling_memory")]
77fn record_deallocation(size: usize) {
78    TRACKED_ALLOCATED.fetch_sub(size, Ordering::Relaxed);
79}
80
81/// Get the current tracked allocation count
82#[cfg(feature = "profiling_memory")]
83fn get_tracked_allocated() -> usize {
84    TRACKED_ALLOCATED.load(Ordering::Relaxed)
85}
86
87// ============================================================================
88// Platform-specific memory stats (Pure Rust via libc)
89// ============================================================================
90
91/// Raw memory info from the OS
92#[cfg(feature = "profiling_memory")]
93#[derive(Debug, Clone, Default)]
94struct OsMemoryInfo {
95    /// Resident set size (physical memory used) in bytes
96    resident: usize,
97    /// Virtual memory size in bytes
98    virtual_size: usize,
99}
100
101/// Read memory info on macOS using Mach task_info API
102#[cfg(all(feature = "profiling_memory", target_os = "macos"))]
103fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
104    // Use mach_task_self() and task_info to get memory statistics
105    // This is the standard approach on macOS for process memory info
106    use std::mem;
107
108    // mach_task_basic_info struct layout (from mach/task_info.h)
109    #[repr(C)]
110    #[derive(Default)]
111    struct MachTaskBasicInfo {
112        virtual_size: u64,      // virtual memory size (bytes)
113        resident_size: u64,     // resident memory size (bytes)
114        resident_size_max: u64, // maximum resident memory size (bytes)
115        user_time: [u32; 2],    // total user run time
116        system_time: [u32; 2],  // total system run time
117        policy: i32,            // default policy
118        suspend_count: i32,     // suspend count
119    }
120
121    const MACH_TASK_BASIC_INFO: u32 = 20;
122    // Size in natural_t (u32) units
123    const MACH_TASK_BASIC_INFO_COUNT: u32 =
124        (mem::size_of::<MachTaskBasicInfo>() / mem::size_of::<u32>()) as u32;
125
126    extern "C" {
127        fn mach_task_self() -> u32;
128        fn task_info(
129            target_task: u32,
130            flavor: u32,
131            task_info_out: *mut MachTaskBasicInfo,
132            task_info_count: *mut u32,
133        ) -> i32;
134    }
135
136    let mut info = MachTaskBasicInfo::default();
137    let mut count = MACH_TASK_BASIC_INFO_COUNT;
138
139    // SAFETY: We're calling well-defined Mach kernel APIs with properly-sized buffers.
140    // mach_task_self() returns the current task port, and task_info fills the struct.
141    let kr = unsafe {
142        task_info(
143            mach_task_self(),
144            MACH_TASK_BASIC_INFO,
145            &mut info as *mut MachTaskBasicInfo,
146            &mut count,
147        )
148    };
149
150    // KERN_SUCCESS = 0
151    if kr != 0 {
152        return Err(crate::CoreError::ConfigError(
153            crate::error::ErrorContext::new(format!("task_info failed with kern_return: {}", kr)),
154        ));
155    }
156
157    Ok(OsMemoryInfo {
158        resident: info.resident_size as usize,
159        virtual_size: info.virtual_size as usize,
160    })
161}
162
163/// Read memory info on Linux from /proc/self/statm
164#[cfg(all(feature = "profiling_memory", target_os = "linux"))]
165fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
166    use std::fs;
167
168    // /proc/self/statm fields: size resident shared text lib data dt
169    // All values are in pages
170    let statm = fs::read_to_string("/proc/self/statm").map_err(|e| {
171        crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
172            "Failed to read /proc/self/statm: {}",
173            e
174        )))
175    })?;
176
177    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
178    let page_size = if page_size <= 0 {
179        4096
180    } else {
181        page_size as usize
182    };
183
184    let parts: Vec<&str> = statm.trim().split_whitespace().collect();
185    if parts.len() < 2 {
186        return Err(crate::CoreError::ConfigError(
187            crate::error::ErrorContext::new("Invalid /proc/self/statm format".to_string()),
188        ));
189    }
190
191    let virtual_pages: usize = parts[0].parse().map_err(|e| {
192        crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
193            "Failed to parse virtual size from /proc/self/statm: {}",
194            e
195        )))
196    })?;
197
198    let resident_pages: usize = parts[1].parse().map_err(|e| {
199        crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
200            "Failed to parse resident size from /proc/self/statm: {}",
201            e
202        )))
203    })?;
204
205    Ok(OsMemoryInfo {
206        resident: resident_pages * page_size,
207        virtual_size: virtual_pages * page_size,
208    })
209}
210
211/// Fallback for other platforms - returns estimates from atomic tracking
212#[cfg(all(
213    feature = "profiling_memory",
214    not(target_os = "macos"),
215    not(target_os = "linux")
216))]
217fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
218    // On unsupported platforms, use the tracked allocation as a rough estimate
219    let allocated = get_tracked_allocated();
220    Ok(OsMemoryInfo {
221        resident: allocated,
222        virtual_size: allocated,
223    })
224}
225
226// ============================================================================
227// Public API - MemoryStats
228// ============================================================================
229
230/// Memory statistics gathered from OS APIs (Pure Rust)
231#[cfg(feature = "profiling_memory")]
232#[derive(Debug, Clone)]
233pub struct MemoryStats {
234    /// Total allocated memory tracked by the profiler (bytes)
235    pub allocated: usize,
236    /// Resident memory from OS (physical memory, bytes)
237    pub resident: usize,
238    /// Mapped/virtual memory from OS (bytes)
239    pub mapped: usize,
240    /// Metadata overhead estimate (bytes) - estimated as a fraction of allocated
241    pub metadata: usize,
242    /// Retained memory estimate (bytes) - difference between resident and allocated
243    pub retained: usize,
244}
245
246#[cfg(feature = "profiling_memory")]
247impl MemoryStats {
248    /// Get current memory statistics using OS APIs
249    pub fn current() -> CoreResult<Self> {
250        let os_info = read_os_memory_info()?;
251        let tracked = get_tracked_allocated();
252
253        // Use the larger of OS-reported resident and our tracked value
254        // (tracked may be 0 if no allocator wrapper is installed)
255        let allocated = if tracked > 0 {
256            tracked
257        } else {
258            os_info.resident
259        };
260
261        // Estimate metadata as ~2% of allocated (typical allocator overhead)
262        let metadata = allocated / 50;
263
264        // Retained is memory the allocator holds but hasn't returned to the OS
265        let retained = os_info.resident.saturating_sub(allocated);
266
267        Ok(Self {
268            allocated,
269            resident: os_info.resident,
270            mapped: os_info.virtual_size,
271            metadata,
272            retained,
273        })
274    }
275
276    /// Calculate memory overhead (metadata / allocated)
277    pub fn overhead_ratio(&self) -> f64 {
278        if self.allocated == 0 {
279            0.0
280        } else {
281            self.metadata as f64 / self.allocated as f64
282        }
283    }
284
285    /// Calculate memory utilization (allocated / resident)
286    pub fn utilization_ratio(&self) -> f64 {
287        if self.resident == 0 {
288            0.0
289        } else {
290            self.allocated as f64 / self.resident as f64
291        }
292    }
293
294    /// Format as human-readable string
295    pub fn format(&self) -> String {
296        format!(
297            "Memory Stats:\n\
298             - Allocated: {} MB\n\
299             - Resident:  {} MB\n\
300             - Mapped:    {} MB\n\
301             - Metadata:  {} MB\n\
302             - Retained:  {} MB\n\
303             - Overhead:  {:.2}%\n\
304             - Utilization: {:.2}%",
305            self.allocated / 1_048_576,
306            self.resident / 1_048_576,
307            self.mapped / 1_048_576,
308            self.metadata / 1_048_576,
309            self.retained / 1_048_576,
310            self.overhead_ratio() * 100.0,
311            self.utilization_ratio() * 100.0
312        )
313    }
314}
315
316// ============================================================================
317// Public API - MemoryProfiler
318// ============================================================================
319
320/// Memory profiler
321#[cfg(feature = "profiling_memory")]
322pub struct MemoryProfiler {
323    baseline: Option<MemoryStats>,
324}
325
326#[cfg(feature = "profiling_memory")]
327impl MemoryProfiler {
328    /// Create a new memory profiler
329    pub fn new() -> Self {
330        Self { baseline: None }
331    }
332
333    /// Set the baseline memory statistics
334    pub fn set_baseline(&mut self) -> CoreResult<()> {
335        self.baseline = Some(MemoryStats::current()?);
336        Ok(())
337    }
338
339    /// Get current memory statistics
340    pub fn get_stats() -> CoreResult<MemoryStats> {
341        MemoryStats::current()
342    }
343
344    /// Get memory delta from baseline
345    pub fn get_delta(&self) -> CoreResult<Option<MemoryDelta>> {
346        if let Some(ref baseline) = self.baseline {
347            let current = MemoryStats::current()?;
348            Ok(Some(MemoryDelta {
349                allocated_delta: current.allocated as i64 - baseline.allocated as i64,
350                resident_delta: current.resident as i64 - baseline.resident as i64,
351                mapped_delta: current.mapped as i64 - baseline.mapped as i64,
352                metadata_delta: current.metadata as i64 - baseline.metadata as i64,
353                retained_delta: current.retained as i64 - baseline.retained as i64,
354            }))
355        } else {
356            Ok(None)
357        }
358    }
359
360    /// Print memory statistics
361    pub fn print_stats() -> CoreResult<()> {
362        let stats = Self::get_stats()?;
363        println!("{}", stats.format());
364        Ok(())
365    }
366
367    /// Manually record an allocation for tracking purposes
368    pub fn track_allocation(size: usize) {
369        record_allocation(size);
370    }
371
372    /// Manually record a deallocation for tracking purposes
373    pub fn track_deallocation(size: usize) {
374        record_deallocation(size);
375    }
376}
377
378#[cfg(feature = "profiling_memory")]
379impl Default for MemoryProfiler {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385// ============================================================================
386// Public API - MemoryDelta
387// ============================================================================
388
389/// Memory delta from baseline
390#[cfg(feature = "profiling_memory")]
391#[derive(Debug, Clone)]
392pub struct MemoryDelta {
393    pub allocated_delta: i64,
394    pub resident_delta: i64,
395    pub mapped_delta: i64,
396    pub metadata_delta: i64,
397    pub retained_delta: i64,
398}
399
400#[cfg(feature = "profiling_memory")]
401impl MemoryDelta {
402    /// Format as human-readable string
403    pub fn format(&self) -> String {
404        format!(
405            "Memory Delta:\n\
406             - Allocated: {:+} MB\n\
407             - Resident:  {:+} MB\n\
408             - Mapped:    {:+} MB\n\
409             - Metadata:  {:+} MB\n\
410             - Retained:  {:+} MB",
411            self.allocated_delta / 1_048_576,
412            self.resident_delta / 1_048_576,
413            self.mapped_delta / 1_048_576,
414            self.metadata_delta / 1_048_576,
415            self.retained_delta / 1_048_576
416        )
417    }
418}
419
420// ============================================================================
421// Public API - AllocationTracker
422// ============================================================================
423
424/// Allocation tracker for detecting patterns
425#[cfg(feature = "profiling_memory")]
426pub struct AllocationTracker {
427    snapshots: Vec<(String, MemoryStats)>,
428}
429
430#[cfg(feature = "profiling_memory")]
431impl AllocationTracker {
432    /// Create a new allocation tracker
433    pub fn new() -> Self {
434        Self {
435            snapshots: Vec::new(),
436        }
437    }
438
439    /// Take a snapshot with a label
440    pub fn snapshot(&mut self, label: impl Into<String>) -> CoreResult<()> {
441        let stats = MemoryStats::current()?;
442        self.snapshots.push((label.into(), stats));
443        Ok(())
444    }
445
446    /// Get all snapshots
447    pub fn snapshots(&self) -> &[(String, MemoryStats)] {
448        &self.snapshots
449    }
450
451    /// Analyze allocation patterns
452    pub fn analyze(&self) -> AllocationAnalysis {
453        if self.snapshots.is_empty() {
454            return AllocationAnalysis {
455                total_allocated: 0,
456                peak_allocated: 0,
457                total_snapshots: 0,
458                largest_increase: None,
459                patterns: HashMap::new(),
460            };
461        }
462
463        let mut peak_allocated = 0;
464        let mut largest_increase: Option<(String, i64)> = None;
465
466        for i in 0..self.snapshots.len() {
467            let (ref label, ref stats) = self.snapshots[i];
468
469            if stats.allocated > peak_allocated {
470                peak_allocated = stats.allocated;
471            }
472
473            if i > 0 {
474                let prev_stats = &self.snapshots[i - 1].1;
475                let increase = stats.allocated as i64 - prev_stats.allocated as i64;
476
477                if let Some((_, max_increase)) = largest_increase {
478                    if increase > max_increase {
479                        largest_increase = Some((label.clone(), increase));
480                    }
481                } else {
482                    largest_increase = Some((label.clone(), increase));
483                }
484            }
485        }
486
487        let last_allocated = self.snapshots.last().map(|(_, s)| s.allocated).unwrap_or(0);
488
489        AllocationAnalysis {
490            total_allocated: last_allocated,
491            peak_allocated,
492            total_snapshots: self.snapshots.len(),
493            largest_increase,
494            patterns: HashMap::new(),
495        }
496    }
497
498    /// Clear all snapshots
499    pub fn clear(&mut self) {
500        self.snapshots.clear();
501    }
502}
503
504#[cfg(feature = "profiling_memory")]
505impl Default for AllocationTracker {
506    fn default() -> Self {
507        Self::new()
508    }
509}
510
511/// Allocation pattern analysis
512#[cfg(feature = "profiling_memory")]
513#[derive(Debug, Clone)]
514pub struct AllocationAnalysis {
515    pub total_allocated: usize,
516    pub peak_allocated: usize,
517    pub total_snapshots: usize,
518    pub largest_increase: Option<(String, i64)>,
519    pub patterns: HashMap<String, usize>,
520}
521
522/// Enable memory profiling
523#[cfg(feature = "profiling_memory")]
524pub fn enable_profiling() -> CoreResult<()> {
525    // Pure Rust implementation - profiling is always available when feature is enabled.
526    // No special initialization needed (unlike jemalloc which required env vars).
527    Ok(())
528}
529
530/// Disable memory profiling
531#[cfg(feature = "profiling_memory")]
532pub fn disable_profiling() -> CoreResult<()> {
533    // Reset tracked counters
534    TRACKED_ALLOCATED.store(0, Ordering::Relaxed);
535    TRACKED_PEAK.store(0, Ordering::Relaxed);
536    Ok(())
537}
538
539// ============================================================================
540// Stub implementations when profiling_memory feature is disabled
541// ============================================================================
542
543#[cfg(not(feature = "profiling_memory"))]
544use crate::CoreResult;
545
546#[cfg(not(feature = "profiling_memory"))]
547#[derive(Debug, Clone)]
548pub struct MemoryStats {
549    pub allocated: usize,
550    pub resident: usize,
551    pub mapped: usize,
552    pub metadata: usize,
553    pub retained: usize,
554}
555
556#[cfg(not(feature = "profiling_memory"))]
557impl MemoryStats {
558    pub fn current() -> CoreResult<Self> {
559        Ok(Self {
560            allocated: 0,
561            resident: 0,
562            mapped: 0,
563            metadata: 0,
564            retained: 0,
565        })
566    }
567
568    pub fn format(&self) -> String {
569        "Memory profiling not enabled".to_string()
570    }
571}
572
573#[cfg(not(feature = "profiling_memory"))]
574pub struct MemoryProfiler;
575
576#[cfg(not(feature = "profiling_memory"))]
577impl MemoryProfiler {
578    pub fn new() -> Self {
579        Self
580    }
581    pub fn get_stats() -> CoreResult<MemoryStats> {
582        MemoryStats::current()
583    }
584    pub fn print_stats() -> CoreResult<()> {
585        Ok(())
586    }
587}
588
589#[cfg(not(feature = "profiling_memory"))]
590pub fn enable_profiling() -> CoreResult<()> {
591    Ok(())
592}
593
594// ============================================================================
595// Tests
596// ============================================================================
597
598#[cfg(test)]
599#[cfg(feature = "profiling_memory")]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn test_memory_stats() {
605        let stats = MemoryStats::current();
606        assert!(stats.is_ok());
607
608        if let Ok(s) = stats {
609            println!("{}", s.format());
610            // On any platform, resident should be non-zero for a running process
611            assert!(s.resident > 0, "Resident memory should be > 0");
612        }
613    }
614
615    #[test]
616    fn test_memory_profiler() {
617        let mut profiler = MemoryProfiler::new();
618        assert!(profiler.set_baseline().is_ok());
619
620        // Allocate some memory
621        let _vec: Vec<u8> = vec![0; 1_000_000];
622
623        let delta = profiler.get_delta();
624        assert!(delta.is_ok());
625    }
626
627    #[test]
628    fn test_allocation_tracker() {
629        let mut tracker = AllocationTracker::new();
630
631        assert!(tracker.snapshot("baseline").is_ok());
632
633        // Allocate some memory
634        let _vec: Vec<u8> = vec![0; 1_000_000];
635
636        assert!(tracker.snapshot("after_alloc").is_ok());
637
638        let analysis = tracker.analyze();
639        assert_eq!(analysis.total_snapshots, 2);
640    }
641
642    #[test]
643    fn test_memory_delta() {
644        let delta = MemoryDelta {
645            allocated_delta: 1_048_576,
646            resident_delta: 2_097_152,
647            mapped_delta: 0,
648            metadata_delta: 0,
649            retained_delta: 0,
650        };
651
652        let formatted = delta.format();
653        assert!(formatted.contains("Allocated"));
654    }
655
656    #[test]
657    fn test_enable_disable_profiling() {
658        assert!(enable_profiling().is_ok());
659        assert!(disable_profiling().is_ok());
660    }
661
662    #[test]
663    fn test_manual_tracking() {
664        // Reset
665        TRACKED_ALLOCATED.store(0, Ordering::Relaxed);
666        TRACKED_PEAK.store(0, Ordering::Relaxed);
667
668        MemoryProfiler::track_allocation(1024);
669        assert_eq!(get_tracked_allocated(), 1024);
670
671        MemoryProfiler::track_allocation(2048);
672        assert_eq!(get_tracked_allocated(), 3072);
673
674        MemoryProfiler::track_deallocation(1024);
675        assert_eq!(get_tracked_allocated(), 2048);
676
677        // Peak should still be 3072
678        assert_eq!(TRACKED_PEAK.load(Ordering::Relaxed), 3072);
679    }
680
681    #[test]
682    fn test_overhead_and_utilization_ratios() {
683        let stats = MemoryStats {
684            allocated: 1_000_000,
685            resident: 2_000_000,
686            mapped: 4_000_000,
687            metadata: 20_000,
688            retained: 1_000_000,
689        };
690        let overhead = stats.overhead_ratio();
691        assert!((overhead - 0.02).abs() < 1e-6);
692
693        let utilization = stats.utilization_ratio();
694        assert!((utilization - 0.5).abs() < 1e-6);
695    }
696
697    #[test]
698    fn test_zero_stats_ratios() {
699        let stats = MemoryStats {
700            allocated: 0,
701            resident: 0,
702            mapped: 0,
703            metadata: 0,
704            retained: 0,
705        };
706        assert_eq!(stats.overhead_ratio(), 0.0);
707        assert_eq!(stats.utilization_ratio(), 0.0);
708    }
709}