Skip to main content

prax_query/profiling/
snapshot.rs

1//! Memory snapshots for comparing state over time.
2//!
3//! Snapshots capture the complete memory state at a point in time,
4//! allowing you to compare before/after states and identify changes.
5
6use 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// ============================================================================
11// Memory Snapshot
12// ============================================================================
13
14/// A point-in-time snapshot of memory state.
15#[derive(Debug, Clone)]
16pub struct MemorySnapshot {
17    /// Unix timestamp in milliseconds.
18    pub timestamp: u64,
19    /// Instant for duration calculations.
20    pub instant: Instant,
21    /// Allocation statistics.
22    pub stats: AllocationStats,
23    /// Size histogram.
24    pub histogram: SizeHistogram,
25    /// Pool snapshot.
26    pub pools: PoolSnapshot,
27    /// Label for this snapshot.
28    pub label: String,
29}
30
31impl MemorySnapshot {
32    /// Capture a new snapshot.
33    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    /// Capture a labeled snapshot.
48    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    /// Compare this snapshot with another (self - other).
55    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    /// Get total bytes currently allocated.
77    pub fn current_bytes(&self) -> usize {
78        self.stats.current_bytes
79    }
80
81    /// Get peak bytes allocated.
82    pub fn peak_bytes(&self) -> usize {
83        self.stats.peak_bytes
84    }
85}
86
87// ============================================================================
88// Pool Snapshot
89// ============================================================================
90
91/// Snapshot of memory pools.
92#[derive(Debug, Clone, Default)]
93pub struct PoolSnapshot {
94    /// String pool statistics.
95    pub string_pool: PoolStats,
96    /// Number of available buffers in buffer pool.
97    pub buffer_pool_available: usize,
98}
99
100impl PoolSnapshot {
101    /// Capture current pool state.
102    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// ============================================================================
111// Snapshot Diff
112// ============================================================================
113
114/// Difference between two memory snapshots.
115#[derive(Debug, Clone)]
116pub struct SnapshotDiff {
117    /// Time between snapshots.
118    pub time_delta: Duration,
119    /// Change in allocation count.
120    pub allocations_delta: i64,
121    /// Change in deallocation count.
122    pub deallocations_delta: i64,
123    /// Change in current bytes.
124    pub bytes_delta: i64,
125    /// Change in peak bytes.
126    pub peak_delta: i64,
127    /// Change in string pool size.
128    pub string_pool_delta: i64,
129    /// Change in buffer pool available.
130    pub buffer_pool_delta: i64,
131    /// Label of "from" snapshot.
132    pub from_label: String,
133    /// Label of "to" snapshot.
134    pub to_label: String,
135}
136
137impl SnapshotDiff {
138    /// Check if there are potential leaks (positive byte delta).
139    pub fn has_leaks(&self) -> bool {
140        self.bytes_delta > 0 && self.allocations_delta > self.deallocations_delta
141    }
142
143    /// Check if memory grew significantly (>10%).
144    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    /// Get net allocation change.
152    pub fn net_allocations(&self) -> i64 {
153        self.allocations_delta - self.deallocations_delta
154    }
155
156    /// Generate a report string.
157    pub fn report(&self) -> String {
158        let mut s = String::new();
159
160        // Header
161        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        // Allocations
182        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        // Bytes
197        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        // Pools
221        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        // Assessment
232        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
258// ============================================================================
259// Snapshot Series
260// ============================================================================
261
262/// A series of snapshots over time.
263pub struct SnapshotSeries {
264    snapshots: Vec<MemorySnapshot>,
265    max_snapshots: usize,
266}
267
268impl SnapshotSeries {
269    /// Create a new series with max capacity.
270    pub fn new(max_snapshots: usize) -> Self {
271        Self {
272            snapshots: Vec::with_capacity(max_snapshots),
273            max_snapshots,
274        }
275    }
276
277    /// Add a snapshot to the series.
278    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    /// Get all snapshots.
286    pub fn snapshots(&self) -> &[MemorySnapshot] {
287        &self.snapshots
288    }
289
290    /// Get the first snapshot.
291    pub fn first(&self) -> Option<&MemorySnapshot> {
292        self.snapshots.first()
293    }
294
295    /// Get the last snapshot.
296    pub fn last(&self) -> Option<&MemorySnapshot> {
297        self.snapshots.last()
298    }
299
300    /// Get the diff between first and last snapshots.
301    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    /// Check for memory growth trend.
309    pub fn has_growth_trend(&self) -> bool {
310        if self.snapshots.len() < 3 {
311            return false;
312        }
313
314        // Check if each snapshot has more memory than the previous
315        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        // More than 70% growing = trend
322        growing as f64 / (self.snapshots.len() - 1) as f64 > 0.7
323    }
324
325    /// Get memory growth rate (bytes per second).
326    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    /// Clear all snapshots.
336    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}