tensorlogic_scirs_backend/
memory_profiler.rs1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
10use std::sync::{Arc, Mutex};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AllocationRecord {
15 pub id: usize,
17
18 pub size_bytes: u64,
20
21 pub timestamp_ms: u64,
23
24 pub source: String,
26
27 pub alive: bool,
29
30 pub lifetime_ms: Option<u64>,
32}
33
34#[derive(Clone)]
36pub struct MemoryProfiler {
37 inner: Arc<Mutex<MemoryProfilerInner>>,
38}
39
40struct MemoryProfilerInner {
41 allocations: HashMap<usize, AllocationRecord>,
43
44 next_id: usize,
46
47 start_time: std::time::Instant,
49
50 current_usage: u64,
52
53 peak_usage: u64,
55
56 total_allocations: usize,
58
59 total_deallocations: usize,
61}
62
63impl MemoryProfiler {
64 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 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 pub fn record_deallocation(&self, id: usize) {
107 let mut inner = self.inner.lock().unwrap();
108
109 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; 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 pub fn current_usage(&self) -> u64 {
126 self.inner.lock().unwrap().current_usage
127 }
128
129 pub fn peak_usage(&self) -> u64 {
131 self.inner.lock().unwrap().peak_usage
132 }
133
134 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct MemoryStats {
246 pub current_usage_bytes: u64,
248
249 pub peak_usage_bytes: u64,
251
252 pub total_allocations: usize,
254
255 pub total_deallocations: usize,
257
258 pub active_allocations: usize,
260
261 pub leaked_allocations: usize,
263
264 pub leaked_bytes: u64,
266
267 pub avg_allocation_lifetime_ms: u64,
269}
270
271impl MemoryStats {
272 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 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 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#[derive(Debug)]
320pub struct AtomicMemoryCounter {
321 current_bytes: AtomicU64,
322 peak_bytes: AtomicU64,
323 num_allocations: AtomicUsize,
324}
325
326impl AtomicMemoryCounter {
327 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 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 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 pub fn deallocate(&self, bytes: u64) {
358 self.current_bytes.fetch_sub(bytes, Ordering::Relaxed);
359 }
360
361 pub fn current(&self) -> u64 {
363 self.current_bytes.load(Ordering::Relaxed)
364 }
365
366 pub fn peak(&self) -> u64 {
368 self.peak_bytes.load(Ordering::Relaxed)
369 }
370
371 pub fn num_allocations(&self) -> usize {
373 self.num_allocations.load(Ordering::Relaxed)
374 }
375
376 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); 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); 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}