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 = 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(¤t_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 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 pub fn enable(&self) {
186 self.enabled.store(true, Ordering::Relaxed);
187 }
188
189 pub fn disable(&self) {
191 self.enabled.store(false, Ordering::Relaxed);
192 }
193
194 pub fn is_enabled(&self) -> bool {
196 self.enabled.load(Ordering::Relaxed)
197 }
198
199 pub fn get_capture_count(&self) -> usize {
201 self.capture_count.load(Ordering::Relaxed)
202 }
203
204 pub fn clear_cache(&mut self) {
206 self.frame_cache.clear();
207 }
208
209 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 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 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 0
236 }
237
238 #[cfg_attr(feature = "backtrace", allow(dead_code))]
239 fn walk_stack_frame(&self, _current_ip: usize) -> Option<usize> {
240 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 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 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 None
291 }
292
293 #[cfg_attr(feature = "backtrace", allow(dead_code))]
294 fn resolve_filename(&self, _ip: usize) -> Option<String> {
295 None
298 }
299
300 #[cfg_attr(feature = "backtrace", allow(dead_code))]
301 fn resolve_line_number(&self, _ip: usize) -> Option<u32> {
302 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 for frame in &frames {
448 assert!(frame.function_name.is_none() || !frame.is_resolved());
449 }
450 }
451 }
452}