Skip to main content

memscope_rs/snapshot/
types.rs

1//! Snapshot types - Data structures for memory snapshots
2//!
3//! This module defines the core data structures used by the
4//! SnapshotEngine for representing memory snapshots.
5
6use crate::core::types::TrackKind;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Active allocation information in a snapshot
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ActiveAllocation {
13    /// Memory pointer address (None for Container/Value types)
14    pub ptr: Option<usize>,
15    /// Allocation size in bytes
16    pub size: usize,
17    /// Memory allocation semantic role
18    pub kind: TrackKind,
19    /// Timestamp when this allocation was made
20    pub allocated_at: u64,
21    /// Optional variable name
22    pub var_name: Option<String>,
23    /// Optional type name
24    pub type_name: Option<String>,
25    /// Thread ID that made this allocation
26    pub thread_id: u64,
27    /// Optional call stack hash for clone detection
28    pub call_stack_hash: Option<u64>,
29    /// Module path (from module_path!())
30    pub module_path: Option<String>,
31    /// Stack pointer (for StackOwner types like Arc/Rc)
32    pub stack_ptr: Option<usize>,
33}
34
35/// Memory statistics for a snapshot
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct MemoryStats {
38    /// Total number of allocations in the snapshot
39    pub total_allocations: usize,
40    /// Total number of reallocations
41    pub total_reallocations: usize,
42    /// Total number of deallocations
43    pub total_deallocations: usize,
44    /// Number of deallocations without matching allocations
45    pub unmatched_deallocations: usize,
46    /// Releases that were matched by address only, because the address held several live
47    /// bindings. Non-zero means those releases are a policy choice, not identified
48    /// bindings, and any count derived from them is a lower bound of the evidence.
49    #[serde(default)]
50    pub degraded_releases: usize,
51    /// Number of currently active allocations
52    pub active_allocations: usize,
53    /// Total bytes allocated
54    pub total_allocated: usize,
55    /// Total bytes deallocated
56    pub total_deallocated: usize,
57    /// Currently used memory (sum of active allocations)
58    pub current_memory: usize,
59    /// Peak memory usage observed
60    pub peak_memory: usize,
61}
62
63/// Thread-specific memory statistics
64#[derive(Debug, Clone, Serialize, Deserialize, Default)]
65pub struct ThreadMemoryStats {
66    /// Thread ID
67    pub thread_id: u64,
68    /// Number of allocations by this thread
69    pub allocation_count: usize,
70    /// Total bytes allocated by this thread
71    pub total_allocated: usize,
72    /// Total bytes deallocated by this thread
73    pub total_deallocated: usize,
74    /// Current memory usage by this thread
75    pub current_memory: usize,
76    /// Peak memory usage by this thread
77    pub peak_memory: usize,
78}
79
80/// One of several same-named `MemorySnapshot` types in this crate: this one is the
81/// the canonical point-in-time projection built by the snapshot engine.
82///
83/// Convert at the module boundary instead of passing it across; a global rename is
84/// deliberately avoided (see DATA_ACCURACY_DEVELOPMENT_PLAN ยง14.2).
85/// Memory snapshot - a point-in-time view of memory usage
86#[derive(Debug, Clone, Serialize, Deserialize, Default)]
87pub struct MemorySnapshot {
88    /// Timestamp when this snapshot was taken
89    pub timestamp: u64,
90    /// Overall memory statistics
91    pub stats: MemoryStats,
92    /// Compatibility address lookup; returns the newest binding at an address.
93    pub active_allocations: HashMap<usize, ActiveAllocation>,
94    /// Lossless identity-bearing projection. None denotes a legacy address-only snapshot.
95    #[serde(default)]
96    pub active_bindings: Option<Vec<SnapshotBinding>>,
97    /// Per-thread statistics
98    pub thread_stats: HashMap<u64, ThreadMemoryStats>,
99}
100
101/// An allocation together with its observed binding identity.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SnapshotBinding {
104    pub generation_id: u64,
105    pub binding_serial: Option<u64>,
106    pub allocation: ActiveAllocation,
107}
108
109impl MemorySnapshot {
110    /// Create a new empty snapshot
111    pub fn new() -> Self {
112        Self {
113            timestamp: std::time::SystemTime::now()
114                .duration_since(std::time::UNIX_EPOCH)
115                .unwrap_or_default()
116                .as_nanos() as u64,
117            stats: MemoryStats::default(),
118            active_allocations: HashMap::new(),
119            active_bindings: None,
120            thread_stats: HashMap::new(),
121        }
122    }
123
124    /// Build a MemorySnapshot from a list of AllocationInfo (capture module type)
125    pub fn from_allocation_infos(allocations: Vec<crate::capture::types::AllocationInfo>) -> Self {
126        let mut snapshot = Self::new();
127        let mut thread_stats: HashMap<u64, ThreadMemoryStats> = HashMap::new();
128        let mut current_memory: usize = 0;
129
130        for alloc in allocations {
131            let thread_id = alloc.thread_id_u64;
132
133            let active_alloc = ActiveAllocation {
134                ptr: Some(alloc.ptr),
135                size: alloc.size,
136                kind: TrackKind::HeapOwner {
137                    ptr: alloc.ptr,
138                    size: alloc.size,
139                },
140                allocated_at: alloc.timestamp_alloc,
141                var_name: alloc.var_name,
142                type_name: alloc.type_name,
143                thread_id,
144                call_stack_hash: None,
145                module_path: alloc.module_path,
146                stack_ptr: None,
147            };
148
149            current_memory += alloc.size;
150
151            snapshot.stats.total_allocations += 1;
152            snapshot.stats.total_allocated += alloc.size;
153
154            let thread_stat = thread_stats
155                .entry(thread_id)
156                .or_insert_with(|| ThreadMemoryStats {
157                    thread_id,
158                    allocation_count: 0,
159                    total_allocated: 0,
160                    total_deallocated: 0,
161                    current_memory: 0,
162                    peak_memory: 0,
163                });
164
165            thread_stat.allocation_count += 1;
166            thread_stat.total_allocated += alloc.size;
167            thread_stat.current_memory += alloc.size;
168            if thread_stat.current_memory > thread_stat.peak_memory {
169                thread_stat.peak_memory = thread_stat.current_memory;
170            }
171
172            snapshot.active_allocations.insert(alloc.ptr, active_alloc);
173        }
174
175        snapshot.stats.current_memory = current_memory;
176        snapshot.stats.peak_memory = 0; // Cannot determine peak from current allocations only
177        snapshot.stats.active_allocations = snapshot.active_allocations.len();
178        snapshot.thread_stats = thread_stats;
179
180        snapshot
181    }
182
183    /// Get the number of active allocations
184    pub fn active_count(&self) -> usize {
185        self.active_bindings
186            .as_ref()
187            .map_or(self.active_allocations.len(), Vec::len)
188    }
189
190    /// Iterate all bindings, including overlapping generations at the same address.
191    /// Legacy snapshots continue to use their address-only map.
192    pub fn allocations(&self) -> impl Iterator<Item = &ActiveAllocation> {
193        self.active_bindings
194            .iter()
195            .flatten()
196            .map(|binding| &binding.allocation)
197            .chain(
198                self.active_allocations
199                    .values()
200                    .filter(|_| self.active_bindings.is_none()),
201            )
202    }
203
204    /// Get the current memory usage
205    pub fn current_memory(&self) -> usize {
206        self.stats.current_memory
207    }
208
209    /// Get the peak memory usage
210    pub fn peak_memory(&self) -> usize {
211        self.stats.peak_memory
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_memory_snapshot_new() {
221        let snapshot = MemorySnapshot::new();
222        assert!(snapshot.timestamp > 0);
223        assert_eq!(snapshot.stats.total_allocations, 0);
224        assert!(snapshot.active_allocations.is_empty());
225    }
226
227    #[test]
228    fn test_memory_snapshot_default() {
229        let snapshot = MemorySnapshot::default();
230        assert_eq!(snapshot.timestamp, 0);
231        assert_eq!(snapshot.stats.total_allocations, 0);
232    }
233
234    #[test]
235    fn test_active_allocation_creation() {
236        let alloc = ActiveAllocation {
237            ptr: Some(0x1000),
238            size: 1024,
239            kind: TrackKind::HeapOwner {
240                ptr: 0x1000,
241                size: 1024,
242            },
243            allocated_at: 1000,
244            var_name: Some("test".to_string()),
245            type_name: Some("Vec<u8>".to_string()),
246            thread_id: 1,
247            call_stack_hash: None,
248            module_path: None,
249            stack_ptr: None,
250        };
251
252        assert_eq!(alloc.ptr, Some(0x1000));
253        assert_eq!(alloc.size, 1024);
254        assert_eq!(alloc.var_name, Some("test".to_string()));
255    }
256
257    #[test]
258    fn test_active_allocation_clone() {
259        let alloc = ActiveAllocation {
260            ptr: Some(0x1000),
261            size: 1024,
262            kind: TrackKind::HeapOwner {
263                ptr: 0x1000,
264                size: 1024,
265            },
266            allocated_at: 1000,
267            var_name: None,
268            type_name: None,
269            thread_id: 1,
270            call_stack_hash: None,
271            module_path: None,
272            stack_ptr: None,
273        };
274
275        let cloned = alloc.clone();
276        assert_eq!(cloned.size, alloc.size);
277    }
278
279    #[test]
280    fn test_active_allocation_debug() {
281        let alloc = ActiveAllocation {
282            ptr: Some(0x1000),
283            size: 1024,
284            kind: TrackKind::HeapOwner {
285                ptr: 0x1000,
286                size: 1024,
287            },
288            allocated_at: 1000,
289            var_name: None,
290            type_name: None,
291            thread_id: 1,
292            call_stack_hash: None,
293            module_path: None,
294            stack_ptr: None,
295        };
296
297        let debug_str = format!("{:?}", alloc);
298        assert!(debug_str.contains("ActiveAllocation"));
299    }
300
301    #[test]
302    fn test_memory_stats_default() {
303        let stats = MemoryStats::default();
304        assert_eq!(stats.total_allocations, 0);
305        assert_eq!(stats.total_reallocations, 0);
306        assert_eq!(stats.total_deallocations, 0);
307        assert_eq!(stats.active_allocations, 0);
308        assert_eq!(stats.current_memory, 0);
309        assert_eq!(stats.peak_memory, 0);
310    }
311
312    #[test]
313    fn test_memory_stats_clone() {
314        let stats = MemoryStats {
315            total_allocations: 100,
316            total_reallocations: 10,
317            total_deallocations: 50,
318            unmatched_deallocations: 2,
319            degraded_releases: 0,
320            active_allocations: 50,
321            total_allocated: 1024 * 1024,
322            total_deallocated: 512 * 1024,
323            current_memory: 512 * 1024,
324            peak_memory: 1024 * 1024,
325        };
326
327        let cloned = stats.clone();
328        assert_eq!(cloned.total_allocations, 100);
329        assert_eq!(cloned.peak_memory, 1024 * 1024);
330    }
331
332    #[test]
333    fn test_thread_memory_stats_default() {
334        let stats = ThreadMemoryStats::default();
335        assert_eq!(stats.thread_id, 0);
336        assert_eq!(stats.allocation_count, 0);
337        assert_eq!(stats.current_memory, 0);
338    }
339
340    #[test]
341    fn test_thread_memory_stats_clone() {
342        let stats = ThreadMemoryStats {
343            thread_id: 1,
344            allocation_count: 50,
345            total_allocated: 4096,
346            total_deallocated: 2048,
347            current_memory: 2048,
348            peak_memory: 4096,
349        };
350
351        let cloned = stats.clone();
352        assert_eq!(cloned.thread_id, 1);
353        assert_eq!(cloned.allocation_count, 50);
354    }
355
356    #[test]
357    fn test_memory_snapshot_active_count() {
358        let mut snapshot = MemorySnapshot::new();
359        assert_eq!(snapshot.active_count(), 0);
360
361        snapshot.active_allocations.insert(
362            0x1000,
363            ActiveAllocation {
364                ptr: Some(0x1000),
365                size: 1024,
366                kind: TrackKind::HeapOwner {
367                    ptr: 0x1000,
368                    size: 1024,
369                },
370                allocated_at: 1000,
371                var_name: None,
372                type_name: None,
373                thread_id: 1,
374                call_stack_hash: None,
375                module_path: None,
376                stack_ptr: Some(0x2000), // Add stack_ptr field
377            },
378        );
379
380        assert_eq!(snapshot.active_count(), 1);
381    }
382
383    #[test]
384    fn test_memory_snapshot_current_memory() {
385        let mut snapshot = MemorySnapshot::new();
386        assert_eq!(snapshot.current_memory(), 0);
387
388        snapshot.stats.current_memory = 4096;
389        assert_eq!(snapshot.current_memory(), 4096);
390    }
391
392    #[test]
393    fn test_memory_snapshot_peak_memory() {
394        let mut snapshot = MemorySnapshot::new();
395        assert_eq!(snapshot.peak_memory(), 0);
396
397        snapshot.stats.peak_memory = 8192;
398        assert_eq!(snapshot.peak_memory(), 8192);
399    }
400
401    #[test]
402    fn test_memory_snapshot_serialization() {
403        let snapshot = MemorySnapshot::new();
404
405        let json = serde_json::to_string(&snapshot);
406        assert!(json.is_ok());
407
408        let deserialized: Result<MemorySnapshot, _> = serde_json::from_str(&json.unwrap());
409        assert!(deserialized.is_ok());
410    }
411
412    #[test]
413    fn test_active_allocation_serialization() {
414        let alloc = ActiveAllocation {
415            ptr: Some(0x1000),
416            size: 1024,
417            kind: TrackKind::HeapOwner {
418                ptr: 0x1000,
419                size: 1024,
420            },
421            allocated_at: 1000,
422            var_name: Some("test".to_string()),
423            type_name: Some("i32".to_string()),
424            thread_id: 1,
425            call_stack_hash: Some(12345),
426            module_path: Some("test_module".to_string()),
427            stack_ptr: None,
428        };
429
430        let json = serde_json::to_string(&alloc);
431        assert!(json.is_ok());
432
433        let deserialized: Result<ActiveAllocation, _> = serde_json::from_str(&json.unwrap());
434        assert!(deserialized.is_ok());
435    }
436
437    #[test]
438    fn test_memory_stats_serialization() {
439        let stats = MemoryStats {
440            total_allocations: 100,
441            total_reallocations: 10,
442            total_deallocations: 50,
443            unmatched_deallocations: 2,
444            degraded_releases: 0,
445            active_allocations: 50,
446            total_allocated: 1024,
447            total_deallocated: 512,
448            current_memory: 512,
449            peak_memory: 1024,
450        };
451
452        let json = serde_json::to_string(&stats);
453        assert!(json.is_ok());
454
455        let deserialized: Result<MemoryStats, _> = serde_json::from_str(&json.unwrap());
456        assert!(deserialized.is_ok());
457    }
458}