memscope_rs/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        let mut skip_count = 0;
81
82        // Simulate stack walking (simplified implementation)
83        let mut current_ip = self.get_current_instruction_pointer();
84
85        while frame_count < self.config.max_depth {
86            if skip_count < self.config.skip_frames {
87                skip_count += 1;
88                current_ip = self.walk_stack_frame(current_ip)?;
89                continue;
90            }
91
92            let frame = if let Some(cached_frame) = self.frame_cache.get(&current_ip) {
93                cached_frame.clone()
94            } else {
95                let new_frame = self.create_frame(current_ip);
96                if self.config.cache_symbols {
97                    self.frame_cache.insert(current_ip, new_frame.clone());
98                }
99                new_frame
100            };
101
102            if self.should_include_frame(&frame) {
103                frames.push(frame);
104                frame_count += 1;
105            }
106
107            current_ip = self.walk_stack_frame(current_ip)?;
108        }
109
110        Some(frames)
111    }
112
113    /// Capture lightweight stack trace (instruction pointers only)
114    /// Much faster than full capture, suitable for hot paths
115    pub fn capture_lightweight(&self) -> Option<Vec<usize>> {
116        if !self.enabled.load(Ordering::Relaxed) {
117            return None;
118        }
119
120        let mut instruction_pointers = Vec::with_capacity(self.config.max_depth);
121        let mut current_ip = self.get_current_instruction_pointer();
122        let mut skip_count = 0;
123
124        for _ in 0..self.config.max_depth {
125            if skip_count < self.config.skip_frames {
126                skip_count += 1;
127                current_ip = self.walk_stack_frame(current_ip)?;
128                continue;
129            }
130
131            instruction_pointers.push(current_ip);
132            current_ip = self.walk_stack_frame(current_ip)?;
133        }
134
135        Some(instruction_pointers)
136    }
137
138    /// Enable stack trace capture
139    pub fn enable(&self) {
140        self.enabled.store(true, Ordering::Relaxed);
141    }
142
143    /// Disable stack trace capture for performance
144    pub fn disable(&self) {
145        self.enabled.store(false, Ordering::Relaxed);
146    }
147
148    /// Check if capture is currently enabled
149    pub fn is_enabled(&self) -> bool {
150        self.enabled.load(Ordering::Relaxed)
151    }
152
153    /// Get total number of captures performed
154    pub fn get_capture_count(&self) -> usize {
155        self.capture_count.load(Ordering::Relaxed)
156    }
157
158    /// Clear the symbol resolution cache
159    pub fn clear_cache(&mut self) {
160        self.frame_cache.clear();
161    }
162
163    /// Get current size of symbol cache
164    pub fn cache_size(&self) -> usize {
165        self.frame_cache.len()
166    }
167
168    fn get_current_instruction_pointer(&self) -> usize {
169        // Platform-specific implementation would go here
170        // For now, return a mock value
171        0x7fff_0000_0000
172    }
173
174    fn walk_stack_frame(&self, current_ip: usize) -> Option<usize> {
175        // Platform-specific stack walking implementation
176        // For now, simulate by decrementing
177        if current_ip > 0x1000_0000 {
178            Some(current_ip - 0x1000)
179        } else {
180            None
181        }
182    }
183
184    fn create_frame(&self, ip: usize) -> StackFrame {
185        let mut frame = StackFrame {
186            instruction_pointer: ip,
187            symbol_name: None,
188            filename: None,
189            line_number: None,
190            function_name: None,
191        };
192
193        if self.config.enable_symbols {
194            // Symbol resolution would happen here
195            frame.function_name = self.resolve_function_name(ip);
196            frame.filename = self.resolve_filename(ip);
197            frame.line_number = self.resolve_line_number(ip);
198        }
199
200        frame
201    }
202
203    fn should_include_frame(&self, frame: &StackFrame) -> bool {
204        if !self.config.filter_system_frames {
205            return true;
206        }
207
208        // Filter out system/library frames
209        if let Some(filename) = &frame.filename {
210            if filename.contains("/usr/lib") || filename.contains("/lib64") {
211                return false;
212            }
213        }
214
215        if let Some(function_name) = &frame.function_name {
216            if function_name.starts_with("__libc_") || function_name.starts_with("_start") {
217                return false;
218            }
219        }
220
221        true
222    }
223
224    fn resolve_function_name(&self, ip: usize) -> Option<String> {
225        // Mock implementation - real version would use debug symbols
226        match ip % 5 {
227            0 => Some("main".to_string()),
228            1 => Some("allocation_function".to_string()),
229            2 => Some("process_data".to_string()),
230            3 => Some("handle_request".to_string()),
231            _ => Some(format!("function_{:x}", ip)),
232        }
233    }
234
235    fn resolve_filename(&self, ip: usize) -> Option<String> {
236        // Mock implementation
237        match ip % 3 {
238            0 => Some("src/main.rs".to_string()),
239            1 => Some("src/lib.rs".to_string()),
240            _ => Some("src/utils.rs".to_string()),
241        }
242    }
243
244    fn resolve_line_number(&self, ip: usize) -> Option<u32> {
245        // Mock implementation
246        Some((ip % 1000) as u32 + 1)
247    }
248}
249
250impl Default for StackTraceCapture {
251    fn default() -> Self {
252        Self::new(CaptureConfig::default())
253    }
254}
255
256impl StackFrame {
257    pub fn new(ip: usize) -> Self {
258        Self {
259            instruction_pointer: ip,
260            symbol_name: None,
261            filename: None,
262            line_number: None,
263            function_name: None,
264        }
265    }
266
267    pub fn with_symbols(
268        ip: usize,
269        function_name: Option<String>,
270        filename: Option<String>,
271        line_number: Option<u32>,
272    ) -> Self {
273        Self {
274            instruction_pointer: ip,
275            symbol_name: function_name.clone(),
276            filename,
277            line_number,
278            function_name,
279        }
280    }
281
282    pub fn is_resolved(&self) -> bool {
283        self.function_name.is_some() || self.filename.is_some()
284    }
285
286    pub fn display_name(&self) -> String {
287        if let Some(func) = &self.function_name {
288            if let (Some(file), Some(line)) = (&self.filename, self.line_number) {
289                format!("{}() at {}:{}", func, file, line)
290            } else {
291                format!("{}()", func)
292            }
293        } else {
294            format!("0x{:x}", self.instruction_pointer)
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_basic_capture() {
305        let mut capture = StackTraceCapture::default();
306
307        assert!(capture.is_enabled());
308
309        let frames = capture.capture();
310        assert!(frames.is_some());
311
312        let frames = frames.expect("Should have frames");
313        assert!(!frames.is_empty());
314        assert!(frames.len() <= 32);
315    }
316
317    #[test]
318    fn test_lightweight_capture() {
319        let capture = StackTraceCapture::default();
320
321        let ips = capture.capture_lightweight();
322        assert!(ips.is_some());
323
324        let ips = ips.expect("Should have IPs");
325        assert!(!ips.is_empty());
326    }
327
328    #[test]
329    fn test_enable_disable() {
330        let capture = StackTraceCapture::default();
331
332        assert!(capture.is_enabled());
333
334        capture.disable();
335        assert!(!capture.is_enabled());
336
337        capture.enable();
338        assert!(capture.is_enabled());
339    }
340
341    #[test]
342    fn test_frame_creation() {
343        let frame = StackFrame::new(0x1234);
344        assert_eq!(frame.instruction_pointer, 0x1234);
345        assert!(!frame.is_resolved());
346
347        let resolved_frame = StackFrame::with_symbols(
348            0x5678,
349            Some("test_func".to_string()),
350            Some("test.rs".to_string()),
351            Some(42),
352        );
353        assert!(resolved_frame.is_resolved());
354        assert_eq!(resolved_frame.display_name(), "test_func() at test.rs:42");
355    }
356
357    #[test]
358    fn test_capture_count() {
359        let mut capture = StackTraceCapture::default();
360
361        assert_eq!(capture.get_capture_count(), 0);
362
363        capture.capture();
364        assert_eq!(capture.get_capture_count(), 1);
365
366        capture.capture();
367        assert_eq!(capture.get_capture_count(), 2);
368    }
369
370    #[test]
371    fn test_custom_config() {
372        let config = CaptureConfig {
373            max_depth: 10,
374            skip_frames: 1,
375            enable_symbols: false,
376            cache_symbols: false,
377            filter_system_frames: false,
378        };
379
380        let mut capture = StackTraceCapture::new(config);
381
382        if let Some(frames) = capture.capture() {
383            assert!(frames.len() <= 10);
384            // With symbols disabled, frames should not be resolved
385            for frame in &frames {
386                assert!(frame.function_name.is_none() || !frame.is_resolved());
387            }
388        }
389    }
390}