Skip to main content

memscope_rs/capture/backends/
global_tracking.rs

1//! Global tracking module for passive memory capture.
2//!
3//! This module provides a unified entry point for automatic memory tracking
4//! across all execution modes (single-thread, multi-thread, async, unsafe/ffi).
5//!
6//! # Design Principles
7//!
8//! - **Merged**: All tracker data stored in a unified GlobalTracker
9//! - **Simplified**: Minimal public API surface
10//! - **Backward Compatible**: Legacy APIs preserved as deprecated wrappers
11//!
12//! # Core API (Recommended)
13//!
14//! ```ignore
15//! use memscope_rs::capture::backends::global_tracking::{init_global_tracking, global_tracker};
16//!
17//! init_global_tracking().unwrap();
18//! let tracker = global_tracker().unwrap();
19//!
20//! tracker.track!(my_variable);
21//! tracker.export_json("output").unwrap();
22//! tracker.export_html("output").unwrap();
23//! ```
24
25use crate::analysis::memory_passport_tracker::{MemoryPassportTracker, PassportTrackerConfig};
26use crate::capture::backends::async_tracker::{
27    register_global as register_async_global, AsyncTracker,
28};
29use crate::core::{MemScopeError, MemScopeResult};
30use crate::tracker::{AnalysisReport, Tracker};
31use std::path::Path;
32use std::sync::Arc;
33use std::time::Instant;
34use tracing::info;
35
36static GLOBAL_TRACKER: std::sync::RwLock<Option<Arc<GlobalTracker>>> = std::sync::RwLock::new(None);
37
38/// Process start-time timer — captured once during `init_global_tracking()`
39/// so that the dashboard can compute real CPU usage as
40/// (cpu_time / wall_time_since_start). Using a `OnceLock` here (in the
41/// `capture` crate) avoids a circular dependency with `render_engine`.
42static PROCESS_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
43
44/// Returns the elapsed wall-clock time since `init_global_tracking()` was
45/// first called, or `None` if tracking was never initialized.
46///
47/// Used by the dashboard renderer to compute CPU usage percentage.
48pub fn process_start_elapsed_secs() -> Option<f64> {
49    PROCESS_START.get().map(|t| t.elapsed().as_secs_f64())
50}
51
52#[derive(Debug, Clone)]
53pub struct TrackerConfig {
54    pub max_allocations: usize,
55    pub enable_statistics: bool,
56}
57
58impl Default for TrackerConfig {
59    fn default() -> Self {
60        Self {
61            max_allocations: 1_000_000,
62            enable_statistics: true,
63        }
64    }
65}
66
67#[derive(Debug, Clone, Default)]
68pub struct GlobalTrackerConfig {
69    pub tracker: TrackerConfig,
70    pub passport: PassportTrackerConfig,
71    // NOTE: Auto-export configuration lives exclusively on
72    // `MemScopeConfig.auto_export` and is consulted by `lifecycle::install`.
73    // Constructing a `GlobalTrackerConfig` directly and passing it to
74    // `init_global_tracking_with_config` does NOT enable auto-export; use
75    // `memscope_rs::start_with` for that.
76}
77
78pub struct GlobalTracker {
79    tracker: Tracker,
80    passport_tracker: Arc<MemoryPassportTracker>,
81    async_tracker: Arc<AsyncTracker>,
82    start_time: Instant,
83}
84
85impl std::fmt::Debug for GlobalTracker {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("GlobalTracker")
88            .field("start_time", &self.start_time)
89            .finish()
90    }
91}
92
93impl GlobalTracker {
94    pub fn new() -> Self {
95        Self::with_config(GlobalTrackerConfig::default())
96    }
97
98    pub fn with_config(config: GlobalTrackerConfig) -> Self {
99        // Construct the underlying Tracker from `config.tracker` rather than
100        // `Tracker::new()` so the user-facing `TrackerConfig` fields
101        // (`max_allocations`, `enable_statistics`) flow through to the Tracker
102        // layer instead of being silently discarded. `Tracker::with_config`
103        // currently consumes them informationally; future enforcement lives
104        // there, not in this file.
105        let tracker = Tracker::with_config(config.tracker);
106        let passport_tracker = Arc::new(MemoryPassportTracker::new(config.passport));
107        let async_tracker = Arc::new(AsyncTracker::new());
108        async_tracker.set_initialized();
109
110        // Share this async_tracker instance with the async_tracker module
111        // so that spawn_tracked() can register task lifecycle events on it.
112        let _ = register_async_global(async_tracker.clone());
113
114        Self {
115            tracker,
116            passport_tracker,
117            async_tracker,
118            start_time: Instant::now(),
119        }
120    }
121
122    pub fn tracker(&self) -> &Tracker {
123        &self.tracker
124    }
125
126    pub fn passport_tracker(&self) -> &Arc<MemoryPassportTracker> {
127        &self.passport_tracker
128    }
129
130    pub fn async_tracker(&self) -> &Arc<AsyncTracker> {
131        &self.async_tracker
132    }
133
134    pub fn elapsed(&self) -> std::time::Duration {
135        self.start_time.elapsed()
136    }
137
138    pub fn track<T: crate::Trackable>(&self, var: &T) {
139        self.track_as(var, "unknown", "", 0, "");
140    }
141
142    pub fn track_as<T: crate::Trackable>(
143        &self,
144        var: &T,
145        name: &str,
146        file: &str,
147        line: u32,
148        module_path: &str,
149    ) {
150        self.tracker.track_as(var, name, file, line, module_path);
151
152        if let Some(task_id) = AsyncTracker::get_current_task() {
153            let kind = var.track_kind();
154            if let crate::core::types::TrackKind::HeapOwner { ptr, size } = kind {
155                let type_name = var.get_type_name().to_string();
156                self.async_tracker.track_allocation_with_location(
157                    ptr,
158                    size,
159                    task_id,
160                    Some(name.to_string()),
161                    Some(type_name),
162                    None,
163                );
164                // Also update TaskIdRegistry memory stats for the Task Relationship Graph.
165                crate::task_registry::global_registry().record_allocation(size);
166            }
167        }
168
169        let kind = var.track_kind();
170        if let crate::core::types::TrackKind::HeapOwner { ptr, size: _ } = kind {
171            let borrow_analyzer = crate::analysis::borrow_analysis::get_global_borrow_analyzer();
172            let type_name = var.get_type_name();
173            let borrow_type = if type_name.contains("Mutex") || type_name.contains("Cell") {
174                crate::analysis::borrow_analysis::BorrowType::Mutable
175            } else {
176                crate::analysis::borrow_analysis::BorrowType::Immutable
177            };
178            let _ = borrow_analyzer.track_borrow(ptr, borrow_type, name);
179        }
180    }
181
182    pub fn create_passport(
183        &self,
184        ptr: usize,
185        size: usize,
186        context: String,
187    ) -> Result<String, crate::capture::types::TrackingError> {
188        self.passport_tracker
189            .create_passport_simple(ptr, size, context)
190    }
191
192    pub fn record_handover(&self, ptr: usize, context: String, function: String) {
193        let _ = self
194            .passport_tracker
195            .record_handover_to_ffi(ptr, context, function);
196    }
197
198    pub fn record_free(&self, ptr: usize, context: String, function: String) {
199        let _ = self
200            .passport_tracker
201            .record_freed_by_foreign(ptr, context, function);
202    }
203
204    pub fn detect_leaks(&self) -> crate::analysis::memory_passport_tracker::LeakDetectionResult {
205        self.passport_tracker.detect_leaks_at_shutdown()
206    }
207
208    pub fn analyze(&self) -> AnalysisReport {
209        self.tracker.analyze()
210    }
211
212    pub fn get_stats(&self) -> GlobalTrackerStats {
213        let report = self.tracker.analyze();
214        let passport_stats = self.passport_tracker.get_stats();
215        let async_stats = self.async_tracker.get_stats();
216
217        GlobalTrackerStats {
218            total_allocations: report.total_allocations,
219            active_allocations: report.active_allocations,
220            peak_memory_bytes: report.peak_memory_bytes,
221            current_memory_bytes: report.current_memory_bytes,
222            passport_count: passport_stats.total_passports_created,
223            active_passports: passport_stats.active_passports,
224            leaks_detected: passport_stats.leaks_detected,
225            async_task_count: async_stats.total_tasks,
226            active_async_tasks: async_stats.active_tasks,
227            uptime: self.elapsed(),
228        }
229    }
230
231    pub fn export_json<P: AsRef<Path>>(&self, path: P) -> MemScopeResult<()> {
232        use crate::render_engine::export::export_all_json;
233
234        let path = path.as_ref();
235        export_all_json(
236            path,
237            &self.tracker,
238            &self.passport_tracker,
239            &self.async_tracker,
240        )
241        .map_err(|e| MemScopeError::error("global_tracking", "export_json", e.to_string()))?;
242        Ok(())
243    }
244
245    pub fn export_html<P: AsRef<Path>>(&self, path: P) -> MemScopeResult<()> {
246        use crate::render_engine::export::export_dashboard_html_with_async;
247
248        let path = path.as_ref();
249        export_dashboard_html_with_async(
250            path,
251            &self.tracker,
252            &self.passport_tracker,
253            &self.async_tracker,
254        )
255        .map_err(|e| MemScopeError::error("global_tracking", "export_html", e.to_string()))?;
256        Ok(())
257    }
258
259    pub fn export_html_with_template<P: AsRef<Path>>(
260        &self,
261        path: P,
262        template: crate::render_engine::export::DashboardTemplate,
263    ) -> MemScopeResult<()> {
264        use crate::render_engine::export::export_dashboard_html_with_template;
265
266        let path = path.as_ref();
267        export_dashboard_html_with_template(
268            path,
269            &self.tracker,
270            &self.passport_tracker,
271            template,
272            Some(&self.async_tracker),
273        )
274        .map_err(|e| {
275            MemScopeError::error(
276                "global_tracking",
277                "export_html_with_template",
278                e.to_string(),
279            )
280        })?;
281        Ok(())
282    }
283}
284
285impl Default for GlobalTracker {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291#[derive(Debug, Clone)]
292pub struct GlobalTrackerStats {
293    pub total_allocations: usize,
294    pub active_allocations: usize,
295    pub peak_memory_bytes: u64,
296    pub current_memory_bytes: u64,
297    pub passport_count: usize,
298    pub active_passports: usize,
299    pub leaks_detected: usize,
300    pub async_task_count: usize,
301    pub active_async_tasks: usize,
302    pub uptime: std::time::Duration,
303}
304
305pub fn init_global_tracking() -> MemScopeResult<()> {
306    // Capture the process start time on the very first init call so the
307    // dashboard can compute real CPU usage. This is idempotent — subsequent
308    // calls (which return Err) do not reset the timer.
309    PROCESS_START.get_or_init(Instant::now);
310
311    let mut guard = GLOBAL_TRACKER.write().map_err(|_| {
312        MemScopeError::error(
313            "global_tracking",
314            "init_global_tracking",
315            "Failed to acquire global tracker lock",
316        )
317    })?;
318
319    if guard.is_some() {
320        return Err(MemScopeError::error(
321            "global_tracking",
322            "init_global_tracking",
323            "Global tracking already initialized",
324        ));
325    }
326
327    *guard = Some(Arc::new(GlobalTracker::new()));
328    info!("Global tracking initialized");
329    Ok(())
330}
331
332pub fn init_global_tracking_with_config(config: GlobalTrackerConfig) -> MemScopeResult<()> {
333    // Capture the process start time (same as init_global_tracking).
334    PROCESS_START.get_or_init(Instant::now);
335
336    let mut guard = GLOBAL_TRACKER.write().map_err(|_| {
337        MemScopeError::error(
338            "global_tracking",
339            "init_global_tracking_with_config",
340            "Failed to acquire global tracker lock",
341        )
342    })?;
343
344    if guard.is_some() {
345        return Err(MemScopeError::error(
346            "global_tracking",
347            "init_global_tracking_with_config",
348            "Global tracking already initialized",
349        ));
350    }
351
352    *guard = Some(Arc::new(GlobalTracker::with_config(config)));
353    info!("Global tracking initialized with config");
354    Ok(())
355}
356
357pub fn reset_global_tracking() {
358    if let Ok(mut guard) = GLOBAL_TRACKER.write() {
359        *guard = None;
360    }
361}
362
363pub fn is_initialized() -> bool {
364    GLOBAL_TRACKER
365        .read()
366        .map(|guard| guard.is_some())
367        .unwrap_or(false)
368}
369
370pub fn global_tracker() -> MemScopeResult<Arc<GlobalTracker>> {
371    GLOBAL_TRACKER
372        .read()
373        .map(|guard| {
374            guard.as_ref().cloned().ok_or_else(|| {
375                MemScopeError::error(
376                    "global_tracking",
377                    "global_tracker",
378                    "Global tracking not initialized",
379                )
380            })
381        })
382        .map_err(|_| {
383            MemScopeError::error(
384                "global_tracking",
385                "global_tracker",
386                "Failed to acquire global tracker lock",
387            )
388        })?
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use std::time::Duration;
395
396    fn reset_global_state() {
397        reset_global_tracking();
398    }
399
400    #[test]
401    fn test_unified_tracker() {
402        let tracker = GlobalTracker::new();
403        assert!(tracker.tracker().analyze().total_allocations == 0);
404
405        let stats = tracker.get_stats();
406        assert_eq!(stats.total_allocations, 0);
407    }
408
409    #[test]
410    fn test_tracker_config_default() {
411        let config = TrackerConfig::default();
412        assert_eq!(config.max_allocations, 1_000_000);
413        assert!(config.enable_statistics);
414    }
415
416    #[test]
417    fn test_tracker_config_clone() {
418        let config = TrackerConfig {
419            max_allocations: 500,
420            enable_statistics: false,
421        };
422        let cloned = config.clone();
423        assert_eq!(cloned.max_allocations, 500);
424        assert!(!cloned.enable_statistics);
425    }
426
427    #[test]
428    fn test_tracker_config_debug() {
429        let config = TrackerConfig::default();
430        let debug_str = format!("{:?}", config);
431        assert!(debug_str.contains("TrackerConfig"));
432        assert!(debug_str.contains("max_allocations"));
433        assert!(debug_str.contains("enable_statistics"));
434    }
435
436    #[test]
437    fn test_global_tracker_config_default() {
438        let config = GlobalTrackerConfig::default();
439        assert_eq!(config.tracker.max_allocations, 1_000_000);
440        assert!(config.tracker.enable_statistics);
441    }
442
443    #[test]
444    fn test_global_tracker_config_clone() {
445        let config = GlobalTrackerConfig {
446            tracker: TrackerConfig {
447                max_allocations: 100,
448                enable_statistics: false,
449            },
450            passport: PassportTrackerConfig::default(),
451        };
452        let cloned = config.clone();
453        assert_eq!(cloned.tracker.max_allocations, 100);
454        assert!(!cloned.tracker.enable_statistics);
455    }
456
457    #[test]
458    fn test_global_tracker_config_debug() {
459        let config = GlobalTrackerConfig::default();
460        let debug_str = format!("{:?}", config);
461        assert!(debug_str.contains("GlobalTrackerConfig"));
462    }
463
464    #[test]
465    fn test_global_tracker_new() {
466        let tracker = GlobalTracker::new();
467        assert!(tracker.elapsed() < Duration::from_secs(1));
468        assert!(tracker.tracker().analyze().total_allocations == 0);
469    }
470
471    #[test]
472    fn test_global_tracker_with_config() {
473        let config = GlobalTrackerConfig {
474            tracker: TrackerConfig {
475                max_allocations: 100,
476                enable_statistics: true,
477            },
478            passport: PassportTrackerConfig::default(),
479        };
480        let tracker = GlobalTracker::with_config(config);
481        assert!(tracker.tracker().analyze().total_allocations == 0);
482    }
483
484    #[test]
485    fn test_global_tracker_default() {
486        let tracker = GlobalTracker::default();
487        assert!(tracker.tracker().analyze().total_allocations == 0);
488    }
489
490    #[test]
491    fn test_global_tracker_debug() {
492        let tracker = GlobalTracker::new();
493        let debug_str = format!("{:?}", tracker);
494        assert!(debug_str.contains("GlobalTracker"));
495        assert!(debug_str.contains("start_time"));
496    }
497
498    #[test]
499    fn test_global_tracker_elapsed() {
500        let tracker = GlobalTracker::new();
501        std::thread::sleep(Duration::from_millis(10));
502        let elapsed = tracker.elapsed();
503        assert!(elapsed >= Duration::from_millis(10));
504    }
505
506    #[test]
507    fn test_global_tracker_tracker_accessor() {
508        let tracker = GlobalTracker::new();
509        let inner_tracker = tracker.tracker();
510        assert!(inner_tracker.analyze().total_allocations == 0);
511    }
512
513    #[test]
514    fn test_global_tracker_passport_tracker_accessor() {
515        let tracker = GlobalTracker::new();
516        let passport = tracker.passport_tracker();
517        let stats = passport.get_stats();
518        assert_eq!(stats.total_passports_created, 0);
519    }
520
521    #[test]
522    fn test_global_tracker_async_tracker_accessor() {
523        let tracker = GlobalTracker::new();
524        let async_tracker = tracker.async_tracker();
525        let stats = async_tracker.get_stats();
526        assert_eq!(stats.total_tasks, 0);
527    }
528
529    #[test]
530    fn test_global_tracker_analyze() {
531        let tracker = GlobalTracker::new();
532        let report = tracker.analyze();
533        assert_eq!(report.total_allocations, 0);
534        assert_eq!(report.active_allocations, 0);
535    }
536
537    #[test]
538    fn test_global_tracker_stats() {
539        let tracker = GlobalTracker::new();
540        let stats = tracker.get_stats();
541        assert_eq!(stats.total_allocations, 0);
542        assert_eq!(stats.active_allocations, 0);
543        assert_eq!(stats.peak_memory_bytes, 0);
544        assert_eq!(stats.current_memory_bytes, 0);
545        assert_eq!(stats.passport_count, 0);
546        assert_eq!(stats.active_passports, 0);
547        assert_eq!(stats.leaks_detected, 0);
548        assert_eq!(stats.async_task_count, 0);
549        assert_eq!(stats.active_async_tasks, 0);
550        assert!(stats.uptime < Duration::from_secs(1));
551    }
552
553    #[test]
554    fn test_global_tracker_stats_debug() {
555        let tracker = GlobalTracker::new();
556        let stats = tracker.get_stats();
557        let debug_str = format!("{:?}", stats);
558        assert!(debug_str.contains("GlobalTrackerStats"));
559        assert!(debug_str.contains("total_allocations"));
560    }
561
562    #[test]
563    fn test_global_tracker_stats_clone() {
564        let tracker = GlobalTracker::new();
565        let stats = tracker.get_stats();
566        let cloned = stats.clone();
567        assert_eq!(cloned.total_allocations, stats.total_allocations);
568        assert_eq!(cloned.active_allocations, stats.active_allocations);
569    }
570
571    #[test]
572    fn test_init_global_tracking_double_init() {
573        reset_global_state();
574        let result1 = init_global_tracking();
575        assert!(result1.is_ok(), "First init should succeed");
576        let result2 = init_global_tracking();
577        assert!(result2.is_err(), "Second init should fail");
578        reset_global_state();
579    }
580
581    #[test]
582    fn test_init_global_tracking_with_config_double_init() {
583        reset_global_state();
584        let config = GlobalTrackerConfig::default();
585        let result1 = init_global_tracking_with_config(config);
586        assert!(result1.is_ok(), "First init should succeed");
587        let config2 = GlobalTrackerConfig::default();
588        let result2 = init_global_tracking_with_config(config2);
589        assert!(result2.is_err(), "Second init should fail");
590        reset_global_state();
591    }
592
593    #[test]
594    fn test_global_tracker_accessor_not_initialized() {
595        reset_global_state();
596        let result = global_tracker();
597        assert!(result.is_err(), "Should fail when not initialized");
598        reset_global_state();
599    }
600
601    #[test]
602    fn test_reset_global_tracking() {
603        reset_global_state();
604        init_global_tracking().unwrap();
605        assert!(is_initialized());
606        reset_global_tracking();
607        assert!(!is_initialized());
608    }
609
610    #[test]
611    fn test_global_tracker_create_passport() {
612        let tracker = GlobalTracker::new();
613        let result = tracker.create_passport(0x1000, 64, "test_context".to_string());
614        assert!(result.is_ok());
615        let passport_id = result.unwrap();
616        assert!(!passport_id.is_empty());
617    }
618
619    #[test]
620    fn test_global_tracker_detect_leaks() {
621        let tracker = GlobalTracker::new();
622        let result = tracker.detect_leaks();
623        assert_eq!(result.total_leaks, 0);
624    }
625
626    #[test]
627    fn test_global_tracker_record_handover() {
628        let tracker = GlobalTracker::new();
629        tracker
630            .create_passport(0x2000, 128, "test".to_string())
631            .unwrap();
632        tracker.record_handover(0x2000, "context".to_string(), "function".to_string());
633    }
634
635    #[test]
636    fn test_global_tracker_record_free() {
637        let tracker = GlobalTracker::new();
638        tracker
639            .create_passport(0x3000, 256, "test".to_string())
640            .unwrap();
641        tracker.record_free(0x3000, "context".to_string(), "function".to_string());
642    }
643
644    #[test]
645    fn test_global_tracker_track_simple() {
646        let tracker = GlobalTracker::new();
647        let value = String::from("test");
648        tracker.track(&value);
649    }
650
651    #[test]
652    fn test_global_tracker_track_as() {
653        let tracker = GlobalTracker::new();
654        let value = String::from("test_value");
655        tracker.track_as(&value, "test_var", "test.rs", 10, "test_module");
656    }
657}