Skip to main content

memscope_rs/capture/platform/
allocator.rs

1use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
2use std::time::Instant;
3
4/// Platform-specific allocator hooking system
5pub struct PlatformAllocator {
6    original_alloc: AtomicPtr<u8>,
7    original_dealloc: AtomicPtr<u8>,
8    config: HookConfig,
9    stats: HookStats,
10}
11
12impl PlatformAllocator {
13    pub fn original_alloc(&self) -> *mut u8 {
14        self.original_alloc.load(Ordering::SeqCst)
15    }
16    pub fn original_dealloc(&self) -> *mut u8 {
17        self.original_dealloc.load(Ordering::SeqCst)
18    }
19}
20
21/// Configuration for allocation hooks
22#[derive(Debug, Clone)]
23pub struct HookConfig {
24    /// Whether to track allocations
25    pub track_allocations: bool,
26    /// Whether to track deallocations
27    pub track_deallocations: bool,
28    /// Minimum allocation size to track
29    pub min_tracked_size: usize,
30    /// Maximum allocation size to track
31    pub max_tracked_size: usize,
32    /// Sample rate for tracking (0.0 to 1.0)
33    pub sample_rate: f64,
34}
35
36/// Statistics for allocation hooks
37#[derive(Debug)]
38struct HookStats {
39    /// Total allocations intercepted
40    total_allocations: AtomicUsize,
41    /// Total deallocations intercepted
42    total_deallocations: AtomicUsize,
43    /// Total bytes allocated
44    total_bytes_allocated: AtomicUsize,
45    /// Total bytes deallocated
46    total_bytes_deallocated: AtomicUsize,
47    /// Hook overhead time
48    total_hook_time: AtomicUsize,
49}
50
51/// Result of allocation hook
52#[derive(Debug, Clone)]
53pub struct HookResult {
54    /// Whether allocation should proceed
55    pub should_proceed: bool,
56    /// Whether to track this allocation
57    pub should_track: bool,
58    /// Additional metadata
59    pub metadata: Option<AllocationMetadata>,
60}
61
62/// One of several same-named `AllocationInfo` types in this crate: this one is the
63/// a platform allocator snapshot entry, not a tracked allocation.
64///
65/// Convert at the module boundary instead of passing it across; a global rename is
66/// deliberately avoided (see DATA_ACCURACY_DEVELOPMENT_PLAN ยง14.2).
67/// Information about an allocation
68#[derive(Debug, Clone)]
69pub struct AllocationInfo {
70    /// Pointer to allocated memory
71    pub ptr: *mut u8,
72    /// Size of allocation
73    pub size: usize,
74    /// Alignment requirement
75    pub align: usize,
76    /// Timestamp of allocation
77    pub timestamp: Instant,
78    /// Thread ID that made allocation
79    pub thread_id: ThreadId,
80    /// Stack trace if captured
81    pub stack_trace: Option<Vec<usize>>,
82}
83
84/// Metadata for allocation tracking
85#[derive(Debug, Clone)]
86pub struct AllocationMetadata {
87    /// Type name if known
88    pub type_name: Option<String>,
89    /// Source location if available
90    pub source_location: Option<SourceLocation>,
91    /// Custom tags
92    pub tags: Vec<String>,
93}
94
95/// Source code location information
96#[derive(Debug, Clone)]
97pub struct SourceLocation {
98    /// File name
99    pub file: String,
100    /// Line number
101    pub line: u32,
102    /// Column number
103    pub column: Option<u32>,
104}
105
106/// Thread identifier
107#[derive(Debug, Clone, PartialEq, Eq, Hash)]
108pub struct ThreadId(pub u64);
109
110/// Allocation hook function type
111pub type AllocationHook = fn(&AllocationInfo) -> HookResult;
112
113/// Deallocation hook function type
114pub type DeallocationHook = fn(*mut u8, usize) -> bool;
115
116impl PlatformAllocator {
117    /// Create new platform allocator
118    pub fn new() -> Self {
119        Self {
120            original_alloc: AtomicPtr::new(std::ptr::null_mut()),
121            original_dealloc: AtomicPtr::new(std::ptr::null_mut()),
122            config: HookConfig::default(),
123            stats: HookStats::new(),
124        }
125    }
126
127    /// Install allocation hooks
128    pub fn install_hooks(&mut self) -> Result<(), HookError> {
129        #[cfg(target_os = "linux")]
130        {
131            self.install_linux_hooks()
132        }
133
134        #[cfg(target_os = "windows")]
135        {
136            self.install_windows_hooks()
137        }
138
139        #[cfg(target_os = "macos")]
140        {
141            self.install_macos_hooks()
142        }
143
144        #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
145        {
146            Err(HookError::UnsupportedPlatform)
147        }
148    }
149
150    /// Remove allocation hooks
151    pub fn remove_hooks(&mut self) -> Result<(), HookError> {
152        #[cfg(target_os = "linux")]
153        {
154            self.remove_linux_hooks()
155        }
156
157        #[cfg(target_os = "windows")]
158        {
159            self.remove_windows_hooks()
160        }
161
162        #[cfg(target_os = "macos")]
163        {
164            self.remove_macos_hooks()
165        }
166
167        #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
168        {
169            Err(HookError::UnsupportedPlatform)
170        }
171    }
172
173    /// Get hook statistics
174    pub fn get_statistics(&self) -> AllocationStatistics {
175        AllocationStatistics {
176            total_allocations: self.stats.total_allocations.load(Ordering::Relaxed),
177            total_deallocations: self.stats.total_deallocations.load(Ordering::Relaxed),
178            total_bytes_allocated: self.stats.total_bytes_allocated.load(Ordering::Relaxed),
179            total_bytes_deallocated: self.stats.total_bytes_deallocated.load(Ordering::Relaxed),
180            current_allocations: self
181                .stats
182                .total_allocations
183                .load(Ordering::Relaxed)
184                .saturating_sub(self.stats.total_deallocations.load(Ordering::Relaxed)),
185            current_bytes: self
186                .stats
187                .total_bytes_allocated
188                .load(Ordering::Relaxed)
189                .saturating_sub(self.stats.total_bytes_deallocated.load(Ordering::Relaxed)),
190            average_hook_overhead: self.calculate_average_overhead(),
191        }
192    }
193
194    /// Update hook configuration
195    pub fn update_config(&mut self, config: HookConfig) {
196        self.config = config;
197    }
198
199    #[cfg(target_os = "linux")]
200    fn install_linux_hooks(&mut self) -> Result<(), HookError> {
201        // Linux-specific implementation using LD_PRELOAD or similar
202        //
203        // NOTE: This is a placeholder implementation. A full implementation would:
204        // 1. Use LD_PRELOAD to intercept malloc/free calls
205        // 2. Or use dlsym to hook into libc allocation functions
206        // 3. Register the GlobalAlloc implementation with the system
207        //
208        // Current implementation: No-op (hooks are already installed via GlobalAlloc trait)
209        // Future implementation: Add LD_PRELOAD support for external libraries
210        tracing::warn!("Linux hooks are not yet implemented. Using GlobalAlloc trait instead.");
211        Ok(())
212    }
213
214    #[cfg(target_os = "linux")]
215    fn remove_linux_hooks(&mut self) -> Result<(), HookError> {
216        // Linux-specific cleanup
217        // Restore original malloc/free if they were hooked
218        tracing::info!("Linux hooks cleanup: No action needed (using GlobalAlloc trait)");
219        Ok(())
220    }
221
222    #[cfg(target_os = "windows")]
223    fn install_windows_hooks(&mut self) -> Result<(), HookError> {
224        // Windows-specific implementation using detours or similar
225        //
226        // NOTE: This is a placeholder implementation. A full implementation would:
227        // 1. Use Detours library to hook HeapAlloc/HeapFree
228        // 2. Or use VirtualProtect to modify import address table
229        // 3. Register the GlobalAlloc implementation with the system
230        //
231        // Current implementation: No-op (hooks are already installed via GlobalAlloc trait)
232        // Future implementation: Add Detours support for external libraries
233        tracing::warn!("Windows hooks are not yet implemented. Using GlobalAlloc trait instead.");
234        Ok(())
235    }
236
237    #[cfg(target_os = "windows")]
238    fn remove_windows_hooks(&mut self) -> Result<(), HookError> {
239        // Windows-specific cleanup
240        // Restore original HeapAlloc/HeapFree if they were hooked
241        tracing::info!("Windows hooks cleanup: No action needed (using GlobalAlloc trait)");
242        Ok(())
243    }
244
245    #[cfg(target_os = "macos")]
246    fn install_macos_hooks(&mut self) -> Result<(), HookError> {
247        // macOS-specific implementation using interpose or similar
248        //
249        // NOTE: This is a placeholder implementation. A full implementation would:
250        // 1. Use DYLD_INSERT_LIBRARIES to intercept malloc/free
251        // 2. Or use interpose to hook into system allocation functions
252        // 3. Register the GlobalAlloc implementation with the system
253        //
254        // Current implementation: No-op (hooks are already installed via GlobalAlloc trait)
255        // Future implementation: Add DYLD_INSERT_LIBRARIES support for external libraries
256        tracing::warn!("macOS hooks are not yet implemented. Using GlobalAlloc trait instead.");
257        Ok(())
258    }
259
260    #[cfg(target_os = "macos")]
261    fn remove_macos_hooks(&mut self) -> Result<(), HookError> {
262        // macOS-specific cleanup
263        // Restore original malloc/free if they were hooked
264        tracing::info!("macOS hooks cleanup: No action needed (using GlobalAlloc trait)");
265        Ok(())
266    }
267
268    fn calculate_average_overhead(&self) -> f64 {
269        let total_time = self.stats.total_hook_time.load(Ordering::Relaxed);
270        let total_calls = self.stats.total_allocations.load(Ordering::Relaxed)
271            + self.stats.total_deallocations.load(Ordering::Relaxed);
272
273        if total_calls > 0 {
274            total_time as f64 / total_calls as f64
275        } else {
276            0.0
277        }
278    }
279
280    /// Handle allocation event
281    pub fn handle_allocation(&self, info: &AllocationInfo) -> HookResult {
282        let start_time = Instant::now();
283
284        // Update statistics
285        self.stats.total_allocations.fetch_add(1, Ordering::Relaxed);
286        self.stats
287            .total_bytes_allocated
288            .fetch_add(info.size, Ordering::Relaxed);
289
290        // Check if we should track this allocation
291        let should_track = self.should_track_allocation(info);
292
293        // Record hook overhead
294        let overhead = start_time.elapsed().as_nanos() as usize;
295        self.stats
296            .total_hook_time
297            .fetch_add(overhead, Ordering::Relaxed);
298
299        HookResult {
300            should_proceed: true,
301            should_track,
302            metadata: self.extract_metadata(info),
303        }
304    }
305
306    /// Handle deallocation event
307    pub fn handle_deallocation(&self, _ptr: *mut u8, size: usize) -> bool {
308        let start_time = Instant::now();
309
310        // Update statistics
311        self.stats
312            .total_deallocations
313            .fetch_add(1, Ordering::Relaxed);
314        self.stats
315            .total_bytes_deallocated
316            .fetch_add(size, Ordering::Relaxed);
317
318        // Record hook overhead
319        let overhead = start_time.elapsed().as_nanos() as usize;
320        self.stats
321            .total_hook_time
322            .fetch_add(overhead, Ordering::Relaxed);
323
324        true
325    }
326
327    fn should_track_allocation(&self, info: &AllocationInfo) -> bool {
328        // Check size limits
329        if info.size < self.config.min_tracked_size || info.size > self.config.max_tracked_size {
330            return false;
331        }
332
333        // Apply sampling with random decision to avoid bias
334        if self.config.sample_rate < 1.0 {
335            let sample_decision = rand::random::<f64>();
336            if sample_decision >= self.config.sample_rate {
337                return false;
338            }
339        }
340
341        true
342    }
343
344    fn extract_metadata(&self, _info: &AllocationInfo) -> Option<AllocationMetadata> {
345        // Extract metadata from allocation context
346        // Real implementation would use debug info, compiler hints, or source location tracking
347        // This feature is not yet implemented
348        None
349    }
350}
351
352impl HookStats {
353    fn new() -> Self {
354        Self {
355            total_allocations: AtomicUsize::new(0),
356            total_deallocations: AtomicUsize::new(0),
357            total_bytes_allocated: AtomicUsize::new(0),
358            total_bytes_deallocated: AtomicUsize::new(0),
359            total_hook_time: AtomicUsize::new(0),
360        }
361    }
362}
363
364/// Statistics about allocation hooks
365#[derive(Debug, Clone)]
366pub struct AllocationStatistics {
367    /// Total number of allocations
368    pub total_allocations: usize,
369    /// Total number of deallocations
370    pub total_deallocations: usize,
371    /// Total bytes allocated
372    pub total_bytes_allocated: usize,
373    /// Total bytes deallocated
374    pub total_bytes_deallocated: usize,
375    /// Current active allocations
376    pub current_allocations: usize,
377    /// Current active bytes
378    pub current_bytes: usize,
379    /// Average hook overhead in nanoseconds
380    pub average_hook_overhead: f64,
381}
382
383/// Errors that can occur during hook installation
384#[derive(Debug, Clone, PartialEq)]
385pub enum HookError {
386    /// Platform not supported
387    UnsupportedPlatform,
388    /// Permission denied
389    PermissionDenied,
390    /// Hook already installed
391    AlreadyInstalled,
392    /// Hook not installed
393    NotInstalled,
394    /// System error
395    SystemError(String),
396}
397
398impl Default for HookConfig {
399    fn default() -> Self {
400        Self {
401            track_allocations: true,
402            track_deallocations: true,
403            min_tracked_size: 1,
404            max_tracked_size: usize::MAX,
405            sample_rate: 1.0,
406        }
407    }
408}
409
410impl Default for PlatformAllocator {
411    fn default() -> Self {
412        Self::new()
413    }
414}
415
416impl std::fmt::Display for HookError {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        match self {
419            HookError::UnsupportedPlatform => {
420                write!(f, "Platform not supported for allocation hooking")
421            }
422            HookError::PermissionDenied => write!(f, "Permission denied for hook installation"),
423            HookError::AlreadyInstalled => write!(f, "Allocation hooks already installed"),
424            HookError::NotInstalled => write!(f, "Allocation hooks not installed"),
425            HookError::SystemError(msg) => write!(f, "System error: {}", msg),
426        }
427    }
428}
429
430impl std::error::Error for HookError {}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn test_platform_allocator_creation() {
438        let allocator = PlatformAllocator::new();
439        let stats = allocator.get_statistics();
440
441        assert_eq!(stats.total_allocations, 0);
442        assert_eq!(stats.total_deallocations, 0);
443        assert_eq!(stats.current_allocations, 0);
444    }
445
446    #[test]
447    fn test_hook_config() {
448        let config = HookConfig::default();
449        assert!(config.track_allocations);
450        assert!(config.track_deallocations);
451        assert_eq!(config.min_tracked_size, 1);
452        assert_eq!(config.sample_rate, 1.0);
453    }
454
455    #[test]
456    fn test_allocation_info() {
457        let info = AllocationInfo {
458            ptr: std::ptr::null_mut(),
459            size: 1024,
460            align: 8,
461            timestamp: Instant::now(),
462            thread_id: ThreadId(1),
463            stack_trace: None,
464        };
465
466        assert_eq!(info.size, 1024);
467        assert_eq!(info.align, 8);
468    }
469
470    #[test]
471    fn test_hook_statistics() {
472        let allocator = PlatformAllocator::new();
473
474        let info = AllocationInfo {
475            ptr: 0x1000 as *mut u8,
476            size: 100,
477            align: 8,
478            timestamp: Instant::now(),
479            thread_id: ThreadId(1),
480            stack_trace: None,
481        };
482
483        let result = allocator.handle_allocation(&info);
484        assert!(result.should_proceed);
485
486        let stats = allocator.get_statistics();
487        assert_eq!(stats.total_allocations, 1);
488        assert_eq!(stats.total_bytes_allocated, 100);
489    }
490
491    #[test]
492    fn test_sample_rate_filtering() {
493        let mut allocator = PlatformAllocator::new();
494        allocator.config.sample_rate = 0.5;
495
496        // Test multiple allocations to check sampling
497        let mut tracked_count = 0;
498        for i in 0..1000 {
499            let info = AllocationInfo {
500                ptr: (0x1000 + i) as *mut u8,
501                size: 64,
502                align: 8,
503                timestamp: Instant::now(),
504                thread_id: ThreadId(1),
505                stack_trace: None,
506            };
507
508            if allocator.should_track_allocation(&info) {
509                tracked_count += 1;
510            }
511        }
512
513        // Should track roughly 50% with some variance
514        assert!(tracked_count > 400 && tracked_count < 600);
515    }
516}