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.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/// Read memory info on Windows via `K32GetProcessMemoryInfo` (kernel32).
212///
213/// `WorkingSetSize` is the Windows equivalent of RSS, and `PagefileUsage` is the
214/// private commit charge, which is the closest analogue to the Unix virtual size
215/// reported above. `K32GetProcessMemoryInfo` lives in kernel32.dll on Windows 7
216/// and later, so no psapi.dll import library is required.
217#[cfg(all(feature = "profiling_memory", target_os = "windows"))]
218fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
219    // PROCESS_MEMORY_COUNTERS as declared in psapi.h. Field order and types are
220    // part of the stable Win32 ABI.
221    #[repr(C)]
222    #[derive(Default)]
223    struct ProcessMemoryCounters {
224        cb: u32,
225        page_fault_count: u32,
226        peak_working_set_size: usize,
227        working_set_size: usize,
228        quota_peak_paged_pool_usage: usize,
229        quota_paged_pool_usage: usize,
230        quota_peak_non_paged_pool_usage: usize,
231        quota_non_paged_pool_usage: usize,
232        pagefile_usage: usize,
233        peak_pagefile_usage: usize,
234    }
235
236    // Both symbols are exported from kernel32.dll (Windows 7+ for the K32 form).
237    #[link(name = "kernel32")]
238    unsafe extern "system" {
239        fn GetCurrentProcess() -> *mut core::ffi::c_void;
240        fn K32GetProcessMemoryInfo(
241            process: *mut core::ffi::c_void,
242            counters: *mut ProcessMemoryCounters,
243            cb: u32,
244        ) -> i32;
245    }
246
247    let mut counters = ProcessMemoryCounters {
248        cb: core::mem::size_of::<ProcessMemoryCounters>() as u32,
249        ..Default::default()
250    };
251
252    // SAFETY: `counters` is a live, correctly sized PROCESS_MEMORY_COUNTERS and
253    // `cb` matches its size; the pseudo-handle from GetCurrentProcess is always
254    // valid and needs no closing.
255    let ok = unsafe {
256        K32GetProcessMemoryInfo(
257            GetCurrentProcess(),
258            &mut counters,
259            core::mem::size_of::<ProcessMemoryCounters>() as u32,
260        )
261    };
262
263    if ok == 0 {
264        return Err(crate::CoreError::ConfigError(
265            crate::error::ErrorContext::new(
266                "K32GetProcessMemoryInfo failed to report process memory counters".to_string(),
267            ),
268        ));
269    }
270
271    Ok(OsMemoryInfo {
272        resident: counters.working_set_size,
273        virtual_size: counters.pagefile_usage,
274    })
275}
276
277/// Fallback for other platforms - returns estimates from atomic tracking
278#[cfg(all(
279    feature = "profiling_memory",
280    not(target_os = "macos"),
281    not(target_os = "linux"),
282    not(target_os = "windows")
283))]
284fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
285    // On unsupported platforms, use the tracked allocation as a rough estimate
286    let allocated = get_tracked_allocated();
287    Ok(OsMemoryInfo {
288        resident: allocated,
289        virtual_size: allocated,
290    })
291}
292
293// ============================================================================
294// Public API - MemoryStats
295// ============================================================================
296
297/// Memory statistics gathered from OS APIs (Pure Rust)
298#[cfg(feature = "profiling_memory")]
299#[derive(Debug, Clone)]
300pub struct MemoryStats {
301    /// Total allocated memory tracked by the profiler (bytes)
302    pub allocated: usize,
303    /// Resident memory from OS (physical memory, bytes)
304    pub resident: usize,
305    /// Mapped/virtual memory from OS (bytes)
306    pub mapped: usize,
307    /// Metadata overhead estimate (bytes) - estimated as a fraction of allocated
308    pub metadata: usize,
309    /// Retained memory estimate (bytes) - difference between resident and allocated
310    pub retained: usize,
311}
312
313#[cfg(feature = "profiling_memory")]
314impl MemoryStats {
315    /// Get current memory statistics using OS APIs
316    pub fn current() -> CoreResult<Self> {
317        let os_info = read_os_memory_info()?;
318        let tracked = get_tracked_allocated();
319
320        // Use the larger of OS-reported resident and our tracked value
321        // (tracked may be 0 if no allocator wrapper is installed)
322        let allocated = if tracked > 0 {
323            tracked
324        } else {
325            os_info.resident
326        };
327
328        // Estimate metadata as ~2% of allocated (typical allocator overhead)
329        let metadata = allocated / 50;
330
331        // Retained is memory the allocator holds but hasn't returned to the OS
332        let retained = os_info.resident.saturating_sub(allocated);
333
334        Ok(Self {
335            allocated,
336            resident: os_info.resident,
337            mapped: os_info.virtual_size,
338            metadata,
339            retained,
340        })
341    }
342
343    /// Calculate memory overhead (metadata / allocated)
344    pub fn overhead_ratio(&self) -> f64 {
345        if self.allocated == 0 {
346            0.0
347        } else {
348            self.metadata as f64 / self.allocated as f64
349        }
350    }
351
352    /// Calculate memory utilization (allocated / resident)
353    pub fn utilization_ratio(&self) -> f64 {
354        if self.resident == 0 {
355            0.0
356        } else {
357            self.allocated as f64 / self.resident as f64
358        }
359    }
360
361    /// Format as human-readable string
362    pub fn format(&self) -> String {
363        format!(
364            "Memory Stats:\n\
365             - Allocated: {} MB\n\
366             - Resident:  {} MB\n\
367             - Mapped:    {} MB\n\
368             - Metadata:  {} MB\n\
369             - Retained:  {} MB\n\
370             - Overhead:  {:.2}%\n\
371             - Utilization: {:.2}%",
372            self.allocated / 1_048_576,
373            self.resident / 1_048_576,
374            self.mapped / 1_048_576,
375            self.metadata / 1_048_576,
376            self.retained / 1_048_576,
377            self.overhead_ratio() * 100.0,
378            self.utilization_ratio() * 100.0
379        )
380    }
381}
382
383// ============================================================================
384// Public API - MemoryProfiler
385// ============================================================================
386
387/// Memory profiler
388#[cfg(feature = "profiling_memory")]
389pub struct MemoryProfiler {
390    baseline: Option<MemoryStats>,
391}
392
393#[cfg(feature = "profiling_memory")]
394impl MemoryProfiler {
395    /// Create a new memory profiler
396    pub fn new() -> Self {
397        Self { baseline: None }
398    }
399
400    /// Set the baseline memory statistics
401    pub fn set_baseline(&mut self) -> CoreResult<()> {
402        self.baseline = Some(MemoryStats::current()?);
403        Ok(())
404    }
405
406    /// Get current memory statistics
407    pub fn get_stats() -> CoreResult<MemoryStats> {
408        MemoryStats::current()
409    }
410
411    /// Get memory delta from baseline
412    pub fn get_delta(&self) -> CoreResult<Option<MemoryDelta>> {
413        if let Some(ref baseline) = self.baseline {
414            let current = MemoryStats::current()?;
415            Ok(Some(MemoryDelta {
416                allocated_delta: current.allocated as i64 - baseline.allocated as i64,
417                resident_delta: current.resident as i64 - baseline.resident as i64,
418                mapped_delta: current.mapped as i64 - baseline.mapped as i64,
419                metadata_delta: current.metadata as i64 - baseline.metadata as i64,
420                retained_delta: current.retained as i64 - baseline.retained as i64,
421            }))
422        } else {
423            Ok(None)
424        }
425    }
426
427    /// Print memory statistics
428    pub fn print_stats() -> CoreResult<()> {
429        let stats = Self::get_stats()?;
430        println!("{}", stats.format());
431        Ok(())
432    }
433
434    /// Manually record an allocation for tracking purposes
435    pub fn track_allocation(size: usize) {
436        record_allocation(size);
437    }
438
439    /// Manually record a deallocation for tracking purposes
440    pub fn track_deallocation(size: usize) {
441        record_deallocation(size);
442    }
443}
444
445#[cfg(feature = "profiling_memory")]
446impl Default for MemoryProfiler {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452// ============================================================================
453// Public API - MemoryDelta
454// ============================================================================
455
456/// Memory delta from baseline
457#[cfg(feature = "profiling_memory")]
458#[derive(Debug, Clone)]
459pub struct MemoryDelta {
460    pub allocated_delta: i64,
461    pub resident_delta: i64,
462    pub mapped_delta: i64,
463    pub metadata_delta: i64,
464    pub retained_delta: i64,
465}
466
467#[cfg(feature = "profiling_memory")]
468impl MemoryDelta {
469    /// Format as human-readable string
470    pub fn format(&self) -> String {
471        format!(
472            "Memory Delta:\n\
473             - Allocated: {:+} MB\n\
474             - Resident:  {:+} MB\n\
475             - Mapped:    {:+} MB\n\
476             - Metadata:  {:+} MB\n\
477             - Retained:  {:+} MB",
478            self.allocated_delta / 1_048_576,
479            self.resident_delta / 1_048_576,
480            self.mapped_delta / 1_048_576,
481            self.metadata_delta / 1_048_576,
482            self.retained_delta / 1_048_576
483        )
484    }
485}
486
487// ============================================================================
488// Public API - AllocationTracker
489// ============================================================================
490
491/// Allocation tracker for detecting patterns
492#[cfg(feature = "profiling_memory")]
493pub struct AllocationTracker {
494    snapshots: Vec<(String, MemoryStats)>,
495}
496
497#[cfg(feature = "profiling_memory")]
498impl AllocationTracker {
499    /// Create a new allocation tracker
500    pub fn new() -> Self {
501        Self {
502            snapshots: Vec::new(),
503        }
504    }
505
506    /// Take a snapshot with a label
507    pub fn snapshot(&mut self, label: impl Into<String>) -> CoreResult<()> {
508        let stats = MemoryStats::current()?;
509        self.snapshots.push((label.into(), stats));
510        Ok(())
511    }
512
513    /// Get all snapshots
514    pub fn snapshots(&self) -> &[(String, MemoryStats)] {
515        &self.snapshots
516    }
517
518    /// Analyze allocation patterns
519    pub fn analyze(&self) -> AllocationAnalysis {
520        if self.snapshots.is_empty() {
521            return AllocationAnalysis {
522                total_allocated: 0,
523                peak_allocated: 0,
524                total_snapshots: 0,
525                largest_increase: None,
526                patterns: HashMap::new(),
527            };
528        }
529
530        let mut peak_allocated = 0;
531        let mut largest_increase: Option<(String, i64)> = None;
532
533        for i in 0..self.snapshots.len() {
534            let (ref label, ref stats) = self.snapshots[i];
535
536            if stats.allocated > peak_allocated {
537                peak_allocated = stats.allocated;
538            }
539
540            if i > 0 {
541                let prev_stats = &self.snapshots[i - 1].1;
542                let increase = stats.allocated as i64 - prev_stats.allocated as i64;
543
544                if let Some((_, max_increase)) = largest_increase {
545                    if increase > max_increase {
546                        largest_increase = Some((label.clone(), increase));
547                    }
548                } else {
549                    largest_increase = Some((label.clone(), increase));
550                }
551            }
552        }
553
554        let last_allocated = self.snapshots.last().map(|(_, s)| s.allocated).unwrap_or(0);
555
556        AllocationAnalysis {
557            total_allocated: last_allocated,
558            peak_allocated,
559            total_snapshots: self.snapshots.len(),
560            largest_increase,
561            patterns: HashMap::new(),
562        }
563    }
564
565    /// Clear all snapshots
566    pub fn clear(&mut self) {
567        self.snapshots.clear();
568    }
569}
570
571#[cfg(feature = "profiling_memory")]
572impl Default for AllocationTracker {
573    fn default() -> Self {
574        Self::new()
575    }
576}
577
578/// Allocation pattern analysis
579#[cfg(feature = "profiling_memory")]
580#[derive(Debug, Clone)]
581pub struct AllocationAnalysis {
582    pub total_allocated: usize,
583    pub peak_allocated: usize,
584    pub total_snapshots: usize,
585    pub largest_increase: Option<(String, i64)>,
586    pub patterns: HashMap<String, usize>,
587}
588
589/// Enable memory profiling
590#[cfg(feature = "profiling_memory")]
591pub fn enable_profiling() -> CoreResult<()> {
592    // Pure Rust implementation - profiling is always available when feature is enabled.
593    // No special initialization needed (unlike jemalloc which required env vars).
594    Ok(())
595}
596
597/// Disable memory profiling
598#[cfg(feature = "profiling_memory")]
599pub fn disable_profiling() -> CoreResult<()> {
600    // Reset tracked counters
601    TRACKED_ALLOCATED.store(0, Ordering::Relaxed);
602    TRACKED_PEAK.store(0, Ordering::Relaxed);
603    Ok(())
604}
605
606// ============================================================================
607// Stub implementations when profiling_memory feature is disabled
608// ============================================================================
609
610#[cfg(not(feature = "profiling_memory"))]
611use crate::CoreResult;
612
613#[cfg(not(feature = "profiling_memory"))]
614#[derive(Debug, Clone)]
615pub struct MemoryStats {
616    pub allocated: usize,
617    pub resident: usize,
618    pub mapped: usize,
619    pub metadata: usize,
620    pub retained: usize,
621}
622
623#[cfg(not(feature = "profiling_memory"))]
624impl MemoryStats {
625    pub fn current() -> CoreResult<Self> {
626        Ok(Self {
627            allocated: 0,
628            resident: 0,
629            mapped: 0,
630            metadata: 0,
631            retained: 0,
632        })
633    }
634
635    pub fn format(&self) -> String {
636        "Memory profiling not enabled".to_string()
637    }
638}
639
640#[cfg(not(feature = "profiling_memory"))]
641pub struct MemoryProfiler;
642
643#[cfg(not(feature = "profiling_memory"))]
644impl MemoryProfiler {
645    pub fn new() -> Self {
646        Self
647    }
648    pub fn get_stats() -> CoreResult<MemoryStats> {
649        MemoryStats::current()
650    }
651    pub fn print_stats() -> CoreResult<()> {
652        Ok(())
653    }
654}
655
656#[cfg(not(feature = "profiling_memory"))]
657pub fn enable_profiling() -> CoreResult<()> {
658    Ok(())
659}
660
661// ============================================================================
662// Tests
663// ============================================================================
664
665#[cfg(test)]
666#[cfg(feature = "profiling_memory")]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_memory_stats() {
672        let stats = MemoryStats::current();
673        assert!(stats.is_ok());
674
675        if let Ok(s) = stats {
676            println!("{}", s.format());
677            // On any platform, resident should be non-zero for a running process
678            assert!(s.resident > 0, "Resident memory should be > 0");
679        }
680    }
681
682    #[test]
683    fn test_memory_profiler() {
684        let mut profiler = MemoryProfiler::new();
685        assert!(profiler.set_baseline().is_ok());
686
687        // Allocate some memory
688        let _vec: Vec<u8> = vec![0; 1_000_000];
689
690        let delta = profiler.get_delta();
691        assert!(delta.is_ok());
692    }
693
694    #[test]
695    fn test_allocation_tracker() {
696        let mut tracker = AllocationTracker::new();
697
698        assert!(tracker.snapshot("baseline").is_ok());
699
700        // Allocate some memory
701        let _vec: Vec<u8> = vec![0; 1_000_000];
702
703        assert!(tracker.snapshot("after_alloc").is_ok());
704
705        let analysis = tracker.analyze();
706        assert_eq!(analysis.total_snapshots, 2);
707    }
708
709    #[test]
710    fn test_memory_delta() {
711        let delta = MemoryDelta {
712            allocated_delta: 1_048_576,
713            resident_delta: 2_097_152,
714            mapped_delta: 0,
715            metadata_delta: 0,
716            retained_delta: 0,
717        };
718
719        let formatted = delta.format();
720        assert!(formatted.contains("Allocated"));
721    }
722
723    #[test]
724    fn test_enable_disable_profiling() {
725        assert!(enable_profiling().is_ok());
726        assert!(disable_profiling().is_ok());
727    }
728
729    #[test]
730    fn test_manual_tracking() {
731        // Reset
732        TRACKED_ALLOCATED.store(0, Ordering::Relaxed);
733        TRACKED_PEAK.store(0, Ordering::Relaxed);
734
735        MemoryProfiler::track_allocation(1024);
736        assert_eq!(get_tracked_allocated(), 1024);
737
738        MemoryProfiler::track_allocation(2048);
739        assert_eq!(get_tracked_allocated(), 3072);
740
741        MemoryProfiler::track_deallocation(1024);
742        assert_eq!(get_tracked_allocated(), 2048);
743
744        // Peak should still be 3072
745        assert_eq!(TRACKED_PEAK.load(Ordering::Relaxed), 3072);
746    }
747
748    #[test]
749    fn test_overhead_and_utilization_ratios() {
750        let stats = MemoryStats {
751            allocated: 1_000_000,
752            resident: 2_000_000,
753            mapped: 4_000_000,
754            metadata: 20_000,
755            retained: 1_000_000,
756        };
757        let overhead = stats.overhead_ratio();
758        assert!((overhead - 0.02).abs() < 1e-6);
759
760        let utilization = stats.utilization_ratio();
761        assert!((utilization - 0.5).abs() < 1e-6);
762    }
763
764    #[test]
765    fn test_zero_stats_ratios() {
766        let stats = MemoryStats {
767            allocated: 0,
768            resident: 0,
769            mapped: 0,
770            metadata: 0,
771            retained: 0,
772        };
773        assert_eq!(stats.overhead_ratio(), 0.0);
774        assert_eq!(stats.utilization_ratio(), 0.0);
775    }
776}