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