Skip to main content

tensorlogic_scirs_backend/
memory_profiler.rs

1//! Memory Profiling Utilities for TensorLogic
2//!
3//! This module provides comprehensive memory profiling capabilities for
4//! tracking tensor allocations, detecting memory leaks, and optimizing
5//! memory usage in TensorLogic execution.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
10use std::sync::{Arc, Mutex};
11
12/// Memory allocation record.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AllocationRecord {
15    /// Allocation ID
16    pub id: usize,
17
18    /// Size in bytes
19    pub size_bytes: u64,
20
21    /// Allocation timestamp (relative)
22    pub timestamp_ms: u64,
23
24    /// Source location (tensor name or operation)
25    pub source: String,
26
27    /// Whether this allocation is still alive
28    pub alive: bool,
29
30    /// Duration the allocation was alive (if deallocated)
31    pub lifetime_ms: Option<u64>,
32}
33
34/// Memory profiler that tracks allocations and deallocations.
35#[derive(Clone)]
36pub struct MemoryProfiler {
37    inner: Arc<Mutex<MemoryProfilerInner>>,
38}
39
40struct MemoryProfilerInner {
41    /// All allocation records
42    allocations: HashMap<usize, AllocationRecord>,
43
44    /// Next allocation ID
45    next_id: usize,
46
47    /// Start time for relative timestamps
48    start_time: std::time::Instant,
49
50    /// Current memory usage
51    current_usage: u64,
52
53    /// Peak memory usage
54    peak_usage: u64,
55
56    /// Total allocations
57    total_allocations: usize,
58
59    /// Total deallocations
60    total_deallocations: usize,
61}
62
63impl MemoryProfiler {
64    /// Create a new memory profiler.
65    pub fn new() -> Self {
66        Self {
67            inner: Arc::new(Mutex::new(MemoryProfilerInner {
68                allocations: HashMap::new(),
69                next_id: 0,
70                start_time: std::time::Instant::now(),
71                current_usage: 0,
72                peak_usage: 0,
73                total_allocations: 0,
74                total_deallocations: 0,
75            })),
76        }
77    }
78
79    /// Record a tensor allocation.
80    pub fn record_allocation(&self, size_bytes: u64, source: String) -> usize {
81        let mut inner = self.inner.lock().unwrap();
82
83        let id = inner.next_id;
84        inner.next_id += 1;
85
86        let timestamp_ms = inner.start_time.elapsed().as_millis() as u64;
87
88        let record = AllocationRecord {
89            id,
90            size_bytes,
91            timestamp_ms,
92            source,
93            alive: true,
94            lifetime_ms: None,
95        };
96
97        inner.allocations.insert(id, record);
98        inner.current_usage += size_bytes;
99        inner.peak_usage = inner.peak_usage.max(inner.current_usage);
100        inner.total_allocations += 1;
101
102        id
103    }
104
105    /// Record a tensor deallocation.
106    pub fn record_deallocation(&self, id: usize) {
107        let mut inner = self.inner.lock().unwrap();
108
109        // Extract timestamp before mutable borrow
110        let now = inner.start_time.elapsed().as_millis() as u64;
111
112        if let Some(record) = inner.allocations.get_mut(&id) {
113            if record.alive {
114                let size_bytes = record.size_bytes; // Copy before mutation
115                record.lifetime_ms = Some(now - record.timestamp_ms);
116                record.alive = false;
117
118                inner.current_usage = inner.current_usage.saturating_sub(size_bytes);
119                inner.total_deallocations += 1;
120            }
121        }
122    }
123
124    /// Get current memory usage in bytes.
125    pub fn current_usage(&self) -> u64 {
126        self.inner.lock().unwrap().current_usage
127    }
128
129    /// Get peak memory usage in bytes.
130    pub fn peak_usage(&self) -> u64 {
131        self.inner.lock().unwrap().peak_usage
132    }
133
134    /// Get memory usage statistics.
135    pub fn get_stats(&self) -> MemoryStats {
136        let inner = self.inner.lock().unwrap();
137
138        let active_count = inner.allocations.values().filter(|r| r.alive).count();
139        let leaked_bytes: u64 = inner
140            .allocations
141            .values()
142            .filter(|r| r.alive)
143            .map(|r| r.size_bytes)
144            .sum();
145
146        let avg_lifetime_ms = if inner.total_deallocations > 0 {
147            let total_lifetime: u64 = inner
148                .allocations
149                .values()
150                .filter_map(|r| r.lifetime_ms)
151                .sum();
152            total_lifetime / inner.total_deallocations as u64
153        } else {
154            0
155        };
156
157        MemoryStats {
158            current_usage_bytes: inner.current_usage,
159            peak_usage_bytes: inner.peak_usage,
160            total_allocations: inner.total_allocations,
161            total_deallocations: inner.total_deallocations,
162            active_allocations: active_count,
163            leaked_allocations: active_count,
164            leaked_bytes,
165            avg_allocation_lifetime_ms: avg_lifetime_ms,
166        }
167    }
168
169    /// Get all allocation records.
170    pub fn get_allocations(&self) -> Vec<AllocationRecord> {
171        self.inner
172            .lock()
173            .unwrap()
174            .allocations
175            .values()
176            .cloned()
177            .collect()
178    }
179
180    /// Get active (not yet deallocated) allocations.
181    pub fn get_active_allocations(&self) -> Vec<AllocationRecord> {
182        self.inner
183            .lock()
184            .unwrap()
185            .allocations
186            .values()
187            .filter(|r| r.alive)
188            .cloned()
189            .collect()
190    }
191
192    /// Reset the profiler.
193    pub fn reset(&self) {
194        let mut inner = self.inner.lock().unwrap();
195        inner.allocations.clear();
196        inner.next_id = 0;
197        inner.start_time = std::time::Instant::now();
198        inner.current_usage = 0;
199        inner.peak_usage = 0;
200        inner.total_allocations = 0;
201        inner.total_deallocations = 0;
202    }
203
204    /// Export memory timeline to CSV.
205    pub fn export_timeline(&self) -> String {
206        let inner = self.inner.lock().unwrap();
207
208        let mut csv = String::from("timestamp_ms,event,size_bytes,source\n");
209
210        let mut events: Vec<_> = inner
211            .allocations
212            .values()
213            .flat_map(|r| {
214                let mut evs = vec![(r.timestamp_ms, "alloc", r.size_bytes, r.source.clone())];
215                if let Some(lifetime) = r.lifetime_ms {
216                    evs.push((
217                        r.timestamp_ms + lifetime,
218                        "dealloc",
219                        r.size_bytes,
220                        r.source.clone(),
221                    ));
222                }
223                evs
224            })
225            .collect();
226
227        events.sort_by_key(|(t, _, _, _)| *t);
228
229        for (timestamp, event, size, source) in events {
230            csv.push_str(&format!("{},{},{},{}\n", timestamp, event, size, source));
231        }
232
233        csv
234    }
235}
236
237impl Default for MemoryProfiler {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243/// Memory usage statistics.
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct MemoryStats {
246    /// Current memory usage in bytes
247    pub current_usage_bytes: u64,
248
249    /// Peak memory usage in bytes
250    pub peak_usage_bytes: u64,
251
252    /// Total number of allocations
253    pub total_allocations: usize,
254
255    /// Total number of deallocations
256    pub total_deallocations: usize,
257
258    /// Number of currently active allocations
259    pub active_allocations: usize,
260
261    /// Number of leaked allocations (allocated but not deallocated)
262    pub leaked_allocations: usize,
263
264    /// Total bytes leaked
265    pub leaked_bytes: u64,
266
267    /// Average allocation lifetime in milliseconds
268    pub avg_allocation_lifetime_ms: u64,
269}
270
271impl MemoryStats {
272    /// Get memory efficiency (deallocated / allocated).
273    pub fn memory_efficiency(&self) -> f64 {
274        if self.total_allocations == 0 {
275            1.0
276        } else {
277            self.total_deallocations as f64 / self.total_allocations as f64
278        }
279    }
280
281    /// Get leak rate (leaked / total allocations).
282    pub fn leak_rate(&self) -> f64 {
283        if self.total_allocations == 0 {
284            0.0
285        } else {
286            self.leaked_allocations as f64 / self.total_allocations as f64
287        }
288    }
289
290    /// Format memory usage as human-readable string.
291    pub fn format_usage(&self) -> String {
292        format!(
293            "Current: {} | Peak: {} | Active: {} | Leaked: {}",
294            Self::format_bytes(self.current_usage_bytes),
295            Self::format_bytes(self.peak_usage_bytes),
296            self.active_allocations,
297            Self::format_bytes(self.leaked_bytes)
298        )
299    }
300
301    fn format_bytes(bytes: u64) -> String {
302        const KB: u64 = 1024;
303        const MB: u64 = KB * 1024;
304        const GB: u64 = MB * 1024;
305
306        if bytes >= GB {
307            format!("{:.2} GB", bytes as f64 / GB as f64)
308        } else if bytes >= MB {
309            format!("{:.2} MB", bytes as f64 / MB as f64)
310        } else if bytes >= KB {
311            format!("{:.2} KB", bytes as f64 / KB as f64)
312        } else {
313            format!("{} B", bytes)
314        }
315    }
316}
317
318/// Thread-safe atomic memory counter.
319#[derive(Debug)]
320pub struct AtomicMemoryCounter {
321    current_bytes: AtomicU64,
322    peak_bytes: AtomicU64,
323    num_allocations: AtomicUsize,
324}
325
326impl AtomicMemoryCounter {
327    /// Create a new atomic memory counter.
328    pub fn new() -> Self {
329        Self {
330            current_bytes: AtomicU64::new(0),
331            peak_bytes: AtomicU64::new(0),
332            num_allocations: AtomicUsize::new(0),
333        }
334    }
335
336    /// Record an allocation.
337    pub fn allocate(&self, bytes: u64) {
338        let current = self.current_bytes.fetch_add(bytes, Ordering::Relaxed) + bytes;
339        self.num_allocations.fetch_add(1, Ordering::Relaxed);
340
341        // Update peak using compare-and-swap loop
342        let mut peak = self.peak_bytes.load(Ordering::Relaxed);
343        while current > peak {
344            match self.peak_bytes.compare_exchange_weak(
345                peak,
346                current,
347                Ordering::Relaxed,
348                Ordering::Relaxed,
349            ) {
350                Ok(_) => break,
351                Err(p) => peak = p,
352            }
353        }
354    }
355
356    /// Record a deallocation.
357    pub fn deallocate(&self, bytes: u64) {
358        self.current_bytes.fetch_sub(bytes, Ordering::Relaxed);
359    }
360
361    /// Get current usage.
362    pub fn current(&self) -> u64 {
363        self.current_bytes.load(Ordering::Relaxed)
364    }
365
366    /// Get peak usage.
367    pub fn peak(&self) -> u64 {
368        self.peak_bytes.load(Ordering::Relaxed)
369    }
370
371    /// Get number of allocations.
372    pub fn num_allocations(&self) -> usize {
373        self.num_allocations.load(Ordering::Relaxed)
374    }
375
376    /// Reset the counter.
377    pub fn reset(&self) {
378        self.current_bytes.store(0, Ordering::Relaxed);
379        self.peak_bytes.store(0, Ordering::Relaxed);
380        self.num_allocations.store(0, Ordering::Relaxed);
381    }
382}
383
384impl Default for AtomicMemoryCounter {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_memory_profiler_basic() {
396        let profiler = MemoryProfiler::new();
397
398        let id1 = profiler.record_allocation(1000, "tensor1".to_string());
399        assert_eq!(profiler.current_usage(), 1000);
400        assert_eq!(profiler.peak_usage(), 1000);
401
402        let id2 = profiler.record_allocation(2000, "tensor2".to_string());
403        assert_eq!(profiler.current_usage(), 3000);
404        assert_eq!(profiler.peak_usage(), 3000);
405
406        profiler.record_deallocation(id1);
407        assert_eq!(profiler.current_usage(), 2000);
408        assert_eq!(profiler.peak_usage(), 3000); // Peak doesn't decrease
409
410        profiler.record_deallocation(id2);
411        assert_eq!(profiler.current_usage(), 0);
412    }
413
414    #[test]
415    fn test_memory_stats() {
416        let profiler = MemoryProfiler::new();
417
418        profiler.record_allocation(1000, "tensor1".to_string());
419        let id2 = profiler.record_allocation(2000, "tensor2".to_string());
420        profiler.record_deallocation(id2);
421
422        let stats = profiler.get_stats();
423
424        assert_eq!(stats.total_allocations, 2);
425        assert_eq!(stats.total_deallocations, 1);
426        assert_eq!(stats.active_allocations, 1);
427        assert_eq!(stats.leaked_allocations, 1);
428        assert_eq!(stats.leaked_bytes, 1000);
429    }
430
431    #[test]
432    fn test_memory_efficiency() {
433        let stats = MemoryStats {
434            current_usage_bytes: 0,
435            peak_usage_bytes: 1000,
436            total_allocations: 10,
437            total_deallocations: 8,
438            active_allocations: 2,
439            leaked_allocations: 2,
440            leaked_bytes: 200,
441            avg_allocation_lifetime_ms: 100,
442        };
443
444        assert_eq!(stats.memory_efficiency(), 0.8);
445        assert_eq!(stats.leak_rate(), 0.2);
446    }
447
448    #[test]
449    fn test_active_allocations() {
450        let profiler = MemoryProfiler::new();
451
452        let id1 = profiler.record_allocation(1000, "tensor1".to_string());
453        let _id2 = profiler.record_allocation(2000, "tensor2".to_string());
454
455        assert_eq!(profiler.get_active_allocations().len(), 2);
456
457        profiler.record_deallocation(id1);
458        assert_eq!(profiler.get_active_allocations().len(), 1);
459    }
460
461    #[test]
462    fn test_profiler_reset() {
463        let profiler = MemoryProfiler::new();
464
465        profiler.record_allocation(1000, "tensor1".to_string());
466        assert_eq!(profiler.current_usage(), 1000);
467
468        profiler.reset();
469        assert_eq!(profiler.current_usage(), 0);
470        assert_eq!(profiler.peak_usage(), 0);
471        assert_eq!(profiler.get_allocations().len(), 0);
472    }
473
474    #[test]
475    fn test_export_timeline() {
476        let profiler = MemoryProfiler::new();
477
478        let id1 = profiler.record_allocation(1000, "tensor1".to_string());
479        profiler.record_deallocation(id1);
480
481        let csv = profiler.export_timeline();
482
483        assert!(csv.contains("timestamp_ms,event,size_bytes,source"));
484        assert!(csv.contains("alloc"));
485        assert!(csv.contains("dealloc"));
486    }
487
488    #[test]
489    fn test_atomic_memory_counter() {
490        let counter = AtomicMemoryCounter::new();
491
492        counter.allocate(1000);
493        assert_eq!(counter.current(), 1000);
494        assert_eq!(counter.peak(), 1000);
495        assert_eq!(counter.num_allocations(), 1);
496
497        counter.allocate(2000);
498        assert_eq!(counter.current(), 3000);
499        assert_eq!(counter.peak(), 3000);
500
501        counter.deallocate(1000);
502        assert_eq!(counter.current(), 2000);
503        assert_eq!(counter.peak(), 3000); // Peak doesn't decrease
504
505        counter.reset();
506        assert_eq!(counter.current(), 0);
507        assert_eq!(counter.peak(), 0);
508    }
509
510    #[test]
511    fn test_format_bytes() {
512        assert_eq!(MemoryStats::format_bytes(512), "512 B");
513        assert_eq!(MemoryStats::format_bytes(1024), "1.00 KB");
514        assert_eq!(MemoryStats::format_bytes(1024 * 1024), "1.00 MB");
515        assert_eq!(MemoryStats::format_bytes(1024 * 1024 * 1024), "1.00 GB");
516    }
517}