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 = if self.config.enable_symbols {
84                backtrace::Backtrace::new()
85            } else {
86                backtrace::Backtrace::new_unresolved()
87            };
88
89            for frame in bt.frames().iter().skip(self.config.skip_frames) {
90                if frame_count >= self.config.max_depth {
91                    break;
92                }
93
94                let ip = frame.ip() as usize;
95
96                let stack_frame = if let Some(cached_frame) = self.frame_cache.get(&ip) {
97                    cached_frame.clone()
98                } else {
99                    let new_frame = self.create_frame_from_backtrace(frame);
100                    if self.config.cache_symbols {
101                        self.frame_cache.insert(ip, new_frame.clone());
102                    }
103                    new_frame
104                };
105
106                if self.should_include_frame(&stack_frame) {
107                    frames.push(stack_frame);
108                    frame_count += 1;
109                }
110            }
111        }
112
113        #[cfg(not(feature = "backtrace"))]
114        {
115            let mut skip_count = 0;
116            let mut current_ip = self.get_current_instruction_pointer();
117
118            while frame_count < self.config.max_depth {
119                if skip_count < self.config.skip_frames {
120                    skip_count += 1;
121                    current_ip = self.walk_stack_frame(current_ip)?;
122                    continue;
123                }
124
125                let frame = if let Some(cached_frame) = self.frame_cache.get(&current_ip) {
126                    cached_frame.clone()
127                } else {
128                    let new_frame = self.create_frame(current_ip);
129                    if self.config.cache_symbols {
130                        self.frame_cache.insert(current_ip, new_frame.clone());
131                    }
132                    new_frame
133                };
134
135                if self.should_include_frame(&frame) {
136                    frames.push(frame);
137                    frame_count += 1;
138                }
139
140                current_ip = self.walk_stack_frame(current_ip)?;
141            }
142        }
143
144        Some(frames)
145    }
146
147    /// Capture lightweight stack trace (instruction pointers only)
148    /// Much faster than full capture, suitable for hot paths
149    pub fn capture_lightweight(&self) -> Option<Vec<usize>> {
150        if !self.enabled.load(Ordering::Relaxed) {
151            return None;
152        }
153
154        let mut instruction_pointers = Vec::with_capacity(self.config.max_depth);
155
156        #[cfg(feature = "backtrace")]
157        {
158            let bt = backtrace::Backtrace::new_unresolved();
159
160            for frame in bt.frames().iter().skip(self.config.skip_frames) {
161                if instruction_pointers.len() >= self.config.max_depth {
162                    break;
163                }
164                instruction_pointers.push(frame.ip() as usize);
165            }
166        }
167
168        #[cfg(not(feature = "backtrace"))]
169        {
170            let mut current_ip = self.get_current_instruction_pointer();
171            let mut skip_count = 0;
172
173            for _ in 0..self.config.max_depth {
174                if skip_count < self.config.skip_frames {
175                    skip_count += 1;
176                    current_ip = self.walk_stack_frame(current_ip)?;
177                    continue;
178                }
179
180                instruction_pointers.push(current_ip);
181                current_ip = self.walk_stack_frame(current_ip)?;
182            }
183        }
184
185        Some(instruction_pointers)
186    }
187
188    /// Enable stack trace capture
189    pub fn enable(&self) {
190        self.enabled.store(true, Ordering::Relaxed);
191    }
192
193    /// Disable stack trace capture for performance
194    pub fn disable(&self) {
195        self.enabled.store(false, Ordering::Relaxed);
196    }
197
198    /// Check if capture is currently enabled
199    pub fn is_enabled(&self) -> bool {
200        self.enabled.load(Ordering::Relaxed)
201    }
202
203    /// Get total number of captures performed
204    pub fn get_capture_count(&self) -> usize {
205        self.capture_count.load(Ordering::Relaxed)
206    }
207
208    /// Clear the symbol resolution cache
209    pub fn clear_cache(&mut self) {
210        self.frame_cache.clear();
211    }
212
213    /// Get current size of symbol cache
214    pub fn cache_size(&self) -> usize {
215        self.frame_cache.len()
216    }
217
218    #[cfg(target_os = "macos")]
219    #[cfg_attr(feature = "backtrace", allow(dead_code))]
220    fn get_current_instruction_pointer(&self) -> usize {
221        // Platform-specific implementation would use __builtin_return_address or similar
222        // Real stack trace is captured via backtrace crate in walk_stack
223        0
224    }
225
226    #[cfg(target_os = "linux")]
227    #[cfg_attr(feature = "backtrace", allow(dead_code))]
228    fn get_current_instruction_pointer(&self) -> usize {
229        // Platform-specific implementation would use libc::backtrace or similar
230        // Real stack trace is captured via backtrace crate in walk_stack
231        0
232    }
233
234    #[cfg(target_os = "windows")]
235    #[cfg_attr(feature = "backtrace", allow(dead_code))]
236    fn get_current_instruction_pointer(&self) -> usize {
237        // Platform-specific implementation would use _ReturnAddress or similar
238        // Real stack trace is captured via backtrace crate in walk_stack
239        0
240    }
241
242    #[cfg_attr(feature = "backtrace", allow(dead_code))]
243    fn walk_stack_frame(&self, _current_ip: usize) -> Option<usize> {
244        // Platform-specific stack walking implementation
245        // This feature is not yet implemented
246        None
247    }
248
249    #[cfg_attr(feature = "backtrace", allow(dead_code))]
250    fn create_frame(&self, ip: usize) -> StackFrame {
251        let mut frame = StackFrame {
252            instruction_pointer: ip,
253            symbol_name: None,
254            filename: None,
255            line_number: None,
256            function_name: None,
257        };
258
259        if self.config.enable_symbols {
260            // Symbol resolution would happen here
261            frame.function_name = self.resolve_function_name(ip);
262            frame.filename = self.resolve_filename(ip);
263            frame.line_number = self.resolve_line_number(ip);
264        }
265
266        frame
267    }
268
269    fn should_include_frame(&self, frame: &StackFrame) -> bool {
270        if !self.config.filter_system_frames {
271            return true;
272        }
273
274        // Filter out system/library frames
275        if let Some(filename) = &frame.filename {
276            if filename.contains("/usr/lib") || filename.contains("/lib64") {
277                return false;
278            }
279        }
280
281        if let Some(function_name) = &frame.function_name {
282            if function_name.starts_with("__libc_") || function_name.starts_with("_start") {
283                return false;
284            }
285        }
286
287        true
288    }
289
290    #[cfg_attr(feature = "backtrace", allow(dead_code))]
291    fn resolve_function_name(&self, _ip: usize) -> Option<String> {
292        // Real implementation would use debug symbols
293        // This feature is not yet implemented
294        None
295    }
296
297    #[cfg_attr(feature = "backtrace", allow(dead_code))]
298    fn resolve_filename(&self, _ip: usize) -> Option<String> {
299        // Real implementation would use debug symbols
300        // This feature is not yet implemented
301        None
302    }
303
304    #[cfg_attr(feature = "backtrace", allow(dead_code))]
305    fn resolve_line_number(&self, _ip: usize) -> Option<u32> {
306        // Real implementation would use debug symbols
307        // This feature is not yet implemented
308        None
309    }
310
311    #[cfg(feature = "backtrace")]
312    fn create_frame_from_backtrace(&self, frame: &backtrace::BacktraceFrame) -> StackFrame {
313        let ip = frame.ip() as usize;
314
315        let symbol_name = frame
316            .symbols()
317            .first()
318            .and_then(|sym| sym.name())
319            .map(|name| name.to_string());
320
321        let filename = frame
322            .symbols()
323            .first()
324            .and_then(|sym| sym.filename())
325            .map(|path| path.display().to_string());
326
327        let line_number = frame.symbols().first().and_then(|sym| sym.lineno());
328
329        let function_name = symbol_name.clone();
330
331        StackFrame {
332            instruction_pointer: ip,
333            symbol_name,
334            filename,
335            line_number,
336            function_name,
337        }
338    }
339}
340
341impl Default for StackTraceCapture {
342    fn default() -> Self {
343        Self::new(CaptureConfig::default())
344    }
345}
346
347impl StackFrame {
348    pub fn new(ip: usize) -> Self {
349        Self {
350            instruction_pointer: ip,
351            symbol_name: None,
352            filename: None,
353            line_number: None,
354            function_name: None,
355        }
356    }
357
358    pub fn with_symbols(
359        ip: usize,
360        function_name: Option<String>,
361        filename: Option<String>,
362        line_number: Option<u32>,
363    ) -> Self {
364        Self {
365            instruction_pointer: ip,
366            symbol_name: function_name.clone(),
367            filename,
368            line_number,
369            function_name,
370        }
371    }
372
373    pub fn is_resolved(&self) -> bool {
374        self.function_name.is_some() || self.filename.is_some()
375    }
376
377    pub fn display_name(&self) -> String {
378        if let Some(func) = &self.function_name {
379            if let (Some(file), Some(line)) = (&self.filename, self.line_number) {
380                format!("{}() at {}:{}", func, file, line)
381            } else {
382                format!("{}()", func)
383            }
384        } else {
385            format!("0x{:x}", self.instruction_pointer)
386        }
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_enable_disable() {
396        let capture = StackTraceCapture::default();
397
398        assert!(capture.is_enabled());
399
400        capture.disable();
401        assert!(!capture.is_enabled());
402
403        capture.enable();
404        assert!(capture.is_enabled());
405    }
406
407    #[test]
408    fn test_frame_creation() {
409        let frame = StackFrame::new(0x1234);
410        assert_eq!(frame.instruction_pointer, 0x1234);
411        assert!(!frame.is_resolved());
412
413        let resolved_frame = StackFrame::with_symbols(
414            0x5678,
415            Some("test_func".to_string()),
416            Some("test.rs".to_string()),
417            Some(42),
418        );
419        assert!(resolved_frame.is_resolved());
420        assert_eq!(resolved_frame.display_name(), "test_func() at test.rs:42");
421    }
422
423    #[test]
424    fn test_capture_count() {
425        let mut capture = StackTraceCapture::default();
426
427        assert_eq!(capture.get_capture_count(), 0);
428
429        capture.capture();
430        assert_eq!(capture.get_capture_count(), 1);
431
432        capture.capture();
433        assert_eq!(capture.get_capture_count(), 2);
434    }
435
436    /// Objective: Disabling symbols must keep raw frames without resolving metadata.
437    /// Invariants: the backtrace backend returns frames, respects depth, and leaves no cache.
438    #[test]
439    fn test_custom_config() {
440        let config = CaptureConfig {
441            max_depth: 10,
442            skip_frames: 1,
443            enable_symbols: false,
444            cache_symbols: false,
445            filter_system_frames: false,
446        };
447
448        let mut capture = StackTraceCapture::new(config);
449
450        for _ in 0..2 {
451            let frames = capture.capture();
452            #[cfg(feature = "backtrace")]
453            assert!(
454                frames.as_ref().is_some_and(|frames| !frames.is_empty()),
455                "the backtrace backend must still capture instruction pointers"
456            );
457            if let Some(frames) = frames {
458                assert!(frames.len() <= 10, "capture must respect max_depth");
459                for frame in &frames {
460                    assert!(
461                        frame.function_name.is_none()
462                            && frame.symbol_name.is_none()
463                            && frame.filename.is_none()
464                            && frame.line_number.is_none(),
465                        "symbols disabled: all symbol fields must remain absent"
466                    );
467                }
468            }
469        }
470        assert_eq!(
471            capture.cache_size(),
472            0,
473            "cache_symbols=false must not cache frames"
474        );
475    }
476}