1#[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#[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#[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 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#[cfg(feature = "profiling_memory")]
77fn record_deallocation(size: usize) {
78 TRACKED_ALLOCATED.fetch_sub(size, Ordering::Relaxed);
79}
80
81#[cfg(feature = "profiling_memory")]
83fn get_tracked_allocated() -> usize {
84 TRACKED_ALLOCATED.load(Ordering::Relaxed)
85}
86
87#[cfg(feature = "profiling_memory")]
93#[derive(Debug, Clone, Default)]
94struct OsMemoryInfo {
95 resident: usize,
97 virtual_size: usize,
99}
100
101#[cfg(all(feature = "profiling_memory", target_os = "macos"))]
103fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
104 use std::mem;
107
108 #[repr(C)]
110 #[derive(Default)]
111 struct MachTaskBasicInfo {
112 virtual_size: u64, resident_size: u64, resident_size_max: u64, user_time: [u32; 2], system_time: [u32; 2], policy: i32, suspend_count: i32, }
120
121 const MACH_TASK_BASIC_INFO: u32 = 20;
122 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 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 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#[cfg(all(feature = "profiling_memory", target_os = "linux"))]
165fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
166 use std::fs;
167
168 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#[cfg(all(feature = "profiling_memory", target_os = "windows"))]
218fn read_os_memory_info() -> CoreResult<OsMemoryInfo> {
219 #[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 #[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 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#[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 let allocated = get_tracked_allocated();
287 Ok(OsMemoryInfo {
288 resident: allocated,
289 virtual_size: allocated,
290 })
291}
292
293#[cfg(feature = "profiling_memory")]
299#[derive(Debug, Clone)]
300pub struct MemoryStats {
301 pub allocated: usize,
303 pub resident: usize,
305 pub mapped: usize,
307 pub metadata: usize,
309 pub retained: usize,
311}
312
313#[cfg(feature = "profiling_memory")]
314impl MemoryStats {
315 pub fn current() -> CoreResult<Self> {
317 let os_info = read_os_memory_info()?;
318 let tracked = get_tracked_allocated();
319
320 let allocated = if tracked > 0 {
323 tracked
324 } else {
325 os_info.resident
326 };
327
328 let metadata = allocated / 50;
330
331 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 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 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 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#[cfg(feature = "profiling_memory")]
389pub struct MemoryProfiler {
390 baseline: Option<MemoryStats>,
391}
392
393#[cfg(feature = "profiling_memory")]
394impl MemoryProfiler {
395 pub fn new() -> Self {
397 Self { baseline: None }
398 }
399
400 pub fn set_baseline(&mut self) -> CoreResult<()> {
402 self.baseline = Some(MemoryStats::current()?);
403 Ok(())
404 }
405
406 pub fn get_stats() -> CoreResult<MemoryStats> {
408 MemoryStats::current()
409 }
410
411 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 pub fn print_stats() -> CoreResult<()> {
429 let stats = Self::get_stats()?;
430 println!("{}", stats.format());
431 Ok(())
432 }
433
434 pub fn track_allocation(size: usize) {
436 record_allocation(size);
437 }
438
439 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#[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 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#[cfg(feature = "profiling_memory")]
493pub struct AllocationTracker {
494 snapshots: Vec<(String, MemoryStats)>,
495}
496
497#[cfg(feature = "profiling_memory")]
498impl AllocationTracker {
499 pub fn new() -> Self {
501 Self {
502 snapshots: Vec::new(),
503 }
504 }
505
506 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 pub fn snapshots(&self) -> &[(String, MemoryStats)] {
515 &self.snapshots
516 }
517
518 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 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#[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#[cfg(feature = "profiling_memory")]
591pub fn enable_profiling() -> CoreResult<()> {
592 Ok(())
595}
596
597#[cfg(feature = "profiling_memory")]
599pub fn disable_profiling() -> CoreResult<()> {
600 TRACKED_ALLOCATED.store(0, Ordering::Relaxed);
602 TRACKED_PEAK.store(0, Ordering::Relaxed);
603 Ok(())
604}
605
606#[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#[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 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 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 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 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 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}