1use std::{
7 fs::File,
8 io::Write,
9 path::PathBuf,
10 sync::{
11 atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
12 Arc, OnceLock,
13 },
14 thread::ThreadId,
15};
16
17use crossbeam::queue::SegQueue;
18use dashmap::DashMap;
19
20use super::lockfree_types::{Event, EventType, MemoryStats};
21
22static TRACKING_ENABLED: AtomicBool = AtomicBool::new(false);
23static OUTPUT_DIRECTORY: OnceLock<std::path::PathBuf> = OnceLock::new();
24
25pub struct ThreadLocalTracker {
30 thread_id: ThreadId,
31 events: Arc<SegQueue<Event>>,
32 active_allocations: Arc<DashMap<usize, usize>>,
33 total_allocations: AtomicU64,
34 total_allocated: AtomicU64,
35 total_deallocations: AtomicU64,
36 total_deallocated: AtomicU64,
37 active_memory: AtomicU64,
38 peak_memory: AtomicU64,
39 output_file: PathBuf,
40 sample_rate: f64,
41 total_seen: AtomicUsize,
42 total_tracked: AtomicUsize,
43}
44
45impl ThreadLocalTracker {
46 pub fn new(thread_id: ThreadId, output_file: PathBuf, sample_rate: f64) -> Self {
47 Self {
48 thread_id,
49 events: Arc::new(SegQueue::new()),
50 active_allocations: Arc::new(DashMap::new()),
51 total_allocations: AtomicU64::new(0),
52 total_allocated: AtomicU64::new(0),
53 total_deallocations: AtomicU64::new(0),
54 total_deallocated: AtomicU64::new(0),
55 active_memory: AtomicU64::new(0),
56 peak_memory: AtomicU64::new(0),
57 output_file,
58 sample_rate: sample_rate.clamp(0.0, 1.0),
59 total_seen: AtomicUsize::new(0),
60 total_tracked: AtomicUsize::new(0),
61 }
62 }
63
64 pub fn track_allocation(&self, ptr: usize, size: usize, call_stack_hash: u64) {
65 self.total_seen.fetch_add(1, Ordering::Relaxed);
66
67 if self.sample_rate < 1.0 {
68 let sample_decision = rand::random::<f64>();
69 if sample_decision >= self.sample_rate {
70 return;
71 }
72 }
73
74 self.total_tracked.fetch_add(1, Ordering::Relaxed);
75
76 let event = Event::allocation(ptr, size, call_stack_hash, self.thread_id);
77 self.events.push(event);
78
79 self.active_allocations.insert(ptr, size);
80
81 self.total_allocations.fetch_add(1, Ordering::Relaxed);
82 self.total_allocated
83 .fetch_add(size as u64, Ordering::Relaxed);
84
85 let new_active = self.active_memory.fetch_add(size as u64, Ordering::Relaxed) + size as u64;
86
87 let mut current_peak = self.peak_memory.load(Ordering::Relaxed);
89 let mut backoff_count = 0u32;
90 const MAX_BACKOFF_ATTEMPTS: u32 = 10;
91
92 while new_active > current_peak {
93 match self.peak_memory.compare_exchange_weak(
94 current_peak,
95 new_active,
96 Ordering::Relaxed,
97 Ordering::Relaxed,
98 ) {
99 Ok(_) => break,
100 Err(actual) => {
101 current_peak = actual;
102 backoff_count += 1;
103
104 if backoff_count < MAX_BACKOFF_ATTEMPTS {
106 std::hint::spin_loop();
108 } else if backoff_count < MAX_BACKOFF_ATTEMPTS * 2 {
109 std::thread::yield_now();
111 } else {
112 std::thread::sleep(std::time::Duration::from_micros(1));
114 }
115 }
116 }
117 }
118 }
119
120 pub fn track_deallocation(&self, ptr: usize, call_stack_hash: u64) {
121 let size = self
122 .active_allocations
123 .remove(&ptr)
124 .map(|(_, v)| v)
125 .unwrap_or(0);
126
127 let event = Event::deallocation(ptr, size, call_stack_hash, self.thread_id);
128 self.events.push(event);
129
130 self.total_deallocations.fetch_add(1, Ordering::Relaxed);
131 self.total_deallocated
132 .fetch_add(size as u64, Ordering::Relaxed);
133
134 let mut current = self.active_memory.load(Ordering::Relaxed);
136 while let Err(actual) = self.active_memory.compare_exchange_weak(
137 current,
138 current.saturating_sub(size as u64),
139 Ordering::Relaxed,
140 Ordering::Relaxed,
141 ) {
142 current = actual;
143 }
144 }
145
146 pub fn get_stats(&self) -> MemoryStats {
147 MemoryStats {
148 total_allocations: self.total_allocations.load(Ordering::Relaxed) as usize,
149 total_allocated: self.total_allocated.load(Ordering::Relaxed) as usize,
150 total_deallocations: self.total_deallocations.load(Ordering::Relaxed) as usize,
151 total_deallocated: self.total_deallocated.load(Ordering::Relaxed) as usize,
152 active_memory: self.active_memory.load(Ordering::Relaxed) as usize,
153 peak_memory: self.peak_memory.load(Ordering::Relaxed) as usize,
154 }
155 }
156
157 pub fn get_sampling_stats(&self) -> (usize, usize) {
158 (
159 self.total_seen.load(Ordering::Relaxed),
160 self.total_tracked.load(Ordering::Relaxed),
161 )
162 }
163
164 pub fn finalize(&self) -> std::io::Result<()> {
165 let mut events = Vec::new();
166 while let Some(event) = self.events.pop() {
167 events.push(event);
168 }
169
170 if events.is_empty() {
171 return Ok(());
172 }
173
174 if let Some(parent) = self.output_file.parent() {
175 std::fs::create_dir_all(parent)?;
176 }
177
178 let mut file = File::create(&self.output_file)?;
179
180 let header = "MEMSCOPE_LOCKFREE";
181 file.write_all(header.as_bytes())?;
182
183 for event in events {
184 self.write_event(&mut file, &event)?;
185 }
186
187 file.flush()?;
188 Ok(())
189 }
190
191 fn write_event(&self, file: &mut File, event: &Event) -> std::io::Result<()> {
192 let event_type_byte = match event.event_type {
193 EventType::Allocation => 1u8,
194 EventType::Deallocation => 2u8,
195 EventType::Clone => 3u8,
196 EventType::Move => 4u8,
197 EventType::Borrow => 5u8,
198 EventType::MutBorrow => 6u8,
199 };
200 file.write_all(&event_type_byte.to_le_bytes())?;
201 file.write_all(&event.timestamp.to_le_bytes())?;
202 file.write_all(&event.ptr.to_le_bytes())?;
203 file.write_all(&event.size.to_le_bytes())?;
204 file.write_all(&event.call_stack_hash.to_le_bytes())?;
205 Ok(())
206 }
207
208 pub fn thread_id(&self) -> ThreadId {
209 self.thread_id
210 }
211
212 pub fn output_file(&self) -> &PathBuf {
213 &self.output_file
214 }
215
216 pub fn event_count(&self) -> usize {
217 self.events.len()
218 }
219
220 pub fn clear_events(&self) {
221 while self.events.pop().is_some() {}
222 }
223}
224
225impl Drop for ThreadLocalTracker {
226 fn drop(&mut self) {
227 if let Err(e) = self.finalize() {
228 tracing::warn!("Failed to finalize thread-local tracker: {}", e);
229 }
230 }
231}
232
233pub fn calculate_call_stack_hash(call_stack: &[usize]) -> u64 {
234 use std::collections::hash_map::DefaultHasher;
235 use std::hash::{Hash, Hasher};
236
237 let mut hasher = DefaultHasher::new();
238 for addr in call_stack {
239 addr.hash(&mut hasher);
240 }
241 hasher.finish()
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_thread_local_tracker_creation() {
250 let thread_id = std::thread::current().id();
251 let output_file = PathBuf::from("/tmp/test_tracker.bin");
252 let tracker = ThreadLocalTracker::new(thread_id, output_file, 1.0);
253
254 assert_eq!(tracker.thread_id(), thread_id);
255 assert_eq!(tracker.event_count(), 0);
256 }
257
258 #[test]
259 fn test_allocation_tracking() {
260 let thread_id = std::thread::current().id();
261 let output_file = PathBuf::from("/tmp/test_tracker2.bin");
262 let tracker = ThreadLocalTracker::new(thread_id, output_file, 1.0);
263
264 tracker.track_allocation(0x1000, 1024, 12345);
265
266 let stats = tracker.get_stats();
267 assert_eq!(stats.total_allocations, 1);
268 assert_eq!(stats.total_allocated, 1024);
269 assert_eq!(stats.active_memory, 1024);
270 assert_eq!(tracker.event_count(), 1);
271 }
272
273 #[test]
274 fn test_deallocation_tracking() {
275 let thread_id = std::thread::current().id();
276 let output_file = PathBuf::from("/tmp/test_tracker3.bin");
277 let tracker = ThreadLocalTracker::new(thread_id, output_file, 1.0);
278
279 tracker.track_allocation(0x1000, 1024, 12345);
280 tracker.track_deallocation(0x1000, 12345);
281
282 let stats = tracker.get_stats();
283 assert_eq!(stats.total_allocations, 1);
284 assert_eq!(stats.total_deallocations, 1);
285 assert_eq!(stats.active_memory, 0);
286 }
287
288 #[test]
289 fn test_call_stack_hash() {
290 let call_stack = vec![0x1000, 0x2000, 0x3000];
291 let hash1 = calculate_call_stack_hash(&call_stack);
292 let hash2 = calculate_call_stack_hash(&call_stack);
293
294 assert_eq!(hash1, hash2);
295
296 let different_stack = vec![0x1000, 0x2000, 0x4000];
297 let hash3 = calculate_call_stack_hash(&different_stack);
298 assert_ne!(hash1, hash3);
299 }
300}
301
302thread_local! {
303 static THREAD_TRACKER: std::cell::RefCell<Option<ThreadLocalTracker>> = const { std::cell::RefCell::new(None) };
304}
305
306fn get_thread_id() -> u64 {
307 crate::utils::current_thread_id_u64()
308}
309
310pub fn init_thread_tracker(
311 output_dir: &std::path::Path,
312 sample_rate: Option<f64>,
313) -> Result<(), Box<dyn std::error::Error>> {
314 let sample_rate = sample_rate.unwrap_or(1.0);
315 let thread_id = std::thread::current().id();
316 let output_file = output_dir.join(format!("memscope_thread_{}.bin", get_thread_id()));
317
318 let tracker = ThreadLocalTracker::new(thread_id, output_file, sample_rate);
319
320 THREAD_TRACKER.with(|thread_tracker| {
321 *thread_tracker.borrow_mut() = Some(tracker);
322 });
323
324 Ok(())
325}
326
327pub fn track_allocation_lockfree(
328 ptr: usize,
329 size: usize,
330 call_stack_hash: u64,
331) -> Result<(), Box<dyn std::error::Error>> {
332 THREAD_TRACKER.with(|thread_tracker| {
333 if let Some(ref tracker) = *thread_tracker.borrow() {
334 tracker.track_allocation(ptr, size, call_stack_hash);
335 Ok(())
336 } else {
337 Err("Thread tracker not initialized. Call init_thread_tracker() first.".into())
338 }
339 })
340}
341
342pub fn track_deallocation_lockfree(
343 ptr: usize,
344 call_stack_hash: u64,
345) -> Result<(), Box<dyn std::error::Error>> {
346 THREAD_TRACKER.with(|thread_tracker| {
347 if let Some(ref tracker) = *thread_tracker.borrow() {
348 tracker.track_deallocation(ptr, call_stack_hash);
349 Ok(())
350 } else {
351 Err("Thread tracker not initialized. Call init_thread_tracker() first.".into())
352 }
353 })
354}
355
356pub fn finalize_thread_tracker() -> Result<(), Box<dyn std::error::Error>> {
357 THREAD_TRACKER.with(|thread_tracker| {
358 let mut tracker_ref = thread_tracker.borrow_mut();
359 if let Some(ref mut tracker) = *tracker_ref {
360 tracker
361 .finalize()
362 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
363 } else {
364 Ok(())
365 }
366 })
367}
368
369pub fn get_current_tracker() -> Option<ThreadLocalTracker> {
380 THREAD_TRACKER.with(|thread_tracker| {
381 thread_tracker
382 .borrow()
383 .as_ref()
384 .map(|tracker| ThreadLocalTracker {
385 thread_id: tracker.thread_id,
386 events: Arc::clone(&tracker.events),
387 active_allocations: Arc::clone(&tracker.active_allocations),
388 total_allocations: AtomicU64::new(
389 tracker.total_allocations.load(Ordering::Relaxed),
390 ),
391 total_allocated: AtomicU64::new(tracker.total_allocated.load(Ordering::Relaxed)),
392 total_deallocations: AtomicU64::new(
393 tracker.total_deallocations.load(Ordering::Relaxed),
394 ),
395 total_deallocated: AtomicU64::new(
396 tracker.total_deallocated.load(Ordering::Relaxed),
397 ),
398 active_memory: AtomicU64::new(tracker.active_memory.load(Ordering::Relaxed)),
399 peak_memory: AtomicU64::new(tracker.peak_memory.load(Ordering::Relaxed)),
400 output_file: tracker.output_file.clone(),
401 sample_rate: tracker.sample_rate,
402 total_seen: AtomicUsize::new(tracker.total_seen.load(Ordering::Relaxed)),
403 total_tracked: AtomicUsize::new(tracker.total_tracked.load(Ordering::Relaxed)),
404 })
405 })
406}
407
408pub fn trace_all<P: AsRef<std::path::Path>>(
409 output_dir: &P,
410) -> Result<(), Box<dyn std::error::Error>> {
411 let output_path = output_dir.as_ref().to_path_buf();
412
413 let _ = OUTPUT_DIRECTORY.set(output_path.clone());
414
415 if output_path.exists() {
416 let timestamp = std::time::SystemTime::now()
417 .duration_since(std::time::UNIX_EPOCH)
418 .map(|d| d.as_secs())
419 .unwrap_or(0);
420 let backup_name = format!(
421 "{}.backup.{}",
422 output_path
423 .file_name()
424 .unwrap_or_default()
425 .to_string_lossy(),
426 timestamp
427 );
428 let backup_path = output_path.with_file_name(backup_name);
429 std::fs::rename(&output_path, &backup_path)?;
430 tracing::info!("Existing directory backed up to: {}", backup_path.display());
431 }
432 std::fs::create_dir_all(&output_path)?;
433
434 TRACKING_ENABLED.store(true, Ordering::SeqCst);
435
436 tracing::info!("Lockfree tracking started: {}", output_path.display());
437
438 Ok(())
439}
440
441pub fn trace_thread<P: AsRef<std::path::Path>>(
442 output_dir: &P,
443) -> Result<(), Box<dyn std::error::Error>> {
444 let output_path = output_dir.as_ref().to_path_buf();
445
446 if !output_path.exists() {
447 std::fs::create_dir_all(&output_path)?;
448 }
449
450 init_thread_tracker(&output_path, Some(1.0))?;
451
452 Ok(())
453}
454
455pub fn stop_tracing() -> Result<(), Box<dyn std::error::Error>> {
456 if !TRACKING_ENABLED.load(Ordering::SeqCst) {
457 return Ok(());
458 }
459
460 let _ = finalize_thread_tracker();
461
462 TRACKING_ENABLED.store(false, Ordering::SeqCst);
463
464 Ok(())
465}
466
467pub fn is_tracking() -> bool {
468 TRACKING_ENABLED.load(Ordering::SeqCst)
469}
470
471pub fn memory_snapshot() -> super::lockfree_types::MemorySnapshot {
472 use super::lockfree_types::MemorySnapshot;
473
474 let (current_mb, peak_mb, allocations, deallocations) = THREAD_TRACKER.with(|thread_tracker| {
475 if let Some(tracker) = thread_tracker.borrow().as_ref() {
476 let stats = tracker.get_stats();
477 (
478 stats.active_memory as f64 / (1024.0 * 1024.0),
479 stats.peak_memory as f64 / (1024.0 * 1024.0),
480 stats.total_allocations,
481 stats.total_deallocations,
482 )
483 } else {
484 (0.0, 0.0, 0, 0)
485 }
486 });
487
488 MemorySnapshot {
489 current_mb,
490 peak_mb,
491 allocations: allocations as u64,
492 deallocations: deallocations as u64,
493 active_threads: if TRACKING_ENABLED.load(Ordering::SeqCst) {
494 1
495 } else {
496 0
497 },
498 }
499}
500
501pub fn quick_trace<F, R>(f: F) -> R
502where
503 F: FnOnce() -> R,
504{
505 let temp_dir = std::env::temp_dir().join("memscope_lockfree_quick");
506
507 if trace_all(&temp_dir).is_err() {
508 return f();
509 }
510
511 let result = f();
512
513 let _ = stop_tracing();
514
515 tracing::info!("Quick trace completed - check {}", temp_dir.display());
516
517 result
518}
519
520#[cfg(test)]
521mod global_api_tests {
522 use super::*;
523
524 #[test]
525 fn test_init_thread_tracker() {
526 let temp_dir = std::env::temp_dir().join("memscope_global_test");
527 std::fs::create_dir_all(&temp_dir).unwrap();
528
529 let result = init_thread_tracker(&temp_dir, Some(1.0));
530 assert!(result.is_ok(), "Should successfully initialize tracker");
531
532 let result2 = init_thread_tracker(&temp_dir, Some(0.5));
533 assert!(result2.is_ok(), "Should handle duplicate initialization");
534 }
535
536 #[test]
537 fn test_track_without_init() {
538 THREAD_TRACKER.with(|t| {
539 *t.borrow_mut() = None;
540 });
541
542 let result = track_allocation_lockfree(0x1000, 1024, 12345);
543 assert!(result.is_err(), "Should fail without initialization");
544 }
545
546 #[test]
547 fn test_finalize_without_init() {
548 THREAD_TRACKER.with(|t| {
549 *t.borrow_mut() = None;
550 });
551
552 let result = finalize_thread_tracker();
553 assert!(
554 result.is_ok(),
555 "Should handle finalization without initialization"
556 );
557 }
558
559 #[test]
560 fn test_global_api_workflow() {
561 let temp_dir = std::env::temp_dir().join("memscope_workflow_test");
562 std::fs::create_dir_all(&temp_dir).unwrap();
563
564 init_thread_tracker(&temp_dir, Some(1.0)).unwrap();
565
566 track_allocation_lockfree(0x1000, 1024, 12345).unwrap();
567 track_deallocation_lockfree(0x1000, 12345).unwrap();
568
569 let tracker = get_current_tracker();
570 assert!(tracker.is_some(), "Should have active tracker");
571
572 if let Some(t) = tracker {
573 let stats = t.get_stats();
574 assert_eq!(stats.total_allocations, 1);
575 assert_eq!(stats.total_deallocations, 1);
576 }
577
578 finalize_thread_tracker().unwrap();
579 }
580}