1use super::allocation::{AllocationStats, AllocationTracker, SizeHistogram};
7use crate::memory::{GLOBAL_BUFFER_POOL, GLOBAL_STRING_POOL, PoolStats};
8use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone)]
16pub struct MemorySnapshot {
17 pub timestamp: u64,
19 pub instant: Instant,
21 pub stats: AllocationStats,
23 pub histogram: SizeHistogram,
25 pub pools: PoolSnapshot,
27 pub label: String,
29}
30
31impl MemorySnapshot {
32 pub fn capture(tracker: &AllocationTracker) -> Self {
34 Self {
35 timestamp: SystemTime::now()
36 .duration_since(UNIX_EPOCH)
37 .map(|d| d.as_millis() as u64)
38 .unwrap_or(0),
39 instant: Instant::now(),
40 stats: tracker.stats(),
41 histogram: tracker.histogram(),
42 pools: PoolSnapshot::capture(),
43 label: String::new(),
44 }
45 }
46
47 pub fn capture_labeled(tracker: &AllocationTracker, label: impl Into<String>) -> Self {
49 let mut snap = Self::capture(tracker);
50 snap.label = label.into();
51 snap
52 }
53
54 pub fn diff(&self, other: &MemorySnapshot) -> SnapshotDiff {
56 SnapshotDiff {
57 time_delta: self
58 .instant
59 .checked_duration_since(other.instant)
60 .unwrap_or_default(),
61 allocations_delta: self.stats.total_allocations as i64
62 - other.stats.total_allocations as i64,
63 deallocations_delta: self.stats.total_deallocations as i64
64 - other.stats.total_deallocations as i64,
65 bytes_delta: self.stats.current_bytes as i64 - other.stats.current_bytes as i64,
66 peak_delta: self.stats.peak_bytes as i64 - other.stats.peak_bytes as i64,
67 string_pool_delta: self.pools.string_pool.count as i64
68 - other.pools.string_pool.count as i64,
69 buffer_pool_delta: self.pools.buffer_pool_available as i64
70 - other.pools.buffer_pool_available as i64,
71 from_label: other.label.clone(),
72 to_label: self.label.clone(),
73 }
74 }
75
76 pub fn current_bytes(&self) -> usize {
78 self.stats.current_bytes
79 }
80
81 pub fn peak_bytes(&self) -> usize {
83 self.stats.peak_bytes
84 }
85}
86
87#[derive(Debug, Clone, Default)]
93pub struct PoolSnapshot {
94 pub string_pool: PoolStats,
96 pub buffer_pool_available: usize,
98}
99
100impl PoolSnapshot {
101 pub fn capture() -> Self {
103 Self {
104 string_pool: GLOBAL_STRING_POOL.stats(),
105 buffer_pool_available: GLOBAL_BUFFER_POOL.available(),
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
116pub struct SnapshotDiff {
117 pub time_delta: Duration,
119 pub allocations_delta: i64,
121 pub deallocations_delta: i64,
123 pub bytes_delta: i64,
125 pub peak_delta: i64,
127 pub string_pool_delta: i64,
129 pub buffer_pool_delta: i64,
131 pub from_label: String,
133 pub to_label: String,
135}
136
137impl SnapshotDiff {
138 pub fn has_leaks(&self) -> bool {
140 self.bytes_delta > 0 && self.allocations_delta > self.deallocations_delta
141 }
142
143 pub fn significant_growth(&self, from_bytes: usize) -> bool {
145 if from_bytes == 0 {
146 return self.bytes_delta > 0;
147 }
148 (self.bytes_delta as f64 / from_bytes as f64).abs() > 0.1
149 }
150
151 pub fn net_allocations(&self) -> i64 {
153 self.allocations_delta - self.deallocations_delta
154 }
155
156 pub fn report(&self) -> String {
158 let mut s = String::new();
159
160 if !self.from_label.is_empty() || !self.to_label.is_empty() {
162 s.push_str(&format!(
163 "=== Snapshot Diff: '{}' -> '{}' ===\n",
164 if self.from_label.is_empty() {
165 "start"
166 } else {
167 &self.from_label
168 },
169 if self.to_label.is_empty() {
170 "end"
171 } else {
172 &self.to_label
173 },
174 ));
175 } else {
176 s.push_str("=== Snapshot Diff ===\n");
177 }
178
179 s.push_str(&format!("Time elapsed: {:?}\n\n", self.time_delta));
180
181 s.push_str("Allocations:\n");
183 s.push_str(&format!(
184 " New allocations: {:+}\n",
185 self.allocations_delta
186 ));
187 s.push_str(&format!(
188 " New deallocations: {:+}\n",
189 self.deallocations_delta
190 ));
191 s.push_str(&format!(
192 " Net allocations: {:+}\n",
193 self.net_allocations()
194 ));
195
196 s.push_str("\nMemory:\n");
198 let bytes_str = if self.bytes_delta >= 0 {
199 format!(
200 "+{} bytes (+{:.2} KB)",
201 self.bytes_delta,
202 self.bytes_delta as f64 / 1024.0
203 )
204 } else {
205 format!(
206 "{} bytes ({:.2} KB)",
207 self.bytes_delta,
208 self.bytes_delta as f64 / 1024.0
209 )
210 };
211 s.push_str(&format!(" Current bytes: {}\n", bytes_str));
212
213 let peak_str = if self.peak_delta >= 0 {
214 format!("+{}", self.peak_delta)
215 } else {
216 format!("{}", self.peak_delta)
217 };
218 s.push_str(&format!(" Peak bytes: {}\n", peak_str));
219
220 s.push_str("\nPools:\n");
222 s.push_str(&format!(
223 " String pool entries: {:+}\n",
224 self.string_pool_delta
225 ));
226 s.push_str(&format!(
227 " Buffer pool available: {:+}\n",
228 self.buffer_pool_delta
229 ));
230
231 s.push_str("\nAssessment:\n");
233 if self.has_leaks() {
234 s.push_str(" ⚠️ Potential memory leak detected!\n");
235 s.push_str(&format!(
236 " {} bytes held across {} net allocations\n",
237 self.bytes_delta,
238 self.net_allocations()
239 ));
240 } else if self.bytes_delta > 0 {
241 s.push_str(" ⚡ Memory increased (may be normal caching)\n");
242 } else if self.bytes_delta < 0 {
243 s.push_str(" ✅ Memory decreased (cleanup working)\n");
244 } else {
245 s.push_str(" ✅ No memory change\n");
246 }
247
248 s
249 }
250}
251
252impl std::fmt::Display for SnapshotDiff {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 write!(f, "{}", self.report())
255 }
256}
257
258pub struct SnapshotSeries {
264 snapshots: Vec<MemorySnapshot>,
265 max_snapshots: usize,
266}
267
268impl SnapshotSeries {
269 pub fn new(max_snapshots: usize) -> Self {
271 Self {
272 snapshots: Vec::with_capacity(max_snapshots),
273 max_snapshots,
274 }
275 }
276
277 pub fn add(&mut self, snapshot: MemorySnapshot) {
279 if self.snapshots.len() >= self.max_snapshots {
280 self.snapshots.remove(0);
281 }
282 self.snapshots.push(snapshot);
283 }
284
285 pub fn snapshots(&self) -> &[MemorySnapshot] {
287 &self.snapshots
288 }
289
290 pub fn first(&self) -> Option<&MemorySnapshot> {
292 self.snapshots.first()
293 }
294
295 pub fn last(&self) -> Option<&MemorySnapshot> {
297 self.snapshots.last()
298 }
299
300 pub fn total_diff(&self) -> Option<SnapshotDiff> {
302 match (self.first(), self.last()) {
303 (Some(first), Some(last)) if !std::ptr::eq(first, last) => Some(last.diff(first)),
304 _ => None,
305 }
306 }
307
308 pub fn has_growth_trend(&self) -> bool {
310 if self.snapshots.len() < 3 {
311 return false;
312 }
313
314 let growing = self
316 .snapshots
317 .windows(2)
318 .filter(|w| w[1].stats.current_bytes > w[0].stats.current_bytes)
319 .count();
320
321 growing as f64 / (self.snapshots.len() - 1) as f64 > 0.7
323 }
324
325 pub fn growth_rate(&self) -> f64 {
327 if let Some(diff) = self.total_diff() {
328 if diff.time_delta.as_secs_f64() > 0.0 {
329 return diff.bytes_delta as f64 / diff.time_delta.as_secs_f64();
330 }
331 }
332 0.0
333 }
334
335 pub fn clear(&mut self) {
337 self.snapshots.clear();
338 }
339}
340
341impl Default for SnapshotSeries {
342 fn default() -> Self {
343 Self::new(100)
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::super::allocation::AllocationTracker;
350 use super::*;
351
352 #[test]
353 fn test_memory_snapshot() {
354 let tracker = AllocationTracker::new();
355 let snapshot = MemorySnapshot::capture(&tracker);
356
357 assert!(snapshot.timestamp > 0);
358 }
359
360 #[test]
361 fn test_snapshot_diff() {
362 let diff = SnapshotDiff {
363 time_delta: Duration::from_secs(10),
364 allocations_delta: 100,
365 deallocations_delta: 80,
366 bytes_delta: 2000,
367 peak_delta: 500,
368 string_pool_delta: 5,
369 buffer_pool_delta: -2,
370 from_label: "start".to_string(),
371 to_label: "end".to_string(),
372 };
373
374 assert_eq!(diff.net_allocations(), 20);
375 assert!(diff.has_leaks());
376 }
377
378 #[test]
379 fn test_snapshot_diff_no_leaks() {
380 let diff = SnapshotDiff {
381 time_delta: Duration::from_secs(10),
382 allocations_delta: 100,
383 deallocations_delta: 100,
384 bytes_delta: 0,
385 peak_delta: 0,
386 string_pool_delta: 0,
387 buffer_pool_delta: 0,
388 from_label: String::new(),
389 to_label: String::new(),
390 };
391
392 assert!(!diff.has_leaks());
393 }
394
395 #[test]
396 fn test_snapshot_series() {
397 let tracker = AllocationTracker::new();
398 let mut series = SnapshotSeries::new(5);
399
400 for i in 0..3 {
401 let mut snap = MemorySnapshot::capture(&tracker);
402 snap.label = format!("snap_{}", i);
403 series.add(snap);
404 }
405
406 assert_eq!(series.snapshots().len(), 3);
407 assert_eq!(series.first().unwrap().label, "snap_0");
408 assert_eq!(series.last().unwrap().label, "snap_2");
409 }
410
411 #[test]
412 fn test_snapshot_series_max_capacity() {
413 let tracker = AllocationTracker::new();
414 let mut series = SnapshotSeries::new(3);
415
416 for i in 0..5 {
417 let mut snap = MemorySnapshot::capture(&tracker);
418 snap.label = format!("snap_{}", i);
419 series.add(snap);
420 }
421
422 assert_eq!(series.snapshots().len(), 3);
423 assert_eq!(series.first().unwrap().label, "snap_2");
424 assert_eq!(series.last().unwrap().label, "snap_4");
425 }
426
427 #[test]
428 fn test_snapshot_diff_report() {
429 let diff = SnapshotDiff {
430 time_delta: Duration::from_secs(10),
431 allocations_delta: 100,
432 deallocations_delta: 80,
433 bytes_delta: 2000,
434 peak_delta: 500,
435 string_pool_delta: 5,
436 buffer_pool_delta: -2,
437 from_label: "before".to_string(),
438 to_label: "after".to_string(),
439 };
440
441 let report = diff.report();
442 assert!(report.contains("before"));
443 assert!(report.contains("after"));
444 assert!(report.contains("+2000 bytes"));
445 assert!(report.contains("Potential memory leak"));
446 }
447}