1use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use parking_lot::RwLock;
14
15#[derive(Debug, Clone)]
17pub struct MemorySnapshot {
18 pub timestamp: Instant,
20 pub allocated_bytes: u64,
22 pub allocation_count: u64,
24 pub freed_bytes: u64,
26 pub deallocation_count: u64,
28 pub live_bytes: u64,
30 pub live_count: u64,
32 pub peak_bytes: u64,
34 pub rss_bytes: Option<u64>,
36 pub heap_bytes: Option<u64>,
38}
39
40impl MemorySnapshot {
41 pub fn live_mb(&self) -> f64 {
43 self.live_bytes as f64 / (1024.0 * 1024.0)
44 }
45
46 pub fn peak_mb(&self) -> f64 {
48 self.peak_bytes as f64 / (1024.0 * 1024.0)
49 }
50
51 pub fn rss_mb(&self) -> Option<f64> {
53 self.rss_bytes.map(|b| b as f64 / (1024.0 * 1024.0))
54 }
55}
56
57pub struct MemoryProfiler {
59 start_time: Instant,
61 snapshots: Arc<RwLock<Vec<MemorySnapshot>>>,
63 tracker: Arc<AllocationTracker>,
65 sample_interval: Duration,
67 active: Arc<AtomicBool>,
69}
70
71use std::sync::atomic::AtomicBool;
72
73impl MemoryProfiler {
74 pub fn new() -> Self {
76 Self {
77 start_time: Instant::now(),
78 snapshots: Arc::new(RwLock::new(Vec::new())),
79 tracker: Arc::new(AllocationTracker::new()),
80 sample_interval: Duration::from_secs(1),
81 active: Arc::new(AtomicBool::new(false)),
82 }
83 }
84
85 pub fn with_sample_interval(mut self, interval: Duration) -> Self {
87 self.sample_interval = interval;
88 self
89 }
90
91 pub fn start(&self) -> MemoryProfilerGuard {
93 self.active.store(true, Ordering::SeqCst);
94
95 let snapshots = self.snapshots.clone();
96 let tracker = self.tracker.clone();
97 let interval = self.sample_interval;
98 let active = self.active.clone();
99
100 tokio::spawn(async move {
102 while active.load(Ordering::Relaxed) {
103 let snapshot = Self::take_snapshot_internal(&tracker);
104 snapshots.write().push(snapshot);
105 tokio::time::sleep(interval).await;
106 }
107 });
108
109 MemoryProfilerGuard {
110 profiler: self,
111 }
112 }
113
114 pub fn snapshot(&self) -> MemorySnapshot {
116 Self::take_snapshot_internal(&self.tracker)
117 }
118
119 fn take_snapshot_internal(tracker: &AllocationTracker) -> MemorySnapshot {
120 let allocated = tracker.allocated_bytes.load(Ordering::Relaxed);
121 let alloc_count = tracker.allocation_count.load(Ordering::Relaxed);
122 let freed = tracker.freed_bytes.load(Ordering::Relaxed);
123 let free_count = tracker.deallocation_count.load(Ordering::Relaxed);
124 let peak = tracker.peak_bytes.load(Ordering::Relaxed);
125
126 let live_bytes = allocated.saturating_sub(freed);
127 let live_count = alloc_count.saturating_sub(free_count);
128
129 MemorySnapshot {
130 timestamp: Instant::now(),
131 allocated_bytes: allocated,
132 allocation_count: alloc_count,
133 freed_bytes: freed,
134 deallocation_count: free_count,
135 live_bytes,
136 live_count,
137 peak_bytes: peak,
138 rss_bytes: Self::get_rss(),
139 heap_bytes: None,
140 }
141 }
142
143 #[cfg(target_os = "linux")]
145 fn get_rss() -> Option<u64> {
146 std::fs::read_to_string("/proc/self/statm")
147 .ok()
148 .and_then(|s| {
149 let parts: Vec<&str> = s.split_whitespace().collect();
150 parts.get(1)?.parse::<u64>().ok().map(|pages| pages * 4096)
151 })
152 }
153
154 #[cfg(target_os = "macos")]
155 fn get_rss() -> Option<u64> {
156 use std::process::Command;
157
158 let output = Command::new("ps")
159 .args(["-o", "rss=", "-p", &std::process::id().to_string()])
160 .output()
161 .ok()?;
162
163 let rss_kb: u64 = String::from_utf8_lossy(&output.stdout)
164 .trim()
165 .parse()
166 .ok()?;
167
168 Some(rss_kb * 1024)
169 }
170
171 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
172 fn get_rss() -> Option<u64> {
173 None
174 }
175
176 pub fn report(&self) -> MemoryReport {
178 let snapshots = self.snapshots.read().clone();
179 let current = self.snapshot();
180
181 let duration = self.start_time.elapsed();
182
183 let peak_bytes = snapshots.iter()
185 .map(|s| s.live_bytes)
186 .max()
187 .unwrap_or(current.live_bytes);
188
189 let avg_bytes = if !snapshots.is_empty() {
190 snapshots.iter().map(|s| s.live_bytes).sum::<u64>() / snapshots.len() as u64
191 } else {
192 current.live_bytes
193 };
194
195 let potential_leak = if snapshots.len() >= 10 {
197 let recent: Vec<_> = snapshots.iter().rev().take(10).collect();
198 let increasing = recent.windows(2).all(|w| w[0].live_bytes >= w[1].live_bytes);
199 let growth = recent.first().map(|f| f.live_bytes).unwrap_or(0)
200 .saturating_sub(recent.last().map(|l| l.live_bytes).unwrap_or(0));
201 increasing && growth > 1024 * 1024 } else {
203 false
204 };
205
206 MemoryReport {
207 duration,
208 current_snapshot: current,
209 peak_bytes,
210 avg_bytes,
211 snapshots,
212 potential_leak,
213 allocation_rate: self.tracker.allocation_count.load(Ordering::Relaxed) as f64
214 / duration.as_secs_f64(),
215 }
216 }
217
218 pub fn tracker(&self) -> &Arc<AllocationTracker> {
220 &self.tracker
221 }
222}
223
224impl Default for MemoryProfiler {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230pub struct MemoryProfilerGuard<'a> {
232 profiler: &'a MemoryProfiler,
233}
234
235impl<'a> Drop for MemoryProfilerGuard<'a> {
236 fn drop(&mut self) {
237 self.profiler.active.store(false, Ordering::SeqCst);
238 }
239}
240
241pub struct AllocationTracker {
243 pub allocated_bytes: AtomicU64,
244 pub freed_bytes: AtomicU64,
245 pub allocation_count: AtomicU64,
246 pub deallocation_count: AtomicU64,
247 pub peak_bytes: AtomicU64,
248 current_bytes: AtomicU64,
249}
250
251impl AllocationTracker {
252 pub fn new() -> Self {
254 Self {
255 allocated_bytes: AtomicU64::new(0),
256 freed_bytes: AtomicU64::new(0),
257 allocation_count: AtomicU64::new(0),
258 deallocation_count: AtomicU64::new(0),
259 peak_bytes: AtomicU64::new(0),
260 current_bytes: AtomicU64::new(0),
261 }
262 }
263
264 pub fn record_alloc(&self, size: usize) {
266 self.allocated_bytes.fetch_add(size as u64, Ordering::Relaxed);
267 self.allocation_count.fetch_add(1, Ordering::Relaxed);
268
269 let current = self.current_bytes.fetch_add(size as u64, Ordering::Relaxed) + size as u64;
270
271 let mut peak = self.peak_bytes.load(Ordering::Relaxed);
273 while current > peak {
274 match self.peak_bytes.compare_exchange_weak(
275 peak,
276 current,
277 Ordering::Relaxed,
278 Ordering::Relaxed,
279 ) {
280 Ok(_) => break,
281 Err(p) => peak = p,
282 }
283 }
284 }
285
286 pub fn record_dealloc(&self, size: usize) {
288 self.freed_bytes.fetch_add(size as u64, Ordering::Relaxed);
289 self.deallocation_count.fetch_add(1, Ordering::Relaxed);
290 self.current_bytes.fetch_sub(size as u64, Ordering::Relaxed);
291 }
292
293 pub fn live_bytes(&self) -> u64 {
295 self.current_bytes.load(Ordering::Relaxed)
296 }
297
298 pub fn live_count(&self) -> u64 {
300 self.allocation_count.load(Ordering::Relaxed)
301 .saturating_sub(self.deallocation_count.load(Ordering::Relaxed))
302 }
303
304 pub fn reset(&self) {
306 self.allocated_bytes.store(0, Ordering::Relaxed);
307 self.freed_bytes.store(0, Ordering::Relaxed);
308 self.allocation_count.store(0, Ordering::Relaxed);
309 self.deallocation_count.store(0, Ordering::Relaxed);
310 self.peak_bytes.store(0, Ordering::Relaxed);
311 self.current_bytes.store(0, Ordering::Relaxed);
312 }
313}
314
315impl Default for AllocationTracker {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321#[derive(Debug, Clone)]
323pub struct MemoryReport {
324 pub duration: Duration,
326 pub current_snapshot: MemorySnapshot,
328 pub peak_bytes: u64,
330 pub avg_bytes: u64,
332 pub snapshots: Vec<MemorySnapshot>,
334 pub potential_leak: bool,
336 pub allocation_rate: f64,
338}
339
340impl MemoryReport {
341 pub fn peak_mb(&self) -> f64 {
343 self.peak_bytes as f64 / (1024.0 * 1024.0)
344 }
345
346 pub fn avg_mb(&self) -> f64 {
348 self.avg_bytes as f64 / (1024.0 * 1024.0)
349 }
350
351 pub fn current_mb(&self) -> f64 {
353 self.current_snapshot.live_mb()
354 }
355
356 pub fn format(&self) -> String {
358 let mut output = String::new();
359
360 output.push_str("=== Memory Profiling Report ===\n\n");
361 output.push_str(&format!("Duration: {:?}\n", self.duration));
362 output.push_str(&format!("Current Memory: {:.2} MB\n", self.current_mb()));
363 output.push_str(&format!("Peak Memory: {:.2} MB\n", self.peak_mb()));
364 output.push_str(&format!("Average Memory: {:.2} MB\n", self.avg_mb()));
365 output.push_str(&format!("Allocation Rate: {:.2}/sec\n", self.allocation_rate));
366 output.push_str(&format!("Total Allocations: {}\n", self.current_snapshot.allocation_count));
367 output.push_str(&format!("Live Allocations: {}\n", self.current_snapshot.live_count));
368
369 if let Some(rss) = self.current_snapshot.rss_mb() {
370 output.push_str(&format!("RSS: {:.2} MB\n", rss));
371 }
372
373 if self.potential_leak {
374 output.push_str("\n⚠️ POTENTIAL MEMORY LEAK DETECTED!\n");
375 output.push_str("Memory usage has been monotonically increasing.\n");
376 }
377
378 output
379 }
380
381 pub fn check_limits(&self, max_mb: f64) -> bool {
383 self.peak_mb() <= max_mb
384 }
385}
386
387pub fn estimate_memory_usage(devices: usize, points_per_device: usize) -> MemoryEstimate {
389 const DEVICE_OVERHEAD: usize = 512; const POINT_SIZE: usize = 128; const VALUE_SIZE: usize = 32; const REGISTER_ENTRY: usize = 24; let device_memory = devices * DEVICE_OVERHEAD;
396 let point_memory = devices * points_per_device * POINT_SIZE;
397 let value_memory = devices * points_per_device * VALUE_SIZE;
398 let overhead = devices * points_per_device * REGISTER_ENTRY;
399
400 let total = device_memory + point_memory + value_memory + overhead;
401
402 MemoryEstimate {
403 devices,
404 points_per_device,
405 device_memory_bytes: device_memory,
406 point_memory_bytes: point_memory,
407 value_memory_bytes: value_memory,
408 overhead_bytes: overhead,
409 total_bytes: total,
410 }
411}
412
413#[derive(Debug, Clone)]
415pub struct MemoryEstimate {
416 pub devices: usize,
417 pub points_per_device: usize,
418 pub device_memory_bytes: usize,
419 pub point_memory_bytes: usize,
420 pub value_memory_bytes: usize,
421 pub overhead_bytes: usize,
422 pub total_bytes: usize,
423}
424
425impl MemoryEstimate {
426 pub fn total_mb(&self) -> f64 {
428 self.total_bytes as f64 / (1024.0 * 1024.0)
429 }
430
431 pub fn total_gb(&self) -> f64 {
433 self.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
434 }
435
436 pub fn format(&self) -> String {
438 format!(
439 "Memory Estimate for {} devices with {} points each:\n\
440 - Device overhead: {:.2} MB\n\
441 - Point definitions: {:.2} MB\n\
442 - Stored values: {:.2} MB\n\
443 - Data structure overhead: {:.2} MB\n\
444 - Total: {:.2} MB ({:.2} GB)",
445 self.devices,
446 self.points_per_device,
447 self.device_memory_bytes as f64 / (1024.0 * 1024.0),
448 self.point_memory_bytes as f64 / (1024.0 * 1024.0),
449 self.value_memory_bytes as f64 / (1024.0 * 1024.0),
450 self.overhead_bytes as f64 / (1024.0 * 1024.0),
451 self.total_mb(),
452 self.total_gb(),
453 )
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 #[test]
462 fn test_allocation_tracker() {
463 let tracker = AllocationTracker::new();
464
465 tracker.record_alloc(1000);
466 assert_eq!(tracker.live_bytes(), 1000);
467 assert_eq!(tracker.live_count(), 1);
468
469 tracker.record_alloc(500);
470 assert_eq!(tracker.live_bytes(), 1500);
471 assert_eq!(tracker.live_count(), 2);
472
473 tracker.record_dealloc(1000);
474 assert_eq!(tracker.live_bytes(), 500);
475 assert_eq!(tracker.live_count(), 1);
476 }
477
478 #[test]
479 fn test_memory_estimate() {
480 let estimate = estimate_memory_usage(10_000, 100);
481 assert!(estimate.total_mb() > 0.0);
482 println!("{}", estimate.format());
483
484 let large_estimate = estimate_memory_usage(50_000, 100);
485 assert!(large_estimate.total_gb() < 8.0, "Should fit in 8GB limit");
486 }
487
488 #[test]
489 fn test_memory_snapshot() {
490 let snapshot = MemorySnapshot {
491 timestamp: Instant::now(),
492 allocated_bytes: 100 * 1024 * 1024,
493 allocation_count: 10000,
494 freed_bytes: 50 * 1024 * 1024,
495 deallocation_count: 5000,
496 live_bytes: 50 * 1024 * 1024,
497 live_count: 5000,
498 peak_bytes: 75 * 1024 * 1024,
499 rss_bytes: Some(80 * 1024 * 1024),
500 heap_bytes: None,
501 };
502
503 assert!((snapshot.live_mb() - 50.0).abs() < 0.01);
504 assert!((snapshot.peak_mb() - 75.0).abs() < 0.01);
505 }
506}