Skip to main content

mabi_core/profiling/
memory.rs

1//! Memory profiling implementation.
2//!
3//! Provides detailed memory usage tracking with region-based accounting,
4//! historical snapshots, and usage analysis.
5
6use std::collections::{HashMap, VecDeque};
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::time::{Duration, Instant};
9
10use parking_lot::RwLock;
11use serde::{Deserialize, Serialize};
12
13/// Memory profiler configuration.
14#[derive(Debug, Clone)]
15pub struct MemoryProfilerConfig {
16    /// Interval between automatic snapshots.
17    pub sampling_interval: Duration,
18
19    /// Maximum number of historical snapshots to retain.
20    pub max_snapshots: usize,
21
22    /// Whether to track individual allocations.
23    pub track_allocations: bool,
24
25    /// Minimum allocation size to track individually (bytes).
26    pub allocation_threshold: usize,
27}
28
29impl Default for MemoryProfilerConfig {
30    fn default() -> Self {
31        Self {
32            sampling_interval: Duration::from_secs(10),
33            max_snapshots: 1000,
34            track_allocations: true,
35            allocation_threshold: 1024,
36        }
37    }
38}
39
40/// Memory profiler for tracking allocations and usage patterns.
41pub struct MemoryProfiler {
42    config: MemoryProfilerConfig,
43
44    /// Memory regions (e.g., "devices", "registers", "connections")
45    regions: RwLock<HashMap<String, MemoryRegion>>,
46
47    /// Historical snapshots
48    snapshots: RwLock<VecDeque<TimestampedSnapshot>>,
49
50    /// Global counters
51    total_allocated: AtomicU64,
52    total_deallocated: AtomicU64,
53    current_bytes: AtomicU64,
54    peak_bytes: AtomicU64,
55    allocation_count: AtomicU64,
56    deallocation_count: AtomicU64,
57
58    /// Profiler state
59    started_at: RwLock<Option<Instant>>,
60    last_snapshot_at: RwLock<Option<Instant>>,
61    is_running: std::sync::atomic::AtomicBool,
62}
63
64impl MemoryProfiler {
65    /// Create a new memory profiler.
66    pub fn new(config: MemoryProfilerConfig) -> Self {
67        Self {
68            config,
69            regions: RwLock::new(HashMap::new()),
70            snapshots: RwLock::new(VecDeque::new()),
71            total_allocated: AtomicU64::new(0),
72            total_deallocated: AtomicU64::new(0),
73            current_bytes: AtomicU64::new(0),
74            peak_bytes: AtomicU64::new(0),
75            allocation_count: AtomicU64::new(0),
76            deallocation_count: AtomicU64::new(0),
77            started_at: RwLock::new(None),
78            last_snapshot_at: RwLock::new(None),
79            is_running: std::sync::atomic::AtomicBool::new(false),
80        }
81    }
82
83    /// Start profiling.
84    pub fn start(&mut self) {
85        let now = Instant::now();
86        *self.started_at.write() = Some(now);
87        *self.last_snapshot_at.write() = Some(now);
88        self.is_running.store(true, Ordering::SeqCst);
89    }
90
91    /// Stop profiling.
92    pub fn stop(&mut self) {
93        self.is_running.store(false, Ordering::SeqCst);
94    }
95
96    /// Check if profiling is active.
97    pub fn is_running(&self) -> bool {
98        self.is_running.load(Ordering::SeqCst)
99    }
100
101    /// Record a memory allocation.
102    pub fn record_allocation(&self, label: &str, size: usize) {
103        let size = size as u64;
104
105        // Update global counters
106        self.total_allocated.fetch_add(size, Ordering::Relaxed);
107        self.allocation_count.fetch_add(1, Ordering::Relaxed);
108
109        let new_current = self.current_bytes.fetch_add(size, Ordering::Relaxed) + size;
110
111        // Update peak if necessary
112        let mut peak = self.peak_bytes.load(Ordering::Relaxed);
113        while new_current > peak {
114            match self.peak_bytes.compare_exchange_weak(
115                peak,
116                new_current,
117                Ordering::Relaxed,
118                Ordering::Relaxed,
119            ) {
120                Ok(_) => break,
121                Err(p) => peak = p,
122            }
123        }
124
125        // Update region
126        let mut regions = self.regions.write();
127        regions
128            .entry(label.to_string())
129            .or_insert_with(|| MemoryRegion::new(label))
130            .record_allocation(size);
131
132        // Check if we need a new snapshot
133        drop(regions);
134        self.maybe_take_snapshot();
135    }
136
137    /// Record a memory deallocation.
138    pub fn record_deallocation(&self, label: &str, size: usize) {
139        let size = size as u64;
140
141        // Update global counters
142        self.total_deallocated.fetch_add(size, Ordering::Relaxed);
143        self.deallocation_count.fetch_add(1, Ordering::Relaxed);
144        self.current_bytes.fetch_sub(size, Ordering::Relaxed);
145
146        // Update region
147        let mut regions = self.regions.write();
148        if let Some(region) = regions.get_mut(label) {
149            region.record_deallocation(size);
150        }
151    }
152
153    /// Take a snapshot if enough time has passed.
154    fn maybe_take_snapshot(&self) {
155        let mut last_snapshot = self.last_snapshot_at.write();
156        if let Some(last) = *last_snapshot {
157            if last.elapsed() >= self.config.sampling_interval {
158                *last_snapshot = Some(Instant::now());
159                drop(last_snapshot);
160
161                let snapshot = TimestampedSnapshot {
162                    timestamp: chrono::Utc::now(),
163                    snapshot: self.snapshot(),
164                };
165
166                let mut snapshots = self.snapshots.write();
167                snapshots.push_back(snapshot);
168
169                // Maintain max snapshots limit
170                while snapshots.len() > self.config.max_snapshots {
171                    snapshots.pop_front();
172                }
173            }
174        }
175    }
176
177    /// Get current memory snapshot.
178    pub fn snapshot(&self) -> MemorySnapshot {
179        let regions = self.regions.read();
180        let region_snapshots: HashMap<String, RegionSnapshot> = regions
181            .iter()
182            .map(|(name, region)| (name.clone(), region.snapshot()))
183            .collect();
184
185        MemorySnapshot {
186            current_bytes: self.current_bytes.load(Ordering::Relaxed),
187            peak_bytes: self.peak_bytes.load(Ordering::Relaxed),
188            total_allocated: self.total_allocated.load(Ordering::Relaxed),
189            total_deallocated: self.total_deallocated.load(Ordering::Relaxed),
190            allocation_count: self.allocation_count.load(Ordering::Relaxed),
191            deallocation_count: self.deallocation_count.load(Ordering::Relaxed),
192            regions: region_snapshots,
193        }
194    }
195
196    /// Get historical snapshots.
197    pub fn history(&self) -> Vec<TimestampedSnapshot> {
198        self.snapshots.read().iter().cloned().collect()
199    }
200
201    /// Generate a memory report.
202    pub fn generate_report(&self) -> MemoryReport {
203        let snapshot = self.snapshot();
204        let history = self.history();
205
206        // Calculate growth rate if we have history
207        let growth_rate = if history.len() >= 2 {
208            let first = &history[0];
209            let last = &history[history.len() - 1];
210            let time_diff = (last.timestamp - first.timestamp).num_seconds() as f64;
211            if time_diff > 0.0 {
212                let byte_diff =
213                    last.snapshot.current_bytes as f64 - first.snapshot.current_bytes as f64;
214                Some(byte_diff / time_diff) // bytes per second
215            } else {
216                None
217            }
218        } else {
219            None
220        };
221
222        MemoryReport {
223            current_bytes: snapshot.current_bytes,
224            peak_bytes: snapshot.peak_bytes,
225            total_allocated: snapshot.total_allocated,
226            total_deallocated: snapshot.total_deallocated,
227            allocation_count: snapshot.allocation_count,
228            deallocation_count: snapshot.deallocation_count,
229            regions: snapshot.regions,
230            growth_rate_bytes_per_sec: growth_rate,
231            snapshot_count: history.len(),
232        }
233    }
234
235    /// Reset all profiling data.
236    pub fn reset(&mut self) {
237        self.regions.write().clear();
238        self.snapshots.write().clear();
239        self.total_allocated.store(0, Ordering::Relaxed);
240        self.total_deallocated.store(0, Ordering::Relaxed);
241        self.current_bytes.store(0, Ordering::Relaxed);
242        self.peak_bytes.store(0, Ordering::Relaxed);
243        self.allocation_count.store(0, Ordering::Relaxed);
244        self.deallocation_count.store(0, Ordering::Relaxed);
245    }
246
247    /// Get memory usage for a specific region.
248    pub fn region_usage(&self, label: &str) -> Option<RegionSnapshot> {
249        self.regions.read().get(label).map(|r| r.snapshot())
250    }
251
252    /// List all tracked regions.
253    pub fn regions(&self) -> Vec<String> {
254        self.regions.read().keys().cloned().collect()
255    }
256}
257
258/// A memory region for tracking allocations by category.
259#[derive(Debug)]
260pub struct MemoryRegion {
261    name: String,
262    current_bytes: AtomicU64,
263    peak_bytes: AtomicU64,
264    total_allocated: AtomicU64,
265    total_deallocated: AtomicU64,
266    allocation_count: AtomicU64,
267    deallocation_count: AtomicU64,
268}
269
270impl MemoryRegion {
271    /// Create a new memory region.
272    pub fn new(name: &str) -> Self {
273        Self {
274            name: name.to_string(),
275            current_bytes: AtomicU64::new(0),
276            peak_bytes: AtomicU64::new(0),
277            total_allocated: AtomicU64::new(0),
278            total_deallocated: AtomicU64::new(0),
279            allocation_count: AtomicU64::new(0),
280            deallocation_count: AtomicU64::new(0),
281        }
282    }
283
284    /// Record an allocation.
285    pub fn record_allocation(&self, size: u64) {
286        self.total_allocated.fetch_add(size, Ordering::Relaxed);
287        self.allocation_count.fetch_add(1, Ordering::Relaxed);
288
289        let new_current = self.current_bytes.fetch_add(size, Ordering::Relaxed) + size;
290
291        // Update peak
292        let mut peak = self.peak_bytes.load(Ordering::Relaxed);
293        while new_current > peak {
294            match self.peak_bytes.compare_exchange_weak(
295                peak,
296                new_current,
297                Ordering::Relaxed,
298                Ordering::Relaxed,
299            ) {
300                Ok(_) => break,
301                Err(p) => peak = p,
302            }
303        }
304    }
305
306    /// Record a deallocation.
307    pub fn record_deallocation(&self, size: u64) {
308        self.total_deallocated.fetch_add(size, Ordering::Relaxed);
309        self.deallocation_count.fetch_add(1, Ordering::Relaxed);
310        self.current_bytes.fetch_sub(size, Ordering::Relaxed);
311    }
312
313    /// Get a snapshot of this region.
314    pub fn snapshot(&self) -> RegionSnapshot {
315        RegionSnapshot {
316            name: self.name.clone(),
317            current_bytes: self.current_bytes.load(Ordering::Relaxed),
318            peak_bytes: self.peak_bytes.load(Ordering::Relaxed),
319            total_allocated: self.total_allocated.load(Ordering::Relaxed),
320            total_deallocated: self.total_deallocated.load(Ordering::Relaxed),
321            allocation_count: self.allocation_count.load(Ordering::Relaxed),
322            deallocation_count: self.deallocation_count.load(Ordering::Relaxed),
323        }
324    }
325}
326
327/// Snapshot of memory state at a point in time.
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct MemorySnapshot {
330    /// Current memory usage in bytes.
331    pub current_bytes: u64,
332
333    /// Peak memory usage in bytes.
334    pub peak_bytes: u64,
335
336    /// Total bytes allocated since start.
337    pub total_allocated: u64,
338
339    /// Total bytes deallocated since start.
340    pub total_deallocated: u64,
341
342    /// Number of allocations.
343    pub allocation_count: u64,
344
345    /// Number of deallocations.
346    pub deallocation_count: u64,
347
348    /// Per-region snapshots.
349    pub regions: HashMap<String, RegionSnapshot>,
350}
351
352/// Timestamped memory snapshot.
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct TimestampedSnapshot {
355    /// When the snapshot was taken.
356    pub timestamp: chrono::DateTime<chrono::Utc>,
357
358    /// The memory snapshot.
359    pub snapshot: MemorySnapshot,
360}
361
362/// Snapshot of a memory region.
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct RegionSnapshot {
365    /// Region name.
366    pub name: String,
367
368    /// Current memory usage in bytes.
369    pub current_bytes: u64,
370
371    /// Peak memory usage in bytes.
372    pub peak_bytes: u64,
373
374    /// Total bytes allocated.
375    pub total_allocated: u64,
376
377    /// Total bytes deallocated.
378    pub total_deallocated: u64,
379
380    /// Number of allocations.
381    pub allocation_count: u64,
382
383    /// Number of deallocations.
384    pub deallocation_count: u64,
385}
386
387impl RegionSnapshot {
388    /// Calculate fragmentation ratio (allocations / deallocations).
389    pub fn fragmentation_ratio(&self) -> f64 {
390        if self.deallocation_count == 0 {
391            return 0.0;
392        }
393        self.allocation_count as f64 / self.deallocation_count as f64
394    }
395
396    /// Calculate average allocation size.
397    pub fn average_allocation_size(&self) -> u64 {
398        if self.allocation_count == 0 {
399            return 0;
400        }
401        self.total_allocated / self.allocation_count
402    }
403}
404
405/// Memory profiling report.
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct MemoryReport {
408    /// Current memory usage in bytes.
409    pub current_bytes: u64,
410
411    /// Peak memory usage in bytes.
412    pub peak_bytes: u64,
413
414    /// Total bytes allocated since profiling started.
415    pub total_allocated: u64,
416
417    /// Total bytes deallocated since profiling started.
418    pub total_deallocated: u64,
419
420    /// Number of allocations.
421    pub allocation_count: u64,
422
423    /// Number of deallocations.
424    pub deallocation_count: u64,
425
426    /// Per-region data.
427    pub regions: HashMap<String, RegionSnapshot>,
428
429    /// Memory growth rate (bytes per second), if calculable.
430    pub growth_rate_bytes_per_sec: Option<f64>,
431
432    /// Number of snapshots taken.
433    pub snapshot_count: usize,
434}
435
436impl MemoryReport {
437    /// Check if memory usage appears stable (no significant growth).
438    pub fn is_stable(&self, threshold_bytes_per_sec: f64) -> bool {
439        match self.growth_rate_bytes_per_sec {
440            Some(rate) => rate.abs() < threshold_bytes_per_sec,
441            None => true, // Not enough data to determine
442        }
443    }
444
445    /// Get the largest region by current usage.
446    pub fn largest_region(&self) -> Option<(&String, &RegionSnapshot)> {
447        self.regions
448            .iter()
449            .max_by_key(|(_, r)| r.current_bytes)
450    }
451
452    /// Calculate memory efficiency (deallocated / allocated).
453    pub fn efficiency(&self) -> f64 {
454        if self.total_allocated == 0 {
455            return 1.0;
456        }
457        self.total_deallocated as f64 / self.total_allocated as f64
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_memory_profiler_basic() {
467        let mut profiler = MemoryProfiler::new(MemoryProfilerConfig::default());
468        profiler.start();
469
470        profiler.record_allocation("test", 1024);
471        assert_eq!(profiler.snapshot().current_bytes, 1024);
472
473        profiler.record_allocation("test", 2048);
474        assert_eq!(profiler.snapshot().current_bytes, 3072);
475
476        profiler.record_deallocation("test", 1024);
477        assert_eq!(profiler.snapshot().current_bytes, 2048);
478    }
479
480    #[test]
481    fn test_memory_profiler_peak() {
482        let mut profiler = MemoryProfiler::new(MemoryProfilerConfig::default());
483        profiler.start();
484
485        profiler.record_allocation("test", 1000);
486        profiler.record_allocation("test", 2000);
487        profiler.record_deallocation("test", 1500);
488
489        let snapshot = profiler.snapshot();
490        assert_eq!(snapshot.current_bytes, 1500);
491        assert_eq!(snapshot.peak_bytes, 3000);
492    }
493
494    #[test]
495    fn test_memory_profiler_regions() {
496        let mut profiler = MemoryProfiler::new(MemoryProfilerConfig::default());
497        profiler.start();
498
499        profiler.record_allocation("devices", 1000);
500        profiler.record_allocation("registers", 2000);
501        profiler.record_allocation("devices", 500);
502
503        let regions = profiler.regions();
504        assert!(regions.contains(&"devices".to_string()));
505        assert!(regions.contains(&"registers".to_string()));
506
507        let device_region = profiler.region_usage("devices").unwrap();
508        assert_eq!(device_region.current_bytes, 1500);
509        assert_eq!(device_region.allocation_count, 2);
510    }
511
512    #[test]
513    fn test_memory_profiler_reset() {
514        let mut profiler = MemoryProfiler::new(MemoryProfilerConfig::default());
515        profiler.start();
516
517        profiler.record_allocation("test", 1024);
518        profiler.reset();
519
520        let snapshot = profiler.snapshot();
521        assert_eq!(snapshot.current_bytes, 0);
522        assert_eq!(snapshot.allocation_count, 0);
523    }
524
525    #[test]
526    fn test_memory_report() {
527        let mut profiler = MemoryProfiler::new(MemoryProfilerConfig::default());
528        profiler.start();
529
530        profiler.record_allocation("a", 1000);
531        profiler.record_allocation("b", 2000);
532        profiler.record_deallocation("a", 500);
533
534        let report = profiler.generate_report();
535        assert_eq!(report.current_bytes, 2500);
536        assert_eq!(report.allocation_count, 2);
537        assert_eq!(report.deallocation_count, 1);
538
539        let largest = report.largest_region();
540        assert!(largest.is_some());
541        assert_eq!(largest.unwrap().0, "b");
542    }
543
544    #[test]
545    fn test_region_snapshot_metrics() {
546        let region = MemoryRegion::new("test");
547        region.record_allocation(100);
548        region.record_allocation(200);
549        region.record_deallocation(50);
550
551        let snapshot = region.snapshot();
552        assert_eq!(snapshot.average_allocation_size(), 150); // (100+200)/2
553        assert_eq!(snapshot.fragmentation_ratio(), 2.0); // 2 allocs / 1 dealloc
554    }
555}