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::dashboard::renderer::rebuild_allocations_from_events;
44use crate::render_engine::export::{export_snapshot_with_containers, ExportJsonOptions};
45
46use dashmap::DashMap;
47use std::collections::HashMap;
48use std::sync::{Arc, Mutex};
49use std::time::{Duration, Instant};
50
51#[derive(Debug, Clone)]
52pub struct SamplingConfig {
53    pub sample_rate: f64,
54    pub capture_call_stack: bool,
55    pub max_stack_depth: usize,
56}
57
58impl Default for SamplingConfig {
59    fn default() -> Self {
60        Self {
61            sample_rate: 1.0,
62            capture_call_stack: false,
63            max_stack_depth: 10,
64        }
65    }
66}
67
68impl SamplingConfig {
69    pub fn demo() -> Self {
70        Self {
71            sample_rate: 0.1,
72            capture_call_stack: false,
73            max_stack_depth: 5,
74        }
75    }
76
77    pub fn full() -> Self {
78        Self {
79            sample_rate: 1.0,
80            capture_call_stack: true,
81            max_stack_depth: 20,
82        }
83    }
84
85    pub fn high_performance() -> Self {
86        Self {
87            sample_rate: 0.01,
88            capture_call_stack: false,
89            max_stack_depth: 0,
90        }
91    }
92}
93
94#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
95pub struct SystemSnapshot {
96    pub timestamp: u64,
97    pub cpu_usage_percent: f64,
98    pub memory_usage_bytes: u64,
99    pub memory_usage_percent: f64,
100    pub thread_count: usize,
101    pub disk_read_bps: u64,
102    pub disk_write_bps: u64,
103    pub network_rx_bps: u64,
104    pub network_tx_bps: u64,
105    pub gpu_usage_percent: f64,
106    pub gpu_memory_used: u64,
107    pub gpu_memory_total: u64,
108}
109
110#[derive(Debug, Clone)]
111pub struct AnalysisReport {
112    pub total_allocations: usize,
113    pub total_deallocations: usize,
114    pub active_allocations: usize,
115    pub peak_memory_bytes: u64,
116    pub current_memory_bytes: u64,
117    pub allocation_rate_per_sec: f64,
118    pub deallocation_rate_per_sec: f64,
119    pub hotspots: Vec<AllocationHotspot>,
120    pub system_snapshots: Vec<SystemSnapshot>,
121}
122
123#[derive(Debug, Clone)]
124pub struct AllocationHotspot {
125    pub var_name: String,
126    pub type_name: String,
127    pub total_size: usize,
128    pub allocation_count: usize,
129    pub location: Option<String>,
130}
131
132/// State of the bindings this tracker has reported for one address.
133///
134/// Keyed by binding serial, *not* by address: an address can hold several generations and
135/// a release must remove only the identity it closed. Keeping one entry per address made
136/// a degraded release drop the surviving binding's state, so the next observation of that
137/// block published a second allocation.
138type RecordedState = std::collections::HashMap<u64, RecordedBinding>;
139/// Serialised slot holding the state of one address.
140type RecordedSlot = Arc<Mutex<RecordedState>>;
141/// Address -> recorded slot. The map is shared by all clones of a tracker.
142type RecordedBindings = Arc<DashMap<usize, RecordedSlot>>;
143
144/// What this tracker has already reported for an address.
145///
146/// The core cache can be populated by the allocator hook without any event, and a
147/// released address can be reused by a *new* block. Both cases must be told apart from
148/// "this tracker already emitted the event", which is what `serial` (binding identity)
149/// and `typed` (metadata present) express.
150#[derive(Debug, Clone, Copy)]
151struct RecordedBinding {
152    /// Size recorded with the event.
153    size: usize,
154    /// Whether the recorded event carried type information.
155    typed: bool,
156    /// Whether the current task was already charged for this binding. A cache entry only
157    /// means "the core tracker knows the block", never "the allocator charged it".
158    charged: bool,
159}
160
161pub struct Tracker {
162    inner: Arc<MemoryTracker>,
163    event_store: Arc<EventStore>,
164    /// Addresses for which *this* tracker already emitted an allocation event, with the
165    /// size recorded at that moment.
166    ///
167    /// The core cache can be populated by the allocator hook without any event
168    /// (`Tracker::global` shares the hook's tracker), so cache presence alone must not
169    /// be treated as "already reported".
170    /// One slot per address; the slot also serialises the "first record" decision so
171    /// concurrent observers of the same block cannot each emit an event.
172    recorded_bindings: RecordedBindings,
173    config: Arc<Mutex<TrackerConfig>>,
174    start_time: Instant,
175    system_snapshots: Arc<Mutex<Vec<SystemSnapshot>>>,
176}
177
178pub(crate) mod allocator_events;
179mod release;
180
181impl Clone for Tracker {
182    fn clone(&self) -> Self {
183        Tracker {
184            inner: self.inner.clone(),
185            event_store: self.event_store.clone(),
186            recorded_bindings: self.recorded_bindings.clone(),
187            config: self.config.clone(),
188            start_time: Instant::now(), // Use current time for cloned tracker
189            system_snapshots: self.system_snapshots.clone(),
190        }
191    }
192}
193
194#[derive(Debug, Clone)]
195pub struct TrackerConfig {
196    sampling: SamplingConfig,
197    auto_export_on_drop: bool,
198    export_path: Option<String>,
199}
200
201impl Tracker {
202    pub fn new() -> Self {
203        let _suppression = crate::core::allocator::TrackingGuard::acquire();
204        Self {
205            inner: Arc::new(MemoryTracker::new()),
206            event_store: Arc::new(EventStore::new()),
207            recorded_bindings: Arc::new(DashMap::new()),
208            config: Arc::new(Mutex::new(TrackerConfig {
209                sampling: SamplingConfig::default(),
210                auto_export_on_drop: false,
211                export_path: None,
212            })),
213            start_time: Instant::now(),
214            system_snapshots: Arc::new(Mutex::new(Vec::new())),
215        }
216    }
217
218    /// Construct a `Tracker` from a [`global_tracking::TrackerConfig`].
219    ///
220    /// This is the bridge between the user-facing `GlobalTrackerConfig.tracker`
221    /// field and the internal `Tracker` construction. Currently the underlying
222    /// `MemoryTracker` does not honor `max_allocations` (it is unbounded by
223    /// design), so this method consumes the config without applying that knob.
224    /// `enable_statistics` is implicitly always true — the tracker always
225    /// collects statistics; future implementations may gate this.
226    ///
227    /// The method exists so that `GlobalTracker::with_config` does not silently
228    /// discard its `config.tracker` field.
229    ///
230    /// [`global_tracking::TrackerConfig`]: crate::capture::backends::global_tracking::TrackerConfig
231    pub fn with_config(config: crate::capture::backends::global_tracking::TrackerConfig) -> Self {
232        tracing::debug!(
233            target: "memscope::tracker",
234            max_allocations = config.max_allocations,
235            enable_statistics = config.enable_statistics,
236            "constructing Tracker from GlobalTrackerConfig.tracker \
237             (currently informational; fields are reserved for future enforcement)",
238        );
239        Self::new()
240    }
241
242    /// Create a facade `Tracker` that shares `inner`, `event_store`, and
243    /// `recorded_bindings` with `self` but has its own independent `config`
244    /// and `start_time`.
245    ///
246    /// This is used by `GlobalTracker::with_config` to connect the facade
247    /// to the allocator's shared capture state, so that real alloc/free
248    /// events from the allocator hook are visible to the facade's consumers.
249    /// The facade gets its own config so that user-facing settings (sampling,
250    /// auto-export) do not modify the allocator callback's behavior.
251    pub(crate) fn shared_clone(&self) -> Self {
252        Self {
253            inner: Arc::clone(&self.inner),
254            event_store: Arc::clone(&self.event_store),
255            recorded_bindings: Arc::clone(&self.recorded_bindings),
256            // New, independent config so facade changes don't leak into the
257            // allocator callback path.
258            config: Arc::new(Mutex::new(TrackerConfig {
259                sampling: SamplingConfig::default(),
260                auto_export_on_drop: false,
261                export_path: None,
262            })),
263            start_time: Instant::now(),
264            system_snapshots: Arc::clone(&self.system_snapshots),
265        }
266    }
267
268    pub fn global() -> Self {
269        allocator_events::global_tracker().clone()
270    }
271
272    pub fn with_system_monitoring(self) -> Self {
273        self.capture_system_snapshot();
274        self
275    }
276
277    pub fn with_sampling(self, config: SamplingConfig) -> Self {
278        if let Ok(mut cfg) = self.config.lock() {
279            cfg.sampling = config;
280        }
281        self
282    }
283
284    pub fn with_auto_export(self, path: &str) -> Self {
285        if let Ok(mut cfg) = self.config.lock() {
286            cfg.auto_export_on_drop = true;
287            cfg.export_path = Some(path.to_string());
288        }
289        self
290    }
291
292    pub fn track_as<T: crate::Trackable>(
293        &self,
294        var: &T,
295        name: &str,
296        file: &str,
297        line: u32,
298        module_path: &str,
299    ) {
300        let _suppression = crate::core::allocator::TrackingGuard::acquire();
301        if let Ok(cfg) = self.config.lock() {
302            if cfg.sampling.sample_rate < 1.0 {
303                use std::collections::hash_map::DefaultHasher;
304                use std::hash::{Hash, Hasher};
305                let mut hasher = DefaultHasher::new();
306                // Use current timestamp for randomness to ensure sampling works
307                // correctly even with identical variable names in a loop
308                let timestamp = std::time::SystemTime::now()
309                    .duration_since(std::time::UNIX_EPOCH)
310                    .unwrap_or_default()
311                    .as_nanos();
312                timestamp.hash(&mut hasher);
313                std::thread::current().id().hash(&mut hasher);
314                name.hash(&mut hasher);
315                file.hash(&mut hasher);
316                line.hash(&mut hasher);
317                let hash = hasher.finish();
318                let threshold = (cfg.sampling.sample_rate * 1000.0) as u64;
319                if (hash % 1000) > threshold {
320                    return;
321                }
322            }
323        }
324
325        self.track_inner(var, name, file, line, module_path);
326    }
327
328    #[allow(clippy::too_many_arguments)]
329    /// Track a clone operation
330    pub fn track_clone(
331        &self,
332        source_ptr: usize,
333        target_ptr: usize,
334        size: usize,
335        var_name: Option<String>,
336        type_name: Option<String>,
337        file: &str,
338        line: u32,
339        module_path: &str,
340    ) {
341        let _gate = crate::core::allocator::capture_gate::acquire(0);
342        let thread_id_u64 = crate::utils::current_thread_id_u64();
343
344        let mut event = crate::event_store::MemoryEvent::clone_event(
345            source_ptr,
346            target_ptr,
347            size,
348            thread_id_u64,
349            var_name,
350            type_name,
351        );
352        event.source_file = Some(file.to_string());
353        event.source_line = Some(line);
354        event.module_path = Some(module_path.to_string());
355
356        self.event_store.record(event);
357    }
358
359    fn track_inner<T: crate::Trackable>(
360        &self,
361        var: &T,
362        name: &str,
363        file: &str,
364        line: u32,
365        module_path: &str,
366    ) {
367        let type_name = var.get_type_name().to_string();
368        let kind = var.track_kind();
369        let metadata_size = match kind {
370            crate::core::types::TrackKind::Container | crate::core::types::TrackKind::Value => {
371                var.get_size_estimate()
372            }
373            _ => 0,
374        };
375        // User-defined Trackable methods may reenter, panic, or terminate the
376        // process. Invoke them before taking the process-wide capture gate so
377        // an exit-export worker can still read the already committed history.
378        let _gate = crate::core::allocator::capture_gate::acquire(0);
379
380        let thread_id_u64 = crate::utils::current_thread_id_u64();
381
382        match kind {
383            crate::core::types::TrackKind::HeapOwner { ptr, size } => {
384                // The core cache can already hold this address because the allocator hook
385                // registered the block *without* emitting an event, or because a previous
386                // block at this address was released and the address was reused. Only the
387                // binding identity (serial) plus the presence of type metadata tell those
388                // cases apart from a real duplicate observation.
389                // Serialise the whole "read index -> update cache -> emit event -> write
390                // index" sequence per address. Without this, N threads observing the same
391                // block at once each saw "not recorded yet" and produced N allocation
392                // events for one block (plan A06 follow-up).
393                let slot = self.recorded_slot(ptr);
394                let mut slot_guard = match slot.lock() {
395                    Ok(guard) => guard,
396                    Err(poisoned) => poisoned.into_inner(),
397                };
398                if let Err(e) = self.inner.track_allocation(ptr, size) {
399                    tracing::error!("Failed to track allocation at ptr {:x}: {}", ptr, e);
400                    return;
401                }
402
403                let serial = self.inner.binding_serial(ptr).unwrap_or(0);
404                let existing = slot_guard.get(&serial).copied();
405                let same_binding = existing.is_some_and(|binding| binding.size == size);
406
407                if same_binding && existing.is_some_and(|binding| binding.typed) {
408                    // Same binding, same size, type already reported: refresh the cache row
409                    // only, never a second event.
410                    if let Err(e) = self.inner.associate_var(
411                        ptr,
412                        name.to_string(),
413                        type_name,
414                        Some(file),
415                        Some(line),
416                    ) {
417                        tracing::error!(
418                            "Failed to associate var '{}' at ptr {:x}: {}",
419                            name,
420                            ptr,
421                            e
422                        );
423                    }
424                    return;
425                }
426
427                if same_binding {
428                    // The allocation is already reported (for example by an allocator
429                    // reallocation event) but without type information: attach the type as an
430                    // annotation so projections can name the block without counting a second
431                    // allocation. Task memory is charged only if nobody did it yet.
432                    let already_charged = existing.is_some_and(|binding| binding.charged);
433                    if !already_charged {
434                        crate::task_registry::global_registry().record_allocation(size);
435                    }
436                    let mut annotation = MemoryEvent::metadata(
437                        name.to_string(),
438                        type_name.clone(),
439                        thread_id_u64,
440                        size,
441                    );
442                    annotation.ptr = ptr;
443                    annotation.binding_serial = Some(serial);
444                    annotation.source_file = Some(file.to_string());
445                    annotation.source_line = Some(line);
446                    annotation.module_path = Some(module_path.to_string());
447                    self.event_store.record(annotation);
448                    slot_guard.insert(
449                        serial,
450                        RecordedBinding {
451                            size,
452                            typed: true,
453                            charged: true,
454                        },
455                    );
456
457                    if let Err(e) = self.inner.associate_var(
458                        ptr,
459                        name.to_string(),
460                        type_name,
461                        Some(file),
462                        Some(line),
463                    ) {
464                        tracing::error!(
465                            "Failed to associate var '{}' at ptr {:x}: {}",
466                            name,
467                            ptr,
468                            e
469                        );
470                    }
471                    return;
472                }
473
474                // Either the first typed observation of a hook-registered block, or a new
475                // binding (fresh address, or a new serial after the address was reused).
476                // Emit one allocation event and book task memory exactly once: the
477                // allocator marks the addresses it charged, so a locally created tracker
478                // observing the same block does not charge it twice. Asking the core cache
479                // whether the address exists cannot answer this — this very call has just
480                // inserted it.
481                let charge_outcome = allocator_events::allocator_charge(ptr).unwrap_or_else(|| {
482                    crate::task_registry::global_registry().try_record_allocation(size)
483                });
484
485                let mut event = MemoryEvent::allocate(ptr, size, thread_id_u64);
486                event.binding_serial = Some(serial);
487                event.charge_outcome = Some(charge_outcome);
488                event.var_name = Some(name.to_string());
489                event.type_name = Some(type_name.clone());
490                event.source_file = Some(file.to_string());
491                event.source_line = Some(line);
492                event.module_path = Some(module_path.to_string());
493                self.event_store.record(event);
494                slot_guard.insert(
495                    serial,
496                    RecordedBinding {
497                        size,
498                        typed: true,
499                        charged: true,
500                    },
501                );
502
503                if let Err(e) = self.inner.associate_var(
504                    ptr,
505                    name.to_string(),
506                    type_name,
507                    Some(file),
508                    Some(line),
509                ) {
510                    tracing::error!("Failed to associate var '{}' at ptr {:x}: {}", name, ptr, e);
511                }
512            }
513
514            crate::core::types::TrackKind::StackOwner {
515                ptr: stack_ptr,
516                heap_ptr,
517                size,
518            } => {
519                // A smart pointer owns a heap block that the allocator already reported,
520                // and this observation knows the *pointee* size, not the block size:
521                // `Arc::new([u8; 64])` allocates 80 bytes, not 64. Publishing an
522                // allocation here counted the same memory twice (80 + 64). Record the
523                // metadata against the pointee instead and keep the stack pointer so
524                // clone detection can still relate the two views.
525                // The pointee sits *inside* its allocation (an `Arc` stores the value
526                // after its reference counts), so neither the address nor the pointee
527                // size identifies the block. Look up the containing binding and annotate
528                // that identity; without one the annotation stays a diagnostic.
529                let containing = self.inner.binding_containing(heap_ptr);
530                let mut annotation = MemoryEvent::metadata(
531                    name.to_string(),
532                    type_name.clone(),
533                    thread_id_u64,
534                    containing.map_or(size, |binding| binding.size),
535                );
536                annotation.ptr = containing.map_or(heap_ptr, |binding| binding.ptr);
537                annotation.binding_serial = containing.map(|binding| binding.serial);
538                annotation.stack_ptr = Some(stack_ptr);
539                annotation.source_file = Some(file.to_string());
540                annotation.source_line = Some(line);
541                annotation.module_path = Some(module_path.to_string());
542                self.event_store.record(annotation);
543
544                if let Err(e) = self.inner.associate_var(
545                    heap_ptr,
546                    name.to_string(),
547                    type_name,
548                    Some(file),
549                    Some(line),
550                ) {
551                    tracing::error!(
552                        "Failed to associate var '{}' at ptr {:x}: {}",
553                        name,
554                        heap_ptr,
555                        e
556                    );
557                }
558            }
559            crate::core::types::TrackKind::Container | crate::core::types::TrackKind::Value => {
560                // Container and Value record metadata events without heap allocation
561                // They will be tracked as graph nodes but not scanned by HeapScanner
562                let mut event = MemoryEvent::metadata(
563                    name.to_string(),
564                    type_name,
565                    thread_id_u64,
566                    metadata_size,
567                );
568                event.source_file = Some(file.to_string());
569                event.source_line = Some(line);
570                event.module_path = Some(module_path.to_string());
571                self.event_store.record(event);
572            }
573        }
574    }
575
576    /// Number of duplicate observations of an already tracked address.
577    ///
578    /// These sightings are not counted as allocations or bytes; the value exists so
579    /// reports can show that the capture layer saw the same block twice.
580    pub fn duplicate_observations(&self) -> u64 {
581        self.inner.duplicate_observations()
582    }
583
584    /// Releases that could only be matched by address.
585    ///
586    /// Non-zero means a release arrived for an address that still held several live
587    /// bindings, so the closed binding was the oldest one rather than a named identity.
588    pub fn degraded_releases(&self) -> u64 {
589        self.inner.degraded_releases()
590    }
591
592    pub fn events(&self) -> Vec<MemoryEvent> {
593        self.event_store.snapshot()
594    }
595
596    pub fn event_store(&self) -> &Arc<EventStore> {
597        &self.event_store
598    }
599
600    fn capture_system_snapshot(&self) {
601        let snapshot = SystemSnapshot {
602            timestamp: std::time::SystemTime::now()
603                .duration_since(std::time::UNIX_EPOCH)
604                .unwrap_or_default()
605                .as_millis() as u64,
606            cpu_usage_percent: system_monitor::cpu_usage(),
607            memory_usage_bytes: system_monitor::memory_used(),
608            memory_usage_percent: system_monitor::memory_usage_percent(),
609            thread_count: system_monitor::thread_count(),
610            disk_read_bps: system_monitor::disk_read_bps(),
611            disk_write_bps: system_monitor::disk_write_bps(),
612            network_rx_bps: system_monitor::network_rx_bps(),
613            network_tx_bps: system_monitor::network_tx_bps(),
614            gpu_usage_percent: system_monitor::gpu_memory_usage_percent(),
615            gpu_memory_used: system_monitor::gpu_memory_used(),
616            gpu_memory_total: system_monitor::gpu_memory_total(),
617        };
618
619        if let Ok(mut snapshots) = self.system_snapshots.lock() {
620            snapshots.push(snapshot);
621        }
622    }
623
624    pub fn stats(&self) -> crate::core::types::MemoryStats {
625        let stats = self.inner.get_stats().unwrap_or_default();
626        crate::core::types::MemoryStats {
627            total_allocations: stats.total_allocations as usize,
628            total_allocated: stats.total_allocated as usize,
629            active_allocations: stats.active_allocations,
630            active_memory: stats.active_memory as usize,
631            peak_allocations: stats.peak_allocations,
632            peak_memory: stats.peak_memory as usize,
633            total_deallocations: stats.total_deallocations as usize,
634            total_deallocated: stats.total_deallocated as usize,
635            leaked_allocations: stats.leaked_allocations,
636            leaked_memory: stats.leaked_memory as usize,
637            ..Default::default()
638        }
639    }
640
641    pub fn analyze(&self) -> AnalysisReport {
642        let _suppression = crate::core::allocator::TrackingGuard::acquire();
643        let stats = self.stats();
644        let events = self.event_store().snapshot();
645        let allocations = rebuild_allocations_from_events(&events);
646        let elapsed = self.start_time.elapsed().as_secs_f64();
647
648        let current_memory: usize = allocations.iter().map(|a| a.size).sum();
649        let peak_memory = stats.peak_memory.max(current_memory);
650
651        let mut hotspot_map: HashMap<String, (String, usize, usize)> = HashMap::new();
652        for alloc in &allocations {
653            if let Some(ref var_name) = alloc.var_name {
654                let key = var_name.clone();
655                let entry = hotspot_map.entry(key).or_insert((
656                    alloc.type_name.clone().unwrap_or_default(),
657                    0,
658                    0,
659                ));
660                entry.1 += alloc.size;
661                entry.2 += 1;
662            }
663        }
664
665        let hotspots: Vec<AllocationHotspot> = hotspot_map
666            .into_iter()
667            .map(
668                |(var_name, (type_name, total_size, count))| AllocationHotspot {
669                    var_name,
670                    type_name,
671                    total_size,
672                    allocation_count: count,
673                    location: None,
674                },
675            )
676            .collect();
677
678        let system_snapshots = self
679            .system_snapshots
680            .lock()
681            .unwrap_or_else(|e| e.into_inner())
682            .clone();
683
684        AnalysisReport {
685            total_allocations: stats.total_allocations,
686            total_deallocations: stats.total_deallocations,
687            active_allocations: allocations.len(),
688            peak_memory_bytes: peak_memory as u64,
689            current_memory_bytes: current_memory as u64,
690            allocation_rate_per_sec: if elapsed > 0.0 {
691                stats.total_allocations as f64 / elapsed
692            } else {
693                0.0
694            },
695            deallocation_rate_per_sec: if elapsed > 0.0 {
696                stats.total_deallocations as f64 / elapsed
697            } else {
698                0.0
699            },
700            hotspots,
701            system_snapshots,
702        }
703    }
704
705    pub fn inner(&self) -> &Arc<MemoryTracker> {
706        &self.inner
707    }
708
709    pub fn elapsed(&self) -> Duration {
710        self.start_time.elapsed()
711    }
712
713    pub fn system_snapshots(&self) -> Vec<SystemSnapshot> {
714        self.system_snapshots
715            .lock()
716            .unwrap_or_else(|e| e.into_inner())
717            .clone()
718    }
719
720    pub fn current_system_snapshot(&self) -> SystemSnapshot {
721        SystemSnapshot {
722            timestamp: std::time::SystemTime::now()
723                .duration_since(std::time::UNIX_EPOCH)
724                .unwrap_or_default()
725                .as_millis() as u64,
726            cpu_usage_percent: system_monitor::cpu_usage(),
727            memory_usage_bytes: system_monitor::memory_used(),
728            memory_usage_percent: system_monitor::memory_usage_percent(),
729            thread_count: system_monitor::thread_count(),
730            disk_read_bps: system_monitor::disk_read_bps(),
731            disk_write_bps: system_monitor::disk_write_bps(),
732            network_rx_bps: system_monitor::network_rx_bps(),
733            network_tx_bps: system_monitor::network_tx_bps(),
734            gpu_usage_percent: system_monitor::gpu_memory_usage_percent(),
735            gpu_memory_used: system_monitor::gpu_memory_used(),
736            gpu_memory_total: system_monitor::gpu_memory_total(),
737        }
738    }
739}
740
741impl Default for Tracker {
742    fn default() -> Self {
743        Self::new()
744    }
745}
746
747impl Drop for Tracker {
748    fn drop(&mut self) {
749        // Dropping a tracker (or a clone of one that shares the event store) is a
750        // *profiler* event, not an object release. Emitting `Deallocate` for every
751        // still-live allocation here fabricated free events: they silently
752        // converted live objects into "freed" ones and destroyed the accuracy of
753        // every downstream statistic (see plan A01).
754        //
755        // The only thing a drop is still allowed to do is the legacy compatibility
756        // snapshot enabled by `with_auto_export`. That path is read-only with
757        // respect to the event store.
758        self.export_legacy_on_drop();
759    }
760}
761
762impl Tracker {
763    /// Write the legacy auto-export snapshot if `with_auto_export` was enabled.
764    ///
765    /// This is a compatibility entry point, not a session end: it only projects the
766    /// events recorded so far (`memory_analysis.json`, `lifetime.json`,
767    /// `thread_analysis.json`) and never appends release events, so a live object
768    /// stays a live object in the exported files.
769    fn export_legacy_on_drop(&self) {
770        let _suppression = crate::core::allocator::TrackingGuard::acquire();
771        let Some(path) = self.auto_export_path() else {
772            return;
773        };
774
775        // Use the event store as the single data source (covers both HeapOwner and
776        // Container observations).
777        let events = self.event_store().snapshot();
778
779        // The live list cannot reconstruct cumulative totals or the high-water mark, so
780        // the legacy drop export projects the same ledger cut as `export_all_json`
781        // instead of re-aggregating what is still alive (plan A06 follow-up).
782        let mut ledger = crate::snapshot::BindingLedger::new();
783        for event in &events {
784            ledger.apply(event);
785        }
786        let cut_ns = std::time::SystemTime::now()
787            .duration_since(std::time::UNIX_EPOCH)
788            .map(|elapsed| elapsed.as_nanos().min(u128::from(u64::MAX)) as u64)
789            .unwrap_or(0);
790        let snapshot = crate::snapshot::builder::project_snapshot(&ledger, cut_ns);
791        let containers: Vec<crate::capture::types::AllocationInfo> =
792            rebuild_allocations_from_events(&events)
793                .into_iter()
794                .filter(|allocation| allocation.ptr >= crate::analysis::VIRTUAL_PTR_BASE)
795                .collect();
796        let options = ExportJsonOptions::default();
797        if let Err(e) = export_snapshot_with_containers(
798            &snapshot,
799            &containers,
800            std::path::Path::new(&path),
801            &options,
802        ) {
803            tracing::error!("Failed to auto-export on drop: {}", e);
804        }
805    }
806
807    /// Read the configured legacy auto-export path, if enabled.
808    fn auto_export_path(&self) -> Option<String> {
809        let cfg = self.config.lock().ok()?;
810        if !cfg.auto_export_on_drop {
811            return None;
812        }
813        cfg.export_path.clone()
814    }
815}
816
817impl serde::Serialize for AnalysisReport {
818    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
819    where
820        S: serde::Serializer,
821    {
822        use serde::ser::SerializeStruct;
823        let mut state = serializer.serialize_struct("AnalysisReport", 9)?;
824        state.serialize_field("total_allocations", &self.total_allocations)?;
825        state.serialize_field("total_deallocations", &self.total_deallocations)?;
826        state.serialize_field("active_allocations", &self.active_allocations)?;
827        state.serialize_field("peak_memory_bytes", &self.peak_memory_bytes)?;
828        state.serialize_field("current_memory_bytes", &self.current_memory_bytes)?;
829        state.serialize_field("allocation_rate_per_sec", &self.allocation_rate_per_sec)?;
830        state.serialize_field("deallocation_rate_per_sec", &self.deallocation_rate_per_sec)?;
831        state.serialize_field("hotspots", &self.hotspots)?;
832        state.serialize_field("system_snapshots", &self.system_snapshots)?;
833        state.end()
834    }
835}
836
837impl serde::Serialize for AllocationHotspot {
838    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
839    where
840        S: serde::Serializer,
841    {
842        use serde::ser::SerializeStruct;
843        let mut state = serializer.serialize_struct("AllocationHotspot", 5)?;
844        state.serialize_field("var_name", &self.var_name)?;
845        state.serialize_field("type_name", &self.type_name)?;
846        state.serialize_field("total_size", &self.total_size)?;
847        state.serialize_field("allocation_count", &self.allocation_count)?;
848        state.serialize_field("location", &self.location)?;
849        state.end()
850    }
851}
852
853#[macro_export]
854macro_rules! tracker {
855    () => {
856        $crate::tracker::Tracker::new()
857    };
858}
859
860#[macro_export]
861macro_rules! track {
862    ($tracker:expr, $var:expr) => {{
863        let var_name = stringify!($var);
864        $tracker.track_as(&$var, var_name, file!(), line!(), module_path!());
865    }};
866}
867
868#[macro_export]
869macro_rules! track_clone {
870    ($tracker:expr, $source:expr, $target:expr) => {{
871        let source_name = stringify!($source);
872        let target_name = stringify!($target);
873        let source_ptr = &$source as *const _ as usize;
874        let target_ptr = &$target as *const _ as usize;
875        let type_name = $crate::utils::type_of(&$target);
876        $tracker.track_clone(
877            source_ptr,
878            target_ptr,
879            std::mem::size_of_val(&$target),
880            Some(target_name.to_string()),
881            Some(type_name.to_string()),
882            file!(),
883            line!(),
884            module_path!(),
885        );
886    }};
887}
888
889#[cfg(test)]
890mod tests;