Skip to main content

memscope_rs/capture/backends/
lockfree_tracker.rs

1//! Lockfree memory tracker implementation.
2//!
3//! This module contains the ThreadLocalTracker for thread-local memory tracking
4//! using lock-free data structures for optimal concurrent performance.
5
6use 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
25/// Thread-local memory tracker using lock-free data structures.
26///
27/// This tracker uses SegQueue for events and DashMap for active allocations,
28/// ensuring no event loss under high contention.
29pub 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        // CAS loop with exponential backoff to prevent CPU waste under high contention
88        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                    // Exponential backoff strategy
105                    if backoff_count < MAX_BACKOFF_ATTEMPTS {
106                        // Short-term contention: use spin_loop for efficiency
107                        std::hint::spin_loop();
108                    } else if backoff_count < MAX_BACKOFF_ATTEMPTS * 2 {
109                        // Medium-term contention: yield to other threads
110                        std::thread::yield_now();
111                    } else {
112                        // Long-term contention: use small sleep to reduce CPU usage
113                        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        // Use an atomic compare-and-swap loop for saturating_sub to prevent underflow.
135        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
369/// Get a snapshot of the current thread's tracker.
370///
371/// # Warning
372/// The returned tracker shares `events` and `active_allocations` with the original.
373/// **Do NOT call `finalize()` on the returned tracker** - it will drain shared data
374/// and corrupt the original tracker. Use this only for read operations.
375///
376/// # Returns
377/// - `Some(ThreadLocalTracker)` if a tracker is initialized
378/// - `None` if `init_thread_tracker()` was not called
379pub 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}