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        let _suppression = crate::core::allocator::TrackingGuard::acquire();
100        // Share the allocator's global tracker so that `start()` and the
101        // allocator hook see the same `inner`, `event_store`, and
102        // `recorded_bindings`. Without this sharing, the allocator hook
103        // writes release events to one EventStore while `start()` reads
104        // from another, making it appear as if nothing was ever freed.
105        //
106        // `shared_clone` creates a Tracker with its own independent `config`
107        // (sampling, auto-export) so that facade-level settings do not
108        // bleed into the allocator callback path.
109        let tracker = crate::tracker::allocator_events::global_tracker().shared_clone();
110        let passport_tracker = Arc::new(MemoryPassportTracker::new(config.passport));
111        let async_tracker = Arc::new(AsyncTracker::with_event_store(
112            tracker.event_store().clone(),
113        ));
114        async_tracker.set_initialized();
115
116        // Share this async_tracker instance with the async_tracker module
117        // so that spawn_tracked() can register task lifecycle events on it.
118        let _ = register_async_global(async_tracker.clone());
119
120        Self {
121            tracker,
122            passport_tracker,
123            async_tracker,
124            start_time: Instant::now(),
125        }
126    }
127
128    pub fn tracker(&self) -> &Tracker {
129        &self.tracker
130    }
131
132    pub fn passport_tracker(&self) -> &Arc<MemoryPassportTracker> {
133        &self.passport_tracker
134    }
135
136    pub fn async_tracker(&self) -> &Arc<AsyncTracker> {
137        &self.async_tracker
138    }
139
140    pub fn elapsed(&self) -> std::time::Duration {
141        self.start_time.elapsed()
142    }
143
144    pub fn track<T: crate::Trackable>(&self, var: &T) {
145        self.track_as(var, "unknown", "", 0, "");
146    }
147
148    pub fn track_as<T: crate::Trackable>(
149        &self,
150        var: &T,
151        name: &str,
152        file: &str,
153        line: u32,
154        module_path: &str,
155    ) {
156        self.tracker.track_as(var, name, file, line, module_path);
157
158        // Task-attributed memory lives in two independent surfaces that are each
159        // recorded exactly once:
160        //   * `TaskIdRegistry` (task relationship graph) — written by the single
161        //     capture point `Tracker::track_inner`;
162        //   * `AsyncTracker` (per-task async profiles and async KPIs) — written here.
163        // Neither store may be updated twice for the same observation, and neither
164        // is derived from the other, so this is a projection, not a second ledger.
165        //
166        // Suppress allocator tracking during the projection: the String and
167        // container allocations made while passing data to the AsyncTracker
168        // would otherwise be recorded as user allocations attributed to the
169        // current task, causing false leaks (e.g. ~1029 bytes of internal
170        // allocations per track! call).
171        let _guard = crate::core::allocator::TrackingGuard::acquire();
172        if let Some(task_id) = AsyncTracker::get_current_task() {
173            let kind = var.track_kind();
174            if let crate::core::types::TrackKind::HeapOwner { ptr, size } = kind {
175                let type_name = var.get_type_name().to_string();
176                self.async_tracker.track_allocation_with_location(
177                    ptr,
178                    size,
179                    task_id,
180                    Some(name.to_string()),
181                    Some(type_name),
182                    None,
183                );
184            }
185        }
186        drop(_guard);
187
188        // Note: `track!` is an *observation*, not a Rust borrow. Recording an
189        // immutable borrow here for every `Vec`/`String`/`Box` fabricated a borrow
190        // event per call (plan A19). Real borrow events require explicit
191        // instrumentation; borrow statistics stay empty until then rather than
192        // reporting invented numbers.
193    }
194
195    pub fn create_passport(
196        &self,
197        ptr: usize,
198        size: usize,
199        context: String,
200    ) -> Result<String, crate::capture::types::TrackingError> {
201        self.passport_tracker
202            .create_passport_simple(ptr, size, context)
203    }
204
205    pub fn record_handover(&self, ptr: usize, context: String, function: String) {
206        let _ = self
207            .passport_tracker
208            .record_handover_to_ffi(ptr, context, function);
209    }
210
211    pub fn record_free(&self, ptr: usize, context: String, function: String) {
212        let _ = self
213            .passport_tracker
214            .record_freed_by_foreign(ptr, context, function);
215    }
216
217    pub fn detect_leaks(&self) -> crate::analysis::memory_passport_tracker::LeakDetectionResult {
218        self.passport_tracker.detect_leaks_at_shutdown()
219    }
220
221    pub fn analyze(&self) -> AnalysisReport {
222        self.tracker.analyze()
223    }
224
225    pub fn get_stats(&self) -> GlobalTrackerStats {
226        // Suppress allocator tracking during the entire query: event_store
227        // snapshot, task graph export, and live tracker reads all allocate
228        // internally, and those allocations would be recorded as user
229        // allocations, polluting the statistics.
230        let _guard = crate::core::allocator::TrackingGuard::acquire();
231        let report = self.tracker.analyze();
232        let passport_stats = self.passport_tracker.get_stats();
233        let events = self.tracker.event_store().snapshot();
234        let task_graph = crate::task_registry::global_registry().export_graph();
235        let rebuilt = crate::capture::backends::rebuild_with_live_tracker(
236            &events,
237            &task_graph,
238            &self.async_tracker,
239        );
240
241        GlobalTrackerStats {
242            total_allocations: report.total_allocations,
243            active_allocations: report.active_allocations,
244            peak_memory_bytes: report.peak_memory_bytes,
245            current_memory_bytes: report.current_memory_bytes,
246            passport_count: passport_stats.total_passports_created,
247            active_passports: passport_stats.active_passports,
248            leaks_detected: passport_stats.leaks_detected,
249            async_task_count: rebuilt.profiles.len(),
250            active_async_tasks: rebuilt
251                .profiles
252                .iter()
253                .filter(|p| !p.is_completed())
254                .count(),
255            uptime: self.elapsed(),
256        }
257    }
258
259    /// Get all async task profiles, rebuilt from the event stream.
260    ///
261    /// This replaces `self.async_tracker.get_all_profiles()` for any caller
262    /// that needs accurate per-task memory data. The live AsyncTracker only
263    /// records allocations (not releases), so its profiles are stale after
264    /// any deallocation.
265    pub fn get_async_profiles_rebuilt(&self) -> Vec<crate::capture::backends::TaskMemoryProfile> {
266        // Suppress allocator tracking during the entire query to prevent
267        // the snapshot, graph export, and live tracker reads from polluting
268        // the statistics with their own allocations.
269        let _guard = crate::core::allocator::TrackingGuard::acquire();
270        let events = self.tracker.event_store().snapshot();
271        let task_graph = crate::task_registry::global_registry().export_graph();
272        let rebuilt = crate::capture::backends::rebuild_with_live_tracker(
273            &events,
274            &task_graph,
275            &self.async_tracker,
276        );
277        rebuilt.profiles
278    }
279
280    pub fn export_json<P: AsRef<Path>>(&self, path: P) -> MemScopeResult<()> {
281        use crate::render_engine::export::export_all_json;
282
283        let path = path.as_ref();
284        export_all_json(
285            path,
286            &self.tracker,
287            &self.passport_tracker,
288            &self.async_tracker,
289        )
290        .map_err(|e| MemScopeError::error("global_tracking", "export_json", e.to_string()))?;
291        Ok(())
292    }
293
294    pub fn export_html<P: AsRef<Path>>(&self, path: P) -> MemScopeResult<()> {
295        use crate::render_engine::export::export_dashboard_html_with_async;
296
297        let path = path.as_ref();
298        export_dashboard_html_with_async(
299            path,
300            &self.tracker,
301            &self.passport_tracker,
302            &self.async_tracker,
303        )
304        .map_err(|e| MemScopeError::error("global_tracking", "export_html", e.to_string()))?;
305        Ok(())
306    }
307
308    pub fn export_html_with_template<P: AsRef<Path>>(
309        &self,
310        path: P,
311        template: crate::render_engine::export::DashboardTemplate,
312    ) -> MemScopeResult<()> {
313        use crate::render_engine::export::export_dashboard_html_with_template;
314
315        let path = path.as_ref();
316        export_dashboard_html_with_template(
317            path,
318            &self.tracker,
319            &self.passport_tracker,
320            template,
321            Some(&self.async_tracker),
322        )
323        .map_err(|e| {
324            MemScopeError::error(
325                "global_tracking",
326                "export_html_with_template",
327                e.to_string(),
328            )
329        })?;
330        Ok(())
331    }
332}
333
334impl Default for GlobalTracker {
335    fn default() -> Self {
336        Self::new()
337    }
338}
339
340#[derive(Debug, Clone)]
341pub struct GlobalTrackerStats {
342    pub total_allocations: usize,
343    pub active_allocations: usize,
344    pub peak_memory_bytes: u64,
345    pub current_memory_bytes: u64,
346    pub passport_count: usize,
347    pub active_passports: usize,
348    pub leaks_detected: usize,
349    pub async_task_count: usize,
350    pub active_async_tasks: usize,
351    pub uptime: std::time::Duration,
352}
353
354pub fn init_global_tracking() -> MemScopeResult<()> {
355    // Capture the process start time on the very first init call so the
356    // dashboard can compute real CPU usage. This is idempotent — subsequent
357    // calls (which return Err) do not reset the timer.
358    PROCESS_START.get_or_init(Instant::now);
359
360    let mut guard = GLOBAL_TRACKER.write().map_err(|_| {
361        MemScopeError::error(
362            "global_tracking",
363            "init_global_tracking",
364            "Failed to acquire global tracker lock",
365        )
366    })?;
367
368    if guard.is_some() {
369        return Err(MemScopeError::error(
370            "global_tracking",
371            "init_global_tracking",
372            "Global tracking already initialized",
373        ));
374    }
375
376    *guard = Some(Arc::new(GlobalTracker::new()));
377    info!("Global tracking initialized");
378    Ok(())
379}
380
381pub fn init_global_tracking_with_config(config: GlobalTrackerConfig) -> MemScopeResult<()> {
382    // Capture the process start time (same as init_global_tracking).
383    PROCESS_START.get_or_init(Instant::now);
384
385    let mut guard = GLOBAL_TRACKER.write().map_err(|_| {
386        MemScopeError::error(
387            "global_tracking",
388            "init_global_tracking_with_config",
389            "Failed to acquire global tracker lock",
390        )
391    })?;
392
393    if guard.is_some() {
394        return Err(MemScopeError::error(
395            "global_tracking",
396            "init_global_tracking_with_config",
397            "Global tracking already initialized",
398        ));
399    }
400
401    *guard = Some(Arc::new(GlobalTracker::with_config(config)));
402    info!("Global tracking initialized with config");
403    Ok(())
404}
405
406pub fn reset_global_tracking() {
407    if let Ok(mut guard) = GLOBAL_TRACKER.write() {
408        *guard = None;
409    }
410}
411
412pub fn is_initialized() -> bool {
413    GLOBAL_TRACKER
414        .read()
415        .map(|guard| guard.is_some())
416        .unwrap_or(false)
417}
418
419pub fn global_tracker() -> MemScopeResult<Arc<GlobalTracker>> {
420    GLOBAL_TRACKER
421        .read()
422        .map(|guard| {
423            guard.as_ref().cloned().ok_or_else(|| {
424                MemScopeError::error(
425                    "global_tracking",
426                    "global_tracker",
427                    "Global tracking not initialized",
428                )
429            })
430        })
431        .map_err(|_| {
432            MemScopeError::error(
433                "global_tracking",
434                "global_tracker",
435                "Failed to acquire global tracker lock",
436            )
437        })?
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    // The global singleton is process wide: tests that reset or (re)initialise it
444    // must not run concurrently with each other or with `guard`'s serial tests.
445    use serial_test::serial;
446    use std::time::Duration;
447
448    fn reset_global_state() {
449        reset_global_tracking();
450    }
451
452    #[test]
453    fn test_unified_tracker() {
454        // The event_store is shared process-wide via allocator_events,
455        // and the async_tracker may have accumulated state from prior tests.
456        // We verify the tracker is functional rather than asserting zero.
457        let tracker = GlobalTracker::new();
458        let _ = tracker.tracker().analyze().total_allocations;
459        let _ = tracker.get_stats().total_allocations;
460    }
461
462    #[test]
463    fn test_tracker_config_default() {
464        let config = TrackerConfig::default();
465        assert_eq!(config.max_allocations, 1_000_000);
466        assert!(config.enable_statistics);
467    }
468
469    #[test]
470    fn test_tracker_config_clone() {
471        let config = TrackerConfig {
472            max_allocations: 500,
473            enable_statistics: false,
474        };
475        let cloned = config.clone();
476        assert_eq!(cloned.max_allocations, 500);
477        assert!(!cloned.enable_statistics);
478    }
479
480    #[test]
481    fn test_tracker_config_debug() {
482        let config = TrackerConfig::default();
483        let debug_str = format!("{:?}", config);
484        assert!(debug_str.contains("TrackerConfig"));
485        assert!(debug_str.contains("max_allocations"));
486        assert!(debug_str.contains("enable_statistics"));
487    }
488
489    #[test]
490    fn test_global_tracker_config_default() {
491        let config = GlobalTrackerConfig::default();
492        assert_eq!(config.tracker.max_allocations, 1_000_000);
493        assert!(config.tracker.enable_statistics);
494    }
495
496    #[test]
497    fn test_global_tracker_config_clone() {
498        let config = GlobalTrackerConfig {
499            tracker: TrackerConfig {
500                max_allocations: 100,
501                enable_statistics: false,
502            },
503            passport: PassportTrackerConfig::default(),
504        };
505        let cloned = config.clone();
506        assert_eq!(cloned.tracker.max_allocations, 100);
507        assert!(!cloned.tracker.enable_statistics);
508    }
509
510    #[test]
511    fn test_global_tracker_config_debug() {
512        let config = GlobalTrackerConfig::default();
513        let debug_str = format!("{:?}", config);
514        assert!(debug_str.contains("GlobalTrackerConfig"));
515    }
516
517    #[test]
518    fn test_global_tracker_new() {
519        let _guard = crate::core::allocator::TrackingGuard::acquire();
520        let source = crate::tracker::allocator_events::global_tracker().event_store();
521        let before = source.snapshot().len();
522        let tracker = GlobalTracker::new();
523        assert!(
524            Arc::ptr_eq(source, tracker.tracker().event_store()),
525            "facade must share allocator capture"
526        );
527        assert_eq!(
528            source.snapshot().len(),
529            before,
530            "constructing a facade cannot erase process history"
531        );
532    }
533
534    #[test]
535    fn test_global_tracker_with_config() {
536        let config = GlobalTrackerConfig {
537            tracker: TrackerConfig {
538                max_allocations: 100,
539                enable_statistics: true,
540            },
541            passport: PassportTrackerConfig::default(),
542        };
543        let tracker = GlobalTracker::with_config(config);
544        // Event store is shared process-wide; just verify the tracker works.
545        let _ = tracker.tracker().analyze().total_allocations;
546    }
547
548    #[test]
549    fn test_global_tracker_default() {
550        let tracker = GlobalTracker::default();
551        // Event store is shared process-wide; just verify the tracker works.
552        let _ = tracker.tracker().analyze().total_allocations;
553    }
554
555    #[test]
556    fn test_global_tracker_debug() {
557        let tracker = GlobalTracker::new();
558        let debug_str = format!("{:?}", tracker);
559        assert!(debug_str.contains("GlobalTracker"));
560        assert!(debug_str.contains("start_time"));
561    }
562
563    #[test]
564    fn test_global_tracker_elapsed() {
565        let tracker = GlobalTracker::new();
566        std::thread::sleep(Duration::from_millis(10));
567        let elapsed = tracker.elapsed();
568        assert!(elapsed >= Duration::from_millis(10));
569    }
570
571    #[test]
572    fn test_global_tracker_tracker_accessor() {
573        let tracker = GlobalTracker::new();
574        let inner_tracker = tracker.tracker();
575        // Event store is shared process-wide; just verify the accessor works.
576        let _ = inner_tracker.analyze().total_allocations;
577    }
578
579    #[test]
580    fn test_global_tracker_passport_tracker_accessor() {
581        let tracker = GlobalTracker::new();
582        let passport = tracker.passport_tracker();
583        let stats = passport.get_stats();
584        assert_eq!(stats.total_passports_created, 0);
585    }
586
587    #[test]
588    fn test_global_tracker_async_tracker_accessor() {
589        let tracker = GlobalTracker::new();
590        let async_tracker = tracker.async_tracker();
591        let stats = async_tracker.get_stats();
592        // `get_stats` is rebuilt from the process-wide event store, which parallel
593        // tests may already have populated. Assert a coherent snapshot through the
594        // accessor, not a fresh-process task count.
595        assert!(
596            stats.active_tasks <= stats.total_tasks,
597            "the accessor must report a coherent snapshot (active {}, total {})",
598            stats.active_tasks,
599            stats.total_tasks
600        );
601    }
602
603    #[test]
604    fn test_global_tracker_analyze() {
605        let _guard = crate::core::allocator::TrackingGuard::acquire();
606        let tracker = GlobalTracker::new();
607        let data = vec![3u8; 128];
608        tracker.track(&data);
609        let report = tracker.analyze();
610        let recreated = GlobalTracker::new().analyze();
611        assert!(
612            report.total_allocations > 0,
613            "explicit allocation must appear in analysis"
614        );
615        assert_eq!(
616            recreated.total_allocations, report.total_allocations,
617            "recreation preserves totals"
618        );
619        assert_eq!(
620            recreated.active_allocations, report.active_allocations,
621            "recreation preserves live allocations"
622        );
623    }
624
625    #[test]
626    fn test_global_tracker_stats() {
627        let _guard = crate::core::allocator::TrackingGuard::acquire();
628        let tracker = GlobalTracker::new();
629        let stats = tracker.get_stats();
630        let report = tracker.analyze();
631        assert_eq!(
632            stats.total_allocations, report.total_allocations,
633            "stats use shared capture"
634        );
635        assert_eq!(
636            stats.active_allocations, report.active_allocations,
637            "stats and analysis agree on live count"
638        );
639        assert!(
640            stats.peak_memory_bytes >= stats.current_memory_bytes,
641            "peak bounds live bytes"
642        );
643        assert_eq!(stats.passport_count, 0);
644        assert_eq!(stats.active_passports, 0);
645        assert_eq!(stats.leaks_detected, 0);
646        assert!(stats.uptime < Duration::from_secs(1));
647    }
648
649    #[test]
650    fn test_global_tracker_stats_debug() {
651        let tracker = GlobalTracker::new();
652        let stats = tracker.get_stats();
653        let debug_str = format!("{:?}", stats);
654        assert!(debug_str.contains("GlobalTrackerStats"));
655        assert!(debug_str.contains("total_allocations"));
656    }
657
658    #[test]
659    fn test_global_tracker_stats_clone() {
660        let tracker = GlobalTracker::new();
661        let stats = tracker.get_stats();
662        let cloned = stats.clone();
663        assert_eq!(cloned.total_allocations, stats.total_allocations);
664        assert_eq!(cloned.active_allocations, stats.active_allocations);
665    }
666
667    #[test]
668    #[serial]
669    fn test_init_global_tracking_double_init() {
670        reset_global_state();
671        let result1 = init_global_tracking();
672        assert!(result1.is_ok(), "First init should succeed");
673        let result2 = init_global_tracking();
674        assert!(result2.is_err(), "Second init should fail");
675        reset_global_state();
676    }
677
678    #[test]
679    #[serial]
680    fn test_init_global_tracking_with_config_double_init() {
681        reset_global_state();
682        let config = GlobalTrackerConfig::default();
683        let result1 = init_global_tracking_with_config(config);
684        assert!(result1.is_ok(), "First init should succeed");
685        let config2 = GlobalTrackerConfig::default();
686        let result2 = init_global_tracking_with_config(config2);
687        assert!(result2.is_err(), "Second init should fail");
688        reset_global_state();
689    }
690
691    #[test]
692    #[serial]
693    fn test_global_tracker_accessor_not_initialized() {
694        reset_global_state();
695        let result = global_tracker();
696        assert!(result.is_err(), "Should fail when not initialized");
697        reset_global_state();
698    }
699
700    #[test]
701    #[serial]
702    fn test_reset_global_tracking() {
703        reset_global_state();
704        init_global_tracking().unwrap();
705        assert!(is_initialized());
706        reset_global_tracking();
707        assert!(!is_initialized());
708    }
709
710    #[test]
711    fn test_global_tracker_create_passport() {
712        let tracker = GlobalTracker::new();
713        let result = tracker.create_passport(0x1000, 64, "test_context".to_string());
714        assert!(result.is_ok());
715        let passport_id = result.unwrap();
716        assert!(!passport_id.is_empty());
717    }
718
719    #[test]
720    fn test_global_tracker_detect_leaks() {
721        let tracker = GlobalTracker::new();
722        let result = tracker.detect_leaks();
723        assert_eq!(result.total_leaks, 0);
724    }
725
726    #[test]
727    fn test_global_tracker_record_handover() {
728        let tracker = GlobalTracker::new();
729        tracker
730            .create_passport(0x2000, 128, "test".to_string())
731            .unwrap();
732        tracker.record_handover(0x2000, "context".to_string(), "function".to_string());
733    }
734
735    #[test]
736    fn test_global_tracker_record_free() {
737        let tracker = GlobalTracker::new();
738        tracker
739            .create_passport(0x3000, 256, "test".to_string())
740            .unwrap();
741        tracker.record_free(0x3000, "context".to_string(), "function".to_string());
742    }
743
744    #[test]
745    fn test_global_tracker_track_simple() {
746        let tracker = GlobalTracker::new();
747        let value = String::from("test");
748        tracker.track(&value);
749    }
750
751    #[test]
752    fn test_global_tracker_track_as() {
753        let tracker = GlobalTracker::new();
754        let value = String::from("test_value");
755        tracker.track_as(&value, "test_var", "test.rs", 10, "test_module");
756    }
757
758    /// Objective: Verify `track_as` is an observation, not a Rust borrow (plan A19).
759    /// Invariants: the global borrow statistics do not change when a value is
760    /// tracked, while the allocation itself is still recorded.
761    #[test]
762    fn test_track_as_does_not_fabricate_borrow_event() {
763        let analyzer = crate::analysis::borrow_analysis::get_global_borrow_analyzer();
764        let before = analyzer.get_borrow_statistics().total_borrows;
765
766        let tracker = GlobalTracker::new();
767        let data = vec![1u8; 64];
768        tracker.track_as(&data, "data", file!(), line!(), module_path!());
769
770        let after = analyzer.get_borrow_statistics().total_borrows;
771        assert_eq!(
772            after, before,
773            "tracking a variable must not be recorded as a borrow event"
774        );
775        assert!(
776            tracker.get_stats().total_allocations >= 1,
777            "the allocation itself must still be tracked"
778        );
779    }
780}