memscope_rs/metadata/stack_trace/
capture.rs1use 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
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(¤t_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 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 pub fn enable(&self) {
190 self.enabled.store(true, Ordering::Relaxed);
191 }
192
193 pub fn disable(&self) {
195 self.enabled.store(false, Ordering::Relaxed);
196 }
197
198 pub fn is_enabled(&self) -> bool {
200 self.enabled.load(Ordering::Relaxed)
201 }
202
203 pub fn get_capture_count(&self) -> usize {
205 self.capture_count.load(Ordering::Relaxed)
206 }
207
208 pub fn clear_cache(&mut self) {
210 self.frame_cache.clear();
211 }
212
213 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 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 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 0
240 }
241
242 #[cfg_attr(feature = "backtrace", allow(dead_code))]
243 fn walk_stack_frame(&self, _current_ip: usize) -> Option<usize> {
244 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 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 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 None
295 }
296
297 #[cfg_attr(feature = "backtrace", allow(dead_code))]
298 fn resolve_filename(&self, _ip: usize) -> Option<String> {
299 None
302 }
303
304 #[cfg_attr(feature = "backtrace", allow(dead_code))]
305 fn resolve_line_number(&self, _ip: usize) -> Option<u32> {
306 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 #[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}