1use 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
132type RecordedState = std::collections::HashMap<u64, RecordedBinding>;
139type RecordedSlot = Arc<Mutex<RecordedState>>;
141type RecordedBindings = Arc<DashMap<usize, RecordedSlot>>;
143
144#[derive(Debug, Clone, Copy)]
151struct RecordedBinding {
152 size: usize,
154 typed: bool,
156 charged: bool,
159}
160
161pub struct Tracker {
162 inner: Arc<MemoryTracker>,
163 event_store: Arc<EventStore>,
164 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(), 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn duplicate_observations(&self) -> u64 {
581 self.inner.duplicate_observations()
582 }
583
584 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 self.export_legacy_on_drop();
759 }
760}
761
762impl Tracker {
763 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 let events = self.event_store().snapshot();
778
779 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 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;