Skip to main content

memscope_rs/
tracker.rs

1//! Unified Tracker API - Enhanced Version
2//!
3//! This module provides a simple, unified interface for memory tracking
4//! with all the power of the old API and more.
5//!
6//! # Features
7//!
8//! - **Simple API**: `tracker!()` and `track!()` macros
9//! - **Auto-capture**: Automatic variable name and type capture
10//! - **System Monitoring**: CPU, memory monitoring (background thread, zero overhead)
11//! - **Per-thread Tracking**: Independent tracking per thread
12//! - **Sampling**: Configurable sampling rates
13//! - **Hotspot Analysis**: Automatic allocation hotspot detection
14//! - **HTML Dashboard**: Interactive visualization
15//! - **JSON/Binary Export**: Multiple export formats
16//!
17//! # Architecture
18//!
19//! System monitoring runs in a background thread that collects metrics every 100ms.
20//! The `track!` macro only reads atomic values (nanosecond overhead), ensuring
21//! no blocking on data collection.
22//!
23//! # Usage
24//!
25//! ```rust
26//! use memscope_rs::{tracker, track};
27//!
28//! // Simple usage - system monitoring is automatic
29//! let tracker = tracker!();
30//! let my_vec = vec![1, 2, 3];
31//! track!(tracker, my_vec);
32//! // Analyze the tracked allocations
33//! let report = tracker.analyze();
34//!
35//! // Advanced usage with custom sampling
36//! use memscope_rs::tracker::SamplingConfig;
37//! let tracker = tracker!().with_sampling(SamplingConfig::high_performance());
38//! ```
39
40use crate::capture::system_monitor;
41use crate::core::tracker::MemoryTracker;
42use crate::event_store::{EventStore, MemoryEvent};
43use crate::render_engine::export::{export_snapshot_to_json, ExportJsonOptions};
44use crate::snapshot::MemorySnapshot;
45
46use std::collections::HashMap;
47use std::sync::{Arc, Mutex};
48use std::time::{Duration, Instant};
49
50#[derive(Debug, Clone)]
51pub struct SamplingConfig {
52    pub sample_rate: f64,
53    pub capture_call_stack: bool,
54    pub max_stack_depth: usize,
55}
56
57impl Default for SamplingConfig {
58    fn default() -> Self {
59        Self {
60            sample_rate: 1.0,
61            capture_call_stack: false,
62            max_stack_depth: 10,
63        }
64    }
65}
66
67impl SamplingConfig {
68    pub fn demo() -> Self {
69        Self {
70            sample_rate: 0.1,
71            capture_call_stack: false,
72            max_stack_depth: 5,
73        }
74    }
75
76    pub fn full() -> Self {
77        Self {
78            sample_rate: 1.0,
79            capture_call_stack: true,
80            max_stack_depth: 20,
81        }
82    }
83
84    pub fn high_performance() -> Self {
85        Self {
86            sample_rate: 0.01,
87            capture_call_stack: false,
88            max_stack_depth: 0,
89        }
90    }
91}
92
93#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
94pub struct SystemSnapshot {
95    pub timestamp: u64,
96    pub cpu_usage_percent: f64,
97    pub memory_usage_bytes: u64,
98    pub memory_usage_percent: f64,
99    pub thread_count: usize,
100    pub disk_read_bps: u64,
101    pub disk_write_bps: u64,
102    pub network_rx_bps: u64,
103    pub network_tx_bps: u64,
104    pub gpu_usage_percent: f64,
105    pub gpu_memory_used: u64,
106    pub gpu_memory_total: u64,
107}
108
109#[derive(Debug, Clone)]
110pub struct AnalysisReport {
111    pub total_allocations: usize,
112    pub total_deallocations: usize,
113    pub active_allocations: usize,
114    pub peak_memory_bytes: u64,
115    pub current_memory_bytes: u64,
116    pub allocation_rate_per_sec: f64,
117    pub deallocation_rate_per_sec: f64,
118    pub hotspots: Vec<AllocationHotspot>,
119    pub system_snapshots: Vec<SystemSnapshot>,
120}
121
122#[derive(Debug, Clone)]
123pub struct AllocationHotspot {
124    pub var_name: String,
125    pub type_name: String,
126    pub total_size: usize,
127    pub allocation_count: usize,
128    pub location: Option<String>,
129}
130
131pub struct Tracker {
132    inner: Arc<MemoryTracker>,
133    event_store: Arc<EventStore>,
134    config: Arc<Mutex<TrackerConfig>>,
135    start_time: Instant,
136    system_snapshots: Arc<Mutex<Vec<SystemSnapshot>>>,
137}
138
139impl Clone for Tracker {
140    fn clone(&self) -> Self {
141        Tracker {
142            inner: self.inner.clone(),
143            event_store: self.event_store.clone(),
144            config: self.config.clone(),
145            start_time: Instant::now(), // Use current time for cloned tracker
146            system_snapshots: self.system_snapshots.clone(),
147        }
148    }
149}
150
151#[derive(Debug, Clone)]
152struct TrackerConfig {
153    sampling: SamplingConfig,
154    auto_export_on_drop: bool,
155    export_path: Option<String>,
156}
157
158impl Tracker {
159    pub fn new() -> Self {
160        Self {
161            inner: Arc::new(MemoryTracker::new()),
162            event_store: Arc::new(EventStore::new()),
163            config: Arc::new(Mutex::new(TrackerConfig {
164                sampling: SamplingConfig::default(),
165                auto_export_on_drop: false,
166                export_path: None,
167            })),
168            start_time: Instant::now(),
169            system_snapshots: Arc::new(Mutex::new(Vec::new())),
170        }
171    }
172
173    pub fn global() -> Self {
174        use crate::core::tracker::get_tracker;
175        static GLOBAL_EVENT_STORE: std::sync::OnceLock<Arc<EventStore>> =
176            std::sync::OnceLock::new();
177        static GLOBAL_CONFIG: std::sync::OnceLock<Arc<Mutex<TrackerConfig>>> =
178            std::sync::OnceLock::new();
179        static GLOBAL_SYSTEM_SNAPSHOTS: std::sync::OnceLock<Arc<Mutex<Vec<SystemSnapshot>>>> =
180            std::sync::OnceLock::new();
181
182        Self {
183            inner: get_tracker(),
184            event_store: GLOBAL_EVENT_STORE
185                .get_or_init(|| Arc::new(EventStore::new()))
186                .clone(),
187            config: GLOBAL_CONFIG
188                .get_or_init(|| {
189                    Arc::new(Mutex::new(TrackerConfig {
190                        sampling: SamplingConfig::default(),
191                        auto_export_on_drop: false,
192                        export_path: None,
193                    }))
194                })
195                .clone(),
196            start_time: Instant::now(),
197            system_snapshots: GLOBAL_SYSTEM_SNAPSHOTS
198                .get_or_init(|| Arc::new(Mutex::new(Vec::new())))
199                .clone(),
200        }
201    }
202
203    pub fn with_system_monitoring(self) -> Self {
204        self.capture_system_snapshot();
205        self
206    }
207
208    pub fn with_sampling(self, config: SamplingConfig) -> Self {
209        if let Ok(mut cfg) = self.config.lock() {
210            cfg.sampling = config;
211        }
212        self
213    }
214
215    pub fn with_auto_export(self, path: &str) -> Self {
216        if let Ok(mut cfg) = self.config.lock() {
217            cfg.auto_export_on_drop = true;
218            cfg.export_path = Some(path.to_string());
219        }
220        self
221    }
222
223    pub fn track_as<T: crate::Trackable>(&self, var: &T, name: &str, file: &str, line: u32) {
224        if let Ok(cfg) = self.config.lock() {
225            if cfg.sampling.sample_rate < 1.0 {
226                use std::collections::hash_map::DefaultHasher;
227                use std::hash::{Hash, Hasher};
228                let mut hasher = DefaultHasher::new();
229                // Use current timestamp for randomness to ensure sampling works
230                // correctly even with identical variable names in a loop
231                let timestamp = std::time::SystemTime::now()
232                    .duration_since(std::time::UNIX_EPOCH)
233                    .unwrap_or_default()
234                    .as_nanos();
235                timestamp.hash(&mut hasher);
236                std::thread::current().id().hash(&mut hasher);
237                name.hash(&mut hasher);
238                file.hash(&mut hasher);
239                line.hash(&mut hasher);
240                let hash = hasher.finish();
241                let threshold = (cfg.sampling.sample_rate * 1000.0) as u64;
242                if (hash % 1000) > threshold {
243                    return;
244                }
245            }
246        }
247
248        self.track_inner(var, name, file, line);
249    }
250
251    fn track_inner<T: crate::Trackable>(&self, var: &T, name: &str, file: &str, line: u32) {
252        let type_name = var.get_type_name().to_string();
253        let size = var.get_size_estimate();
254
255        let ptr = var.get_heap_ptr().unwrap_or_else(|| {
256            use std::cell::Cell;
257            thread_local! {
258                static COUNTER: Cell<u64> = const { Cell::new(0x8000_0000) };
259            }
260            COUNTER.with(|counter| {
261                let val = counter.get();
262                counter.set(val.wrapping_add(1));
263                val as usize
264            })
265        });
266
267        if let Err(e) = self.inner.track_allocation(ptr, size) {
268            tracing::error!("Failed to track allocation at ptr {:x}: {}", ptr, e);
269            return;
270        }
271
272        let thread_id_u64 = crate::utils::current_thread_id_u64();
273
274        let mut event = MemoryEvent::allocate(ptr, size, thread_id_u64);
275        event.var_name = Some(name.to_string());
276        event.type_name = Some(type_name.clone());
277        self.event_store.record(event);
278
279        if let Err(e) =
280            self.inner
281                .associate_var(ptr, name.to_string(), type_name, Some(file), Some(line))
282        {
283            tracing::error!("Failed to associate var '{}' at ptr {:x}: {}", name, ptr, e);
284        }
285    }
286
287    pub fn track_deallocation(&self, ptr: usize) -> crate::TrackingResult<bool> {
288        let size = self.inner.get_allocation_size(ptr).unwrap_or(0);
289
290        let result = self.inner.track_deallocation(ptr)?;
291
292        // Only record event if deallocation was successful (ptr was tracked)
293        if result {
294            let thread_id_u64 = crate::utils::current_thread_id_u64();
295
296            let event = MemoryEvent::deallocate(ptr, size, thread_id_u64);
297            self.event_store.record(event);
298        }
299
300        Ok(result)
301    }
302
303    pub fn events(&self) -> Vec<MemoryEvent> {
304        self.event_store.snapshot()
305    }
306
307    pub fn event_store(&self) -> &Arc<EventStore> {
308        &self.event_store
309    }
310
311    fn capture_system_snapshot(&self) {
312        let snapshot = SystemSnapshot {
313            timestamp: std::time::SystemTime::now()
314                .duration_since(std::time::UNIX_EPOCH)
315                .unwrap_or_default()
316                .as_millis() as u64,
317            cpu_usage_percent: system_monitor::cpu_usage(),
318            memory_usage_bytes: system_monitor::memory_used(),
319            memory_usage_percent: system_monitor::memory_usage_percent(),
320            thread_count: system_monitor::thread_count(),
321            disk_read_bps: system_monitor::disk_read_bps(),
322            disk_write_bps: system_monitor::disk_write_bps(),
323            network_rx_bps: system_monitor::network_rx_bps(),
324            network_tx_bps: system_monitor::network_tx_bps(),
325            gpu_usage_percent: system_monitor::gpu_memory_usage_percent(),
326            gpu_memory_used: system_monitor::gpu_memory_used(),
327            gpu_memory_total: system_monitor::gpu_memory_total(),
328        };
329
330        if let Ok(mut snapshots) = self.system_snapshots.lock() {
331            snapshots.push(snapshot);
332        }
333    }
334
335    pub fn stats(&self) -> crate::core::types::MemoryStats {
336        let stats = self.inner.get_stats().unwrap_or_default();
337        crate::core::types::MemoryStats {
338            total_allocations: stats.total_allocations as usize,
339            total_allocated: stats.total_allocated as usize,
340            active_allocations: stats.active_allocations,
341            active_memory: stats.active_memory as usize,
342            peak_allocations: stats.peak_allocations,
343            peak_memory: stats.peak_memory as usize,
344            total_deallocations: stats.total_deallocations as usize,
345            total_deallocated: stats.total_deallocated as usize,
346            leaked_allocations: stats.leaked_allocations,
347            leaked_memory: stats.leaked_memory as usize,
348            ..Default::default()
349        }
350    }
351
352    pub fn analyze(&self) -> AnalysisReport {
353        let stats = self.stats();
354        let allocations = self.inner.get_active_allocations().unwrap_or_default();
355        let elapsed = self.start_time.elapsed().as_secs_f64();
356
357        let current_memory: usize = allocations.iter().map(|a| a.size).sum();
358        let peak_memory = stats.peak_memory.max(current_memory);
359
360        let mut hotspot_map: HashMap<String, (String, usize, usize)> = HashMap::new();
361        for alloc in &allocations {
362            if let Some(ref var_name) = alloc.var_name {
363                let key = var_name.clone();
364                let entry = hotspot_map.entry(key).or_insert((
365                    alloc.type_name.clone().unwrap_or_default(),
366                    0,
367                    0,
368                ));
369                entry.1 += alloc.size;
370                entry.2 += 1;
371            }
372        }
373
374        let hotspots: Vec<AllocationHotspot> = hotspot_map
375            .into_iter()
376            .map(
377                |(var_name, (type_name, total_size, count))| AllocationHotspot {
378                    var_name,
379                    type_name,
380                    total_size,
381                    allocation_count: count,
382                    location: None,
383                },
384            )
385            .collect();
386
387        let system_snapshots = self
388            .system_snapshots
389            .lock()
390            .unwrap_or_else(|e| e.into_inner())
391            .clone();
392
393        AnalysisReport {
394            total_allocations: stats.total_allocations,
395            total_deallocations: stats.total_deallocations,
396            active_allocations: allocations.len(),
397            peak_memory_bytes: peak_memory as u64,
398            current_memory_bytes: current_memory as u64,
399            allocation_rate_per_sec: if elapsed > 0.0 {
400                stats.total_allocations as f64 / elapsed
401            } else {
402                0.0
403            },
404            deallocation_rate_per_sec: if elapsed > 0.0 {
405                stats.total_deallocations as f64 / elapsed
406            } else {
407                0.0
408            },
409            hotspots,
410            system_snapshots,
411        }
412    }
413
414    pub fn inner(&self) -> &Arc<MemoryTracker> {
415        &self.inner
416    }
417
418    pub fn elapsed(&self) -> Duration {
419        self.start_time.elapsed()
420    }
421
422    pub fn system_snapshots(&self) -> Vec<SystemSnapshot> {
423        self.system_snapshots
424            .lock()
425            .unwrap_or_else(|e| e.into_inner())
426            .clone()
427    }
428
429    pub fn current_system_snapshot(&self) -> SystemSnapshot {
430        SystemSnapshot {
431            timestamp: std::time::SystemTime::now()
432                .duration_since(std::time::UNIX_EPOCH)
433                .unwrap_or_default()
434                .as_millis() as u64,
435            cpu_usage_percent: system_monitor::cpu_usage(),
436            memory_usage_bytes: system_monitor::memory_used(),
437            memory_usage_percent: system_monitor::memory_usage_percent(),
438            thread_count: system_monitor::thread_count(),
439            disk_read_bps: system_monitor::disk_read_bps(),
440            disk_write_bps: system_monitor::disk_write_bps(),
441            network_rx_bps: system_monitor::network_rx_bps(),
442            network_tx_bps: system_monitor::network_tx_bps(),
443            gpu_usage_percent: system_monitor::gpu_memory_usage_percent(),
444            gpu_memory_used: system_monitor::gpu_memory_used(),
445            gpu_memory_total: system_monitor::gpu_memory_total(),
446        }
447    }
448}
449
450impl Default for Tracker {
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456impl Drop for Tracker {
457    fn drop(&mut self) {
458        if let Ok(cfg) = self.config.lock() {
459            if cfg.auto_export_on_drop {
460                if let Some(ref path) = cfg.export_path {
461                    let allocations = self.inner.get_active_allocations().unwrap_or_default();
462                    let snapshot = MemorySnapshot::from_allocation_infos(allocations);
463                    let options = ExportJsonOptions::default();
464                    if let Err(e) =
465                        export_snapshot_to_json(&snapshot, std::path::Path::new(path), &options)
466                    {
467                        tracing::error!("Failed to auto-export on drop: {}", e);
468                    }
469                }
470            }
471        }
472    }
473}
474
475impl serde::Serialize for AnalysisReport {
476    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
477    where
478        S: serde::Serializer,
479    {
480        use serde::ser::SerializeStruct;
481        let mut state = serializer.serialize_struct("AnalysisReport", 9)?;
482        state.serialize_field("total_allocations", &self.total_allocations)?;
483        state.serialize_field("total_deallocations", &self.total_deallocations)?;
484        state.serialize_field("active_allocations", &self.active_allocations)?;
485        state.serialize_field("peak_memory_bytes", &self.peak_memory_bytes)?;
486        state.serialize_field("current_memory_bytes", &self.current_memory_bytes)?;
487        state.serialize_field("allocation_rate_per_sec", &self.allocation_rate_per_sec)?;
488        state.serialize_field("deallocation_rate_per_sec", &self.deallocation_rate_per_sec)?;
489        state.serialize_field("hotspots", &self.hotspots)?;
490        state.serialize_field("system_snapshots", &self.system_snapshots)?;
491        state.end()
492    }
493}
494
495impl serde::Serialize for AllocationHotspot {
496    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
497    where
498        S: serde::Serializer,
499    {
500        use serde::ser::SerializeStruct;
501        let mut state = serializer.serialize_struct("AllocationHotspot", 5)?;
502        state.serialize_field("var_name", &self.var_name)?;
503        state.serialize_field("type_name", &self.type_name)?;
504        state.serialize_field("total_size", &self.total_size)?;
505        state.serialize_field("allocation_count", &self.allocation_count)?;
506        state.serialize_field("location", &self.location)?;
507        state.end()
508    }
509}
510
511#[macro_export]
512macro_rules! tracker {
513    () => {
514        $crate::tracker::Tracker::new()
515    };
516}
517
518#[macro_export]
519macro_rules! track {
520    ($tracker:expr, $var:expr) => {{
521        let var_name = stringify!($var);
522        $tracker.track_as(&$var, var_name, file!(), line!());
523    }};
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn test_tracker_creation() {
532        let tracker = Tracker::new();
533        let _ = tracker;
534    }
535
536    #[test]
537    fn test_tracker_with_config() {
538        let tracker = Tracker::new()
539            .with_sampling(SamplingConfig::demo())
540            .with_system_monitoring();
541        let _ = tracker;
542    }
543
544    #[test]
545    fn test_track_macro() {
546        let tracker = tracker!();
547        let my_vec = vec![1, 2, 3];
548        track!(tracker, my_vec);
549    }
550
551    #[test]
552    fn test_analyze() {
553        let tracker = tracker!();
554        let data = vec![1, 2, 3];
555        track!(tracker, data);
556        let report = tracker.analyze();
557        assert!(report.total_allocations > 0);
558    }
559
560    #[test]
561    #[cfg(target_os = "macos")]
562    fn test_system_monitoring() {
563        std::thread::sleep(std::time::Duration::from_millis(200));
564
565        let cpu = system_monitor::cpu_usage();
566        let mem = system_monitor::memory_used();
567        let total = system_monitor::memory_total();
568
569        println!("CPU: {:.2}%", cpu);
570        println!("Memory: {} / {} bytes", mem, total);
571
572        assert!((0.0..=100.0).contains(&cpu));
573        assert!(total > 0);
574    }
575
576    #[test]
577    fn test_current_system_snapshot() {
578        std::thread::sleep(std::time::Duration::from_millis(150));
579
580        let tracker = tracker!();
581        let snapshot = tracker.current_system_snapshot();
582
583        println!(
584            "Snapshot: CPU={:.2}%, Mem={:.2}%",
585            snapshot.cpu_usage_percent, snapshot.memory_usage_percent
586        );
587
588        assert!(snapshot.cpu_usage_percent >= 0.0 && snapshot.cpu_usage_percent <= 100.0);
589    }
590}