1use std::collections::HashMap;
2use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3
4#[derive(Debug, Clone)]
6pub struct StackFrame {
7 pub instruction_pointer: usize,
9 pub symbol_name: Option<String>,
11 pub filename: Option<String>,
13 pub line_number: Option<u32>,
15 pub function_name: Option<String>,
17}
18
19#[derive(Debug, Clone)]
21pub struct CaptureConfig {
22 pub max_depth: usize,
24 pub skip_frames: usize,
26 pub enable_symbols: bool,
28 pub cache_symbols: bool,
30 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
46pub struct StackTraceCapture {
48 config: CaptureConfig,
50 enabled: AtomicBool,
52 capture_count: AtomicUsize,
54 frame_cache: HashMap<usize, StackFrame>,
56}
57
58impl StackTraceCapture {
59 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 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 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(¤t_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 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 pub fn enable(&self) {
140 self.enabled.store(true, Ordering::Relaxed);
141 }
142
143 pub fn disable(&self) {
145 self.enabled.store(false, Ordering::Relaxed);
146 }
147
148 pub fn is_enabled(&self) -> bool {
150 self.enabled.load(Ordering::Relaxed)
151 }
152
153 pub fn get_capture_count(&self) -> usize {
155 self.capture_count.load(Ordering::Relaxed)
156 }
157
158 pub fn clear_cache(&mut self) {
160 self.frame_cache.clear();
161 }
162
163 pub fn cache_size(&self) -> usize {
165 self.frame_cache.len()
166 }
167
168 fn get_current_instruction_pointer(&self) -> usize {
169 0x7fff_0000_0000
172 }
173
174 fn walk_stack_frame(&self, current_ip: usize) -> Option<usize> {
175 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 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 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 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 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 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 for frame in &frames {
386 assert!(frame.function_name.is_none() || !frame.is_resolved());
387 }
388 }
389 }
390}