Skip to main content

memscope_rs/metadata/stack_trace/
capture.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3
4/// Represents a single frame in a stack trace
5#[derive(Debug, Clone)]
6pub struct StackFrame {
7    /// Raw instruction pointer address
8    pub instruction_pointer: usize,
9    /// Symbol name if resolved
10    pub symbol_name: Option<String>,
11    /// Source filename if available
12    pub filename: Option<String>,
13    /// Line number in source file
14    pub line_number: Option<u32>,
15    /// Function or method name
16    pub function_name: Option<String>,
17}
18
19/// Configuration for stack trace capture behavior
20#[derive(Debug, Clone)]
21pub struct CaptureConfig {
22    /// Maximum number of frames to capture
23    pub max_depth: usize,
24    /// Number of top frames to skip (e.g., allocator internals)
25    pub skip_frames: usize,
26    /// Whether to resolve symbol information
27    pub enable_symbols: bool,
28    /// Whether to cache resolved symbols for performance
29    pub cache_symbols: bool,
30    /// Whether to filter out system/library frames
31    pub filter_system_frames: bool,
32}
33
34impl Default for CaptureConfig {
35    fn default() -> Self {
36        Self {
37            max_depth: 32,
38            skip_frames: 2,
39            enable_symbols: true,
40            cache_symbols: true,
41            filter_system_frames: true,
42        }
43    }
44}
45
46/// High-performance stack trace capture engine
47pub struct StackTraceCapture {
48    /// Capture configuration settings
49    config: CaptureConfig,
50    /// Whether capture is currently enabled
51    enabled: AtomicBool,
52    /// Total number of captures performed
53    capture_count: AtomicUsize,
54    /// Cache of resolved stack frames for performance
55    frame_cache: HashMap<usize, StackFrame>,
56}
57
58impl StackTraceCapture {
59    /// Create new stack trace capture instance with given configuration
60    pub fn new(config: CaptureConfig) -> Self {
61        Self {
62            config,
63            enabled: AtomicBool::new(true),
64            capture_count: AtomicUsize::new(0),
65            frame_cache: HashMap::new(),
66        }
67    }
68
69    /// Capture full stack trace with symbol resolution
70    /// Returns None if capture is disabled
71    pub fn capture(&mut self) -> Option<Vec<StackFrame>> {
72        if !self.enabled.load(Ordering::Relaxed) {
73            return None;
74        }
75
76        self.capture_count.fetch_add(1, Ordering::Relaxed);
77
78        let mut frames = Vec::with_capacity(self.config.max_depth);
79        let mut frame_count = 0;
80
81        #[cfg(feature = "backtrace")]
82        {
83            let bt = backtrace::Backtrace::new();
84
85            for frame in bt.frames().iter().skip(self.config.skip_frames) {
86                if frame_count >= self.config.max_depth {
87                    break;
88                }
89
90                let ip = frame.ip() as usize;
91
92                let stack_frame = if let Some(cached_frame) = self.frame_cache.get(&ip) {
93                    cached_frame.clone()
94                } else {
95                    let new_frame = self.create_frame_from_backtrace(frame);
96                    if self.config.cache_symbols {
97                        self.frame_cache.insert(ip, new_frame.clone());
98                    }
99                    new_frame
100                };
101
102                if self.should_include_frame(&stack_frame) {
103                    frames.push(stack_frame);
104                    frame_count += 1;
105                }
106            }
107        }
108
109        #[cfg(not(feature = "backtrace"))]
110        {
111            let mut skip_count = 0;
112            let mut current_ip = self.get_current_instruction_pointer();
113
114            while frame_count < self.config.max_depth {
115                if skip_count < self.config.skip_frames {
116                    skip_count += 1;
117                    current_ip = self.walk_stack_frame(current_ip)?;
118                    continue;
119                }
120
121                let frame = if let Some(cached_frame) = self.frame_cache.get(&current_ip) {
122                    cached_frame.clone()
123                } else {
124                    let new_frame = self.create_frame(current_ip);
125                    if self.config.cache_symbols {
126                        self.frame_cache.insert(current_ip, new_frame.clone());
127                    }
128                    new_frame
129                };
130
131                if self.should_include_frame(&frame) {
132                    frames.push(frame);
133                    frame_count += 1;
134                }
135
136                current_ip = self.walk_stack_frame(current_ip)?;
137            }
138        }
139
140        Some(frames)
141    }
142
143    /// Capture lightweight stack trace (instruction pointers only)
144    /// Much faster than full capture, suitable for hot paths
145    pub fn capture_lightweight(&self) -> Option<Vec<usize>> {
146        if !self.enabled.load(Ordering::Relaxed) {
147            return None;
148        }
149
150        let mut instruction_pointers = Vec::with_capacity(self.config.max_depth);
151
152        #[cfg(feature = "backtrace")]
153        {
154            let bt = backtrace::Backtrace::new();
155
156            for frame in bt.frames().iter().skip(self.config.skip_frames) {
157                if instruction_pointers.len() >= self.config.max_depth {
158                    break;
159                }
160                instruction_pointers.push(frame.ip() as usize);
161            }
162        }
163
164        #[cfg(not(feature = "backtrace"))]
165        {
166            let mut current_ip = self.get_current_instruction_pointer();
167            let mut skip_count = 0;
168
169            for _ in 0..self.config.max_depth {
170                if skip_count < self.config.skip_frames {
171                    skip_count += 1;
172                    current_ip = self.walk_stack_frame(current_ip)?;
173                    continue;
174                }
175
176                instruction_pointers.push(current_ip);
177                current_ip = self.walk_stack_frame(current_ip)?;
178            }
179        }
180
181        Some(instruction_pointers)
182    }
183
184    /// Enable stack trace capture
185    pub fn enable(&self) {
186        self.enabled.store(true, Ordering::Relaxed);
187    }
188
189    /// Disable stack trace capture for performance
190    pub fn disable(&self) {
191        self.enabled.store(false, Ordering::Relaxed);
192    }
193
194    /// Check if capture is currently enabled
195    pub fn is_enabled(&self) -> bool {
196        self.enabled.load(Ordering::Relaxed)
197    }
198
199    /// Get total number of captures performed
200    pub fn get_capture_count(&self) -> usize {
201        self.capture_count.load(Ordering::Relaxed)
202    }
203
204    /// Clear the symbol resolution cache
205    pub fn clear_cache(&mut self) {
206        self.frame_cache.clear();
207    }
208
209    /// Get current size of symbol cache
210    pub fn cache_size(&self) -> usize {
211        self.frame_cache.len()
212    }
213
214    #[cfg(target_os = "macos")]
215    #[cfg_attr(feature = "backtrace", allow(dead_code))]
216    fn get_current_instruction_pointer(&self) -> usize {
217        // Platform-specific implementation would use __builtin_return_address or similar
218        // Real stack trace is captured via backtrace crate in walk_stack
219        0
220    }
221
222    #[cfg(target_os = "linux")]
223    #[cfg_attr(feature = "backtrace", allow(dead_code))]
224    fn get_current_instruction_pointer(&self) -> usize {
225        // Platform-specific implementation would use libc::backtrace or similar
226        // Real stack trace is captured via backtrace crate in walk_stack
227        0
228    }
229
230    #[cfg(target_os = "windows")]
231    #[cfg_attr(feature = "backtrace", allow(dead_code))]
232    fn get_current_instruction_pointer(&self) -> usize {
233        // Platform-specific implementation would use _ReturnAddress or similar
234        // Real stack trace is captured via backtrace crate in walk_stack
235        0
236    }
237
238    #[cfg_attr(feature = "backtrace", allow(dead_code))]
239    fn walk_stack_frame(&self, _current_ip: usize) -> Option<usize> {
240        // Platform-specific stack walking implementation
241        // This feature is not yet implemented
242        None
243    }
244
245    #[cfg_attr(feature = "backtrace", allow(dead_code))]
246    fn create_frame(&self, ip: usize) -> StackFrame {
247        let mut frame = StackFrame {
248            instruction_pointer: ip,
249            symbol_name: None,
250            filename: None,
251            line_number: None,
252            function_name: None,
253        };
254
255        if self.config.enable_symbols {
256            // Symbol resolution would happen here
257            frame.function_name = self.resolve_function_name(ip);
258            frame.filename = self.resolve_filename(ip);
259            frame.line_number = self.resolve_line_number(ip);
260        }
261
262        frame
263    }
264
265    fn should_include_frame(&self, frame: &StackFrame) -> bool {
266        if !self.config.filter_system_frames {
267            return true;
268        }
269
270        // Filter out system/library frames
271        if let Some(filename) = &frame.filename {
272            if filename.contains("/usr/lib") || filename.contains("/lib64") {
273                return false;
274            }
275        }
276
277        if let Some(function_name) = &frame.function_name {
278            if function_name.starts_with("__libc_") || function_name.starts_with("_start") {
279                return false;
280            }
281        }
282
283        true
284    }
285
286    #[cfg_attr(feature = "backtrace", allow(dead_code))]
287    fn resolve_function_name(&self, _ip: usize) -> Option<String> {
288        // Real implementation would use debug symbols
289        // This feature is not yet implemented
290        None
291    }
292
293    #[cfg_attr(feature = "backtrace", allow(dead_code))]
294    fn resolve_filename(&self, _ip: usize) -> Option<String> {
295        // Real implementation would use debug symbols
296        // This feature is not yet implemented
297        None
298    }
299
300    #[cfg_attr(feature = "backtrace", allow(dead_code))]
301    fn resolve_line_number(&self, _ip: usize) -> Option<u32> {
302        // Real implementation would use debug symbols
303        // This feature is not yet implemented
304        None
305    }
306
307    #[cfg(feature = "backtrace")]
308    fn create_frame_from_backtrace(&self, frame: &backtrace::BacktraceFrame) -> StackFrame {
309        let ip = frame.ip() as usize;
310
311        let symbol_name = frame
312            .symbols()
313            .first()
314            .and_then(|sym| sym.name())
315            .map(|name| name.to_string());
316
317        let filename = frame
318            .symbols()
319            .first()
320            .and_then(|sym| sym.filename())
321            .map(|path| path.display().to_string());
322
323        let line_number = frame.symbols().first().and_then(|sym| sym.lineno());
324
325        let function_name = symbol_name.clone();
326
327        StackFrame {
328            instruction_pointer: ip,
329            symbol_name,
330            filename,
331            line_number,
332            function_name,
333        }
334    }
335}
336
337impl Default for StackTraceCapture {
338    fn default() -> Self {
339        Self::new(CaptureConfig::default())
340    }
341}
342
343impl StackFrame {
344    pub fn new(ip: usize) -> Self {
345        Self {
346            instruction_pointer: ip,
347            symbol_name: None,
348            filename: None,
349            line_number: None,
350            function_name: None,
351        }
352    }
353
354    pub fn with_symbols(
355        ip: usize,
356        function_name: Option<String>,
357        filename: Option<String>,
358        line_number: Option<u32>,
359    ) -> Self {
360        Self {
361            instruction_pointer: ip,
362            symbol_name: function_name.clone(),
363            filename,
364            line_number,
365            function_name,
366        }
367    }
368
369    pub fn is_resolved(&self) -> bool {
370        self.function_name.is_some() || self.filename.is_some()
371    }
372
373    pub fn display_name(&self) -> String {
374        if let Some(func) = &self.function_name {
375            if let (Some(file), Some(line)) = (&self.filename, self.line_number) {
376                format!("{}() at {}:{}", func, file, line)
377            } else {
378                format!("{}()", func)
379            }
380        } else {
381            format!("0x{:x}", self.instruction_pointer)
382        }
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_enable_disable() {
392        let capture = StackTraceCapture::default();
393
394        assert!(capture.is_enabled());
395
396        capture.disable();
397        assert!(!capture.is_enabled());
398
399        capture.enable();
400        assert!(capture.is_enabled());
401    }
402
403    #[test]
404    fn test_frame_creation() {
405        let frame = StackFrame::new(0x1234);
406        assert_eq!(frame.instruction_pointer, 0x1234);
407        assert!(!frame.is_resolved());
408
409        let resolved_frame = StackFrame::with_symbols(
410            0x5678,
411            Some("test_func".to_string()),
412            Some("test.rs".to_string()),
413            Some(42),
414        );
415        assert!(resolved_frame.is_resolved());
416        assert_eq!(resolved_frame.display_name(), "test_func() at test.rs:42");
417    }
418
419    #[test]
420    fn test_capture_count() {
421        let mut capture = StackTraceCapture::default();
422
423        assert_eq!(capture.get_capture_count(), 0);
424
425        capture.capture();
426        assert_eq!(capture.get_capture_count(), 1);
427
428        capture.capture();
429        assert_eq!(capture.get_capture_count(), 2);
430    }
431
432    #[test]
433    fn test_custom_config() {
434        let config = CaptureConfig {
435            max_depth: 10,
436            skip_frames: 1,
437            enable_symbols: false,
438            cache_symbols: false,
439            filter_system_frames: false,
440        };
441
442        let mut capture = StackTraceCapture::new(config);
443
444        if let Some(frames) = capture.capture() {
445            assert!(frames.len() <= 10);
446            // With symbols disabled, frames should not be resolved
447            for frame in &frames {
448                assert!(frame.function_name.is_none() || !frame.is_resolved());
449            }
450        }
451    }
452}