1use super::core_types::{
7 AllocationInfo, MemoryStats, ThreadRegistryStats, TrackingError, TrackingResult,
8};
9use dashmap::DashMap;
10use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
11use std::sync::{Arc, OnceLock};
12use std::thread;
13
14const STRATEGY_GLOBAL_SINGLETON: u64 = 0;
15const STRATEGY_THREAD_LOCAL: u64 = 1;
16
17pub static TRACKING_STRATEGY: AtomicU64 = AtomicU64::new(STRATEGY_GLOBAL_SINGLETON);
18
19static GLOBAL_TRACKER: OnceLock<Arc<MemoryTracker>> = OnceLock::new();
20
21thread_local! {
22 static THREAD_LOCAL_TRACKER: Arc<MemoryTracker> = {
23 let tracker = Arc::new(MemoryTracker::new());
24 register_current_thread_tracker_local(&tracker);
25 tracker
26 };
27}
28
29static LOCAL_THREAD_REGISTRY: OnceLock<
31 Arc<dashmap::DashMap<thread::ThreadId, Arc<MemoryTracker>>>,
32> = OnceLock::new();
33
34fn get_local_registry() -> Arc<dashmap::DashMap<thread::ThreadId, Arc<MemoryTracker>>> {
35 LOCAL_THREAD_REGISTRY
36 .get_or_init(|| Arc::new(dashmap::DashMap::new()))
37 .clone()
38}
39
40fn register_current_thread_tracker_local(tracker: &Arc<MemoryTracker>) {
41 let thread_id = thread::current().id();
42 get_local_registry().insert(thread_id, tracker.clone());
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct ClosedBinding {
48 pub ptr: usize,
50 pub serial: u64,
52 pub size: usize,
54}
55
56pub struct MemoryTracker {
66 active_allocations: DashMap<usize, Vec<AllocationInfo>>,
74
75 total_allocations: AtomicU64,
77 total_allocated: AtomicU64,
78 total_deallocations: AtomicU64,
79 total_deallocated: AtomicU64,
80 peak_allocations: AtomicUsize,
81 peak_memory: AtomicU64,
82 live_bindings: AtomicUsize,
86
87 duplicate_observations: AtomicU64,
91 next_binding_serial: AtomicU64,
93 replaced_bindings: AtomicU64,
97 degraded_releases: AtomicU64,
101
102 fast_mode: AtomicU64,
104}
105
106impl MemoryTracker {
107 pub fn new() -> Self {
109 let fast_mode = std::env::var("MEMSCOPE_TEST_MODE").is_ok() || cfg!(test);
110
111 Self {
112 active_allocations: DashMap::new(),
113 total_allocations: AtomicU64::new(0),
114 total_allocated: AtomicU64::new(0),
115 total_deallocations: AtomicU64::new(0),
116 total_deallocated: AtomicU64::new(0),
117 peak_allocations: AtomicUsize::new(0),
118 peak_memory: AtomicU64::new(0),
119 live_bindings: AtomicUsize::new(0),
120 next_binding_serial: AtomicU64::new(1),
121 duplicate_observations: AtomicU64::new(0),
122 replaced_bindings: AtomicU64::new(0),
123 degraded_releases: AtomicU64::new(0),
124 fast_mode: AtomicU64::new(fast_mode as u64),
125 }
126 }
127
128 pub fn track_allocation(&self, ptr: usize, size: usize) -> TrackingResult<()> {
151 self.record_binding(ptr, size, None)
152 }
153
154 fn record_binding(
156 &self,
157 ptr: usize,
158 size: usize,
159 var_name: Option<String>,
160 ) -> TrackingResult<()> {
161 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
162
163 let mut entries = self.active_allocations.entry(ptr).or_default();
164
165 if let Some(existing) = entries.last_mut() {
166 if existing.size == size {
167 if var_name.is_some() {
169 existing.var_name = var_name;
170 }
171 self.duplicate_observations.fetch_add(1, Ordering::Relaxed);
172 return Ok(());
173 }
174
175 self.replaced_bindings.fetch_add(1, Ordering::Relaxed);
181 tracing::debug!(
182 "conflicting allocation: address {:#x} still holds a live binding",
183 ptr
184 );
185 }
186
187 let mut allocation = AllocationInfo::new(ptr, size);
188 allocation.binding_serial = self.next_binding_serial.fetch_add(1, Ordering::Relaxed);
189 if var_name.is_some() {
190 allocation.var_name = var_name;
191 }
192 entries.push(allocation);
193 drop(entries);
194 self.live_bindings.fetch_add(1, Ordering::Relaxed);
195
196 self.total_allocations.fetch_add(1, Ordering::Relaxed);
197 self.total_allocated
198 .fetch_add(size as u64, Ordering::Relaxed);
199 self.update_peak_allocations();
200 self.update_peak_memory();
201 Ok(())
202 }
203
204 fn live_binding_count(&self) -> usize {
206 self.live_bindings.load(Ordering::Relaxed)
207 }
208
209 pub fn live_binding_count_at(&self, ptr: usize) -> usize {
211 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
212 self.active_allocations
213 .get(&ptr)
214 .map(|entries| entries.len())
215 .unwrap_or(0)
216 }
217
218 fn update_peak_allocations(&self) {
220 loop {
222 let current_count = self.live_binding_count();
223 let current_peak = self.peak_allocations.load(Ordering::Relaxed);
224 if current_count <= current_peak {
225 break;
226 }
227 if self
228 .peak_allocations
229 .compare_exchange_weak(
230 current_peak,
231 current_count,
232 Ordering::Relaxed,
233 Ordering::Relaxed,
234 )
235 .is_ok()
236 {
237 break;
238 }
239 }
240 }
241
242 fn update_peak_memory(&self) {
244 loop {
245 let current_memory = self
246 .total_allocated
247 .load(Ordering::Relaxed)
248 .saturating_sub(self.total_deallocated.load(Ordering::Relaxed));
249 let current_peak_memory = self.peak_memory.load(Ordering::Relaxed);
250 if current_memory <= current_peak_memory {
251 break;
252 }
253 if self
254 .peak_memory
255 .compare_exchange_weak(
256 current_peak_memory,
257 current_memory,
258 Ordering::Relaxed,
259 Ordering::Relaxed,
260 )
261 .is_ok()
262 {
263 break;
264 }
265 }
266 }
267
268 pub fn track_deallocation(&self, ptr: usize) -> TrackingResult<bool> {
280 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
281 Ok(self.close_binding(ptr, None).is_some())
282 }
283
284 pub fn close_and_report(
290 &self,
291 ptr: usize,
292 serial: Option<u64>,
293 ) -> TrackingResult<Option<ClosedBinding>> {
294 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
295 Ok(self.close_binding(ptr, serial))
296 }
297
298 pub fn binding_at(&self, ptr: usize) -> Option<ClosedBinding> {
303 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
304 self.active_allocations.get(&ptr).and_then(|entries| {
305 entries.last().map(|binding| ClosedBinding {
306 ptr,
307 serial: binding.binding_serial,
308 size: binding.size,
309 })
310 })
311 }
312
313 pub fn binding_containing(&self, addr: usize) -> Option<ClosedBinding> {
319 let _gate = crate::core::allocator::capture_gate::acquire(addr);
320 let mut candidates: Vec<ClosedBinding> = Vec::new();
321 for entry in self.active_allocations.iter() {
322 for binding in entry.value() {
323 let start = binding.ptr;
324 let end = start.saturating_add(binding.size);
325 if addr >= start && addr < end {
326 candidates.push(ClosedBinding {
327 ptr: start,
328 serial: binding.binding_serial,
329 size: binding.size,
330 });
331 }
332 }
333 }
334
335 match candidates.len() {
340 1 => candidates.pop(),
341 0 => None,
342 count => {
343 tracing::debug!(
344 "ambiguous containment at {:#x}: {count} live blocks contain it",
345 addr
346 );
347 None
348 }
349 }
350 }
351
352 pub fn track_deallocation_of(&self, ptr: usize, serial: u64) -> TrackingResult<bool> {
358 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
359 Ok(self.close_binding(ptr, Some(serial)).is_some())
360 }
361
362 fn close_binding(&self, ptr: usize, serial: Option<u64>) -> Option<ClosedBinding> {
364 let closed = {
365 let mut entries = self.active_allocations.get_mut(&ptr)?;
366
367 let index = match serial {
368 Some(serial) => entries
369 .iter()
370 .position(|binding| binding.binding_serial == serial),
371 None => {
372 if entries.len() > 1 {
373 self.degraded_releases.fetch_add(1, Ordering::Relaxed);
374 tracing::debug!(
375 "degraded release: address {:#x} holds {} bindings, closing the oldest",
376 ptr,
377 entries.len()
378 );
379 }
380 Some(0)
381 }
382 };
383
384 let Some(index) = index else {
385 return None;
388 };
389
390 let closed = entries.remove(index);
391 if entries.is_empty() {
392 drop(entries);
393 self.active_allocations.remove(&ptr);
394 }
395 closed
396 };
397
398 self.live_bindings.fetch_sub(1, Ordering::Relaxed);
399 self.total_deallocations.fetch_add(1, Ordering::Relaxed);
400 self.total_deallocated
401 .fetch_add(closed.size as u64, Ordering::Relaxed);
402 Some(ClosedBinding {
403 ptr: closed.ptr,
404 serial: closed.binding_serial,
405 size: closed.size,
406 })
407 }
408
409 pub fn track_reallocation(
423 &self,
424 old_ptr: usize,
425 new_ptr: usize,
426 _old_size: usize,
427 new_size: usize,
428 ) -> TrackingResult<()> {
429 let _gate = crate::core::allocator::capture_gate::acquire(old_ptr);
430
431 let previous = {
432 let Some(mut entries) = self.active_allocations.get_mut(&old_ptr) else {
433 return self.open_unmatched_binding(new_ptr, new_size);
434 };
435 let previous = entries.pop();
439 if entries.is_empty() {
440 drop(entries);
441 self.active_allocations.remove(&old_ptr);
442 }
443 previous
444 };
445
446 let Some(mut allocation) = previous else {
447 return self.open_unmatched_binding(new_ptr, new_size);
448 };
449
450 let previous_size = allocation.size;
453 allocation.ptr = new_ptr;
454 allocation.size = new_size;
455 self.active_allocations
456 .entry(new_ptr)
457 .or_default()
458 .push(allocation);
459
460 self.total_allocated
461 .fetch_add(new_size as u64, Ordering::Relaxed);
462 self.total_deallocated
463 .fetch_add(previous_size as u64, Ordering::Relaxed);
464 self.update_peak_memory();
465 Ok(())
466 }
467
468 fn open_unmatched_binding(&self, new_ptr: usize, new_size: usize) -> TrackingResult<()> {
473 let mut new_binding = AllocationInfo::new(new_ptr, new_size);
474 new_binding.binding_serial = self.next_binding_serial.fetch_add(1, Ordering::Relaxed);
475 self.active_allocations
476 .entry(new_ptr)
477 .or_default()
478 .push(new_binding);
479 self.live_bindings.fetch_add(1, Ordering::Relaxed);
480 self.total_allocations.fetch_add(1, Ordering::Relaxed);
481 self.total_allocated
482 .fetch_add(new_size as u64, Ordering::Relaxed);
483 self.update_peak_allocations();
484 self.update_peak_memory();
485 Ok(())
486 }
487
488 pub fn get_allocation_size(&self, ptr: usize) -> Option<usize> {
497 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
498 self.active_allocations
499 .get(&ptr)
500 .and_then(|entries| entries.last().map(|binding| binding.size))
501 }
502
503 pub fn associate_var(
512 &self,
513 ptr: usize,
514 var_name: String,
515 type_name: String,
516 source_file: Option<&str>,
517 source_line: Option<u32>,
518 ) -> TrackingResult<()> {
519 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
520 if let Some(mut entries) = self.active_allocations.get_mut(&ptr) {
521 if let Some(allocation) = entries.last_mut() {
522 allocation.var_name = Some(var_name);
523 allocation.type_name = Some(type_name);
524 if let (Some(file), Some(line)) = (source_file, source_line) {
525 allocation.set_source_location(file, line);
526 }
527 }
528 }
529
530 Ok(())
531 }
532
533 pub fn fast_track_allocation(
540 &self,
541 ptr: usize,
542 size: usize,
543 var_name: String,
544 ) -> TrackingResult<()> {
545 self.record_binding(ptr, size, Some(var_name))
546 }
547
548 pub fn binding_serial(&self, ptr: usize) -> Option<u64> {
553 let _gate = crate::core::allocator::capture_gate::acquire(ptr);
554 self.active_allocations
555 .get(&ptr)
556 .and_then(|entries| entries.last().map(|binding| binding.binding_serial))
557 }
558
559 pub fn duplicate_observations(&self) -> u64 {
565 self.duplicate_observations.load(Ordering::Relaxed)
566 }
567
568 pub fn replaced_bindings(&self) -> u64 {
573 self.replaced_bindings.load(Ordering::Relaxed)
574 }
575
576 pub fn degraded_releases(&self) -> u64 {
582 self.degraded_releases.load(Ordering::Relaxed)
583 }
584
585 pub fn get_stats(&self) -> TrackingResult<MemoryStats> {
587 let _gate = crate::core::allocator::capture_gate::acquire(0);
588 let active_count = self.live_binding_count();
589 let total_allocated = self.total_allocated.load(Ordering::Relaxed);
590 let total_deallocated = self.total_deallocated.load(Ordering::Relaxed);
591 let active_memory = total_allocated.saturating_sub(total_deallocated);
592
593 Ok(MemoryStats {
594 total_allocations: self.total_allocations.load(Ordering::Relaxed),
595 total_allocated,
596 active_allocations: active_count,
597 active_memory,
598 peak_allocations: self.peak_allocations.load(Ordering::Relaxed),
599 peak_memory: self.peak_memory.load(Ordering::Relaxed),
600 total_deallocations: self.total_deallocations.load(Ordering::Relaxed),
601 total_deallocated,
602 leaked_allocations: 0,
603 leaked_memory: 0,
604 })
605 }
606
607 pub fn detect_leaks(&self) -> (usize, u64) {
613 let _gate = crate::core::allocator::capture_gate::acquire(0);
614 let active_count = self.live_binding_count();
615 let total_allocated = self.total_allocated.load(Ordering::Relaxed);
616 let total_deallocated = self.total_deallocated.load(Ordering::Relaxed);
617 let active_memory = total_allocated.saturating_sub(total_deallocated);
618 (active_count, active_memory)
619 }
620
621 pub fn get_active_allocations(&self) -> TrackingResult<Vec<AllocationInfo>> {
623 let _gate = crate::core::allocator::capture_gate::acquire(0);
624 let mut allocations = Vec::new();
625 for entry in self.active_allocations.iter() {
626 allocations.extend(entry.value().iter().cloned());
627 }
628 Ok(allocations)
629 }
630
631 pub fn get_memory_by_type(&self) -> TrackingResult<std::collections::HashMap<String, usize>> {
633 let _gate = crate::core::allocator::capture_gate::acquire(0);
634 let mut type_sizes: std::collections::HashMap<String, usize> =
635 std::collections::HashMap::new();
636
637 for entry in self.active_allocations.iter() {
638 for alloc in entry.value() {
639 let type_name = alloc
640 .type_name
641 .clone()
642 .unwrap_or_else(|| "unknown".to_string());
643 *type_sizes.entry(type_name).or_insert(0) += alloc.size;
644 }
645 }
646
647 Ok(type_sizes)
648 }
649
650 pub fn set_fast_mode(&self, enabled: bool) {
652 self.fast_mode.store(enabled as u64, Ordering::Relaxed);
653 }
654
655 pub fn is_fast_mode(&self) -> bool {
657 self.fast_mode.load(Ordering::Relaxed) != 0
658 }
659
660 pub fn enable_fast_mode(&self) {
662 self.fast_mode.store(1, Ordering::Relaxed);
663 }
664
665 pub fn ensure_memory_analysis_path<P: AsRef<std::path::Path>>(
667 &self,
668 path: P,
669 ) -> std::path::PathBuf {
670 let path = path.as_ref();
671 let base_dir = path
672 .parent()
673 .unwrap_or(std::path::Path::new("MemoryAnalysis"));
674
675 if let Err(e) = std::fs::create_dir_all(base_dir) {
676 tracing::warn!("Failed to create directory {:?}: {}", base_dir, e);
677 }
678
679 if base_dir == std::path::Path::new("") {
680 std::path::Path::new("MemoryAnalysis").join(path.file_name().unwrap_or_default())
681 } else {
682 path.to_path_buf()
683 }
684 }
685
686 pub fn ensure_memscope_path<P: AsRef<std::path::Path>>(&self, path: P) -> std::path::PathBuf {
688 let mut output_path = self.ensure_memory_analysis_path(path);
689
690 if output_path.extension().is_none()
691 || output_path.extension() != Some(std::ffi::OsStr::new("memscope"))
692 {
693 output_path.set_extension("memscope");
694 }
695
696 output_path
697 }
698
699 pub fn export_to_memscope<P: AsRef<std::path::Path>>(&self, path: P) -> TrackingResult<()> {
704 self.export_to_json(path)
705 }
706
707 pub fn export_to_json<P: AsRef<std::path::Path>>(&self, path: P) -> TrackingResult<()> {
709 let output_path = self.ensure_memory_analysis_path(&path);
710
711 let final_path = if output_path.is_dir() {
712 output_path.join("memory_analysis.json")
713 } else {
714 output_path
715 };
716
717 let allocations = self.get_active_allocations()?;
718
719 let json = serde_json::to_string_pretty(&allocations)
720 .map_err(|e| TrackingError::SerializationError(e.to_string()))?;
721
722 std::fs::write(&final_path, json).map_err(|e| TrackingError::ExportError(e.to_string()))?;
723
724 Ok(())
725 }
726}
727
728impl Default for MemoryTracker {
729 fn default() -> Self {
730 Self::new()
731 }
732}
733
734impl Drop for MemoryTracker {
735 fn drop(&mut self) {
736 if std::env::var("MEMSCOPE_VERBOSE").is_ok() {
737 tracing::info!(
738 "💡 Tip: Use tracker.export_to_json() before drop to save analysis results"
739 );
740 }
741
742 let active_count = self.live_binding_count();
743 if active_count > 0 {
744 tracing::warn!(
745 "Dropping MemoryTracker with {} active allocations (potential memory leaks)",
746 active_count
747 );
748 }
749
750 self.active_allocations.clear();
752 self.live_bindings.store(0, Ordering::Relaxed);
753 }
754}
755
756pub fn configure_tracking_strategy(is_concurrent: bool) {
761 let strategy = if is_concurrent {
762 STRATEGY_THREAD_LOCAL
763 } else {
764 STRATEGY_GLOBAL_SINGLETON
765 };
766
767 TRACKING_STRATEGY.store(strategy, Ordering::Relaxed);
768
769 tracing::info!(
770 "Configured tracking strategy: {}",
771 if is_concurrent {
772 "thread-local"
773 } else {
774 "global-singleton"
775 }
776 );
777}
778
779pub fn get_global_tracker() -> Arc<MemoryTracker> {
786 GLOBAL_TRACKER
787 .get_or_init(|| Arc::new(MemoryTracker::new()))
788 .clone()
789}
790
791pub fn get_tracker() -> Arc<MemoryTracker> {
797 match TRACKING_STRATEGY.load(Ordering::Relaxed) {
798 STRATEGY_GLOBAL_SINGLETON => GLOBAL_TRACKER
799 .get_or_init(|| Arc::new(MemoryTracker::new()))
800 .clone(),
801 STRATEGY_THREAD_LOCAL => THREAD_LOCAL_TRACKER.with(|tracker| tracker.clone()),
802 _ => {
803 tracing::warn!("Unknown tracking strategy, falling back to global singleton");
804 GLOBAL_TRACKER
805 .get_or_init(|| Arc::new(MemoryTracker::new()))
806 .clone()
807 }
808 }
809}
810
811pub fn collect_all_trackers_local() -> Vec<Arc<MemoryTracker>> {
813 get_local_registry()
814 .iter()
815 .map(|entry| entry.value().clone())
816 .collect()
817}
818
819pub fn get_registry_stats_local() -> ThreadRegistryStats {
821 let registry = get_local_registry();
822 let total_threads = registry.len();
823
824 ThreadRegistryStats {
825 total_threads_registered: total_threads,
826 active_threads: total_threads,
827 dead_references: 0,
828 }
829}
830
831pub fn has_active_trackers_local() -> bool {
833 !get_local_registry().is_empty()
834}
835
836#[cfg(test)]
837mod tests;