Skip to main content

memscope_rs/metadata/stack_trace/
resolver.rs

1use super::StackFrame;
2use std::collections::HashMap;
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5/// Fully resolved stack frame with symbol information
6#[derive(Debug, Clone)]
7pub struct ResolvedFrame {
8    /// Original instruction pointer address
9    pub instruction_pointer: usize,
10    /// Raw symbol name from debug info
11    pub symbol_name: String,
12    /// Human-readable demangled symbol name
13    pub demangled_name: Option<String>,
14    /// Source code filename
15    pub filename: Option<String>,
16    /// Line number in source file
17    pub line_number: Option<u32>,
18    /// Column number in source line
19    pub column: Option<u32>,
20    /// Name of module/library containing this symbol
21    pub module_name: Option<String>,
22    /// Offset from symbol start
23    pub offset: Option<usize>,
24}
25
26/// High-performance symbol resolver with caching
27pub struct SymbolResolver {
28    /// Cache of resolved symbols by instruction pointer
29    symbol_cache: HashMap<usize, ResolvedFrame>,
30    /// Total number of resolution attempts
31    resolution_count: AtomicUsize,
32    /// Number of cache hits for performance tracking
33    cache_hits: AtomicUsize,
34    /// Whether to perform symbol demangling
35    enable_demangling: bool,
36    /// Whether to resolve line number information
37    enable_line_info: bool,
38}
39
40impl SymbolResolver {
41    /// Create new symbol resolver with default settings
42    pub fn new() -> Self {
43        Self {
44            symbol_cache: HashMap::new(),
45            resolution_count: AtomicUsize::new(0),
46            cache_hits: AtomicUsize::new(0),
47            enable_demangling: true,
48            enable_line_info: true,
49        }
50    }
51
52    /// Create symbol resolver with custom options
53    pub fn with_options(enable_demangling: bool, enable_line_info: bool) -> Self {
54        Self {
55            symbol_cache: HashMap::new(),
56            resolution_count: AtomicUsize::new(0),
57            cache_hits: AtomicUsize::new(0),
58            enable_demangling,
59            enable_line_info,
60        }
61    }
62
63    /// Resolve single stack frame to symbol information
64    /// Uses cache for performance, falls back to symbol lookup
65    pub fn resolve_frame(&mut self, frame: &StackFrame) -> Option<ResolvedFrame> {
66        self.resolution_count.fetch_add(1, Ordering::Relaxed);
67
68        // Check cache first
69        if let Some(cached) = self.symbol_cache.get(&frame.instruction_pointer) {
70            self.cache_hits.fetch_add(1, Ordering::Relaxed);
71            return Some(cached.clone());
72        }
73
74        // Resolve symbol information
75        let resolved = self.perform_resolution(frame.instruction_pointer)?;
76
77        // Cache the result
78        self.symbol_cache
79            .insert(frame.instruction_pointer, resolved.clone());
80
81        Some(resolved)
82    }
83
84    pub fn resolve_batch(&mut self, frames: &[StackFrame]) -> Vec<Option<ResolvedFrame>> {
85        frames
86            .iter()
87            .map(|frame| self.resolve_frame(frame))
88            .collect()
89    }
90
91    pub fn resolve_addresses(&mut self, addresses: &[usize]) -> Vec<Option<ResolvedFrame>> {
92        addresses
93            .iter()
94            .map(|&addr| {
95                let frame = StackFrame::new(addr);
96                self.resolve_frame(&frame)
97            })
98            .collect()
99    }
100
101    pub fn get_cache_stats(&self) -> (usize, usize, f64) {
102        let resolutions = self.resolution_count.load(Ordering::Relaxed);
103        let hits = self.cache_hits.load(Ordering::Relaxed);
104        let hit_ratio = if resolutions > 0 {
105            hits as f64 / resolutions as f64
106        } else {
107            0.0
108        };
109        (resolutions, hits, hit_ratio)
110    }
111
112    pub fn clear_cache(&mut self) {
113        self.symbol_cache.clear();
114        self.resolution_count.store(0, Ordering::Relaxed);
115        self.cache_hits.store(0, Ordering::Relaxed);
116    }
117
118    pub fn cache_size(&self) -> usize {
119        self.symbol_cache.len()
120    }
121
122    pub fn preload_symbols(&mut self, addresses: &[usize]) {
123        for &addr in addresses {
124            if !self.symbol_cache.contains_key(&addr) {
125                if let Some(resolved) = self.perform_resolution(addr) {
126                    self.symbol_cache.insert(addr, resolved);
127                }
128            }
129        }
130    }
131
132    fn perform_resolution(&self, address: usize) -> Option<ResolvedFrame> {
133        // This would be platform-specific symbol resolution
134        // For now, provide a mock implementation
135
136        let symbol_name = self.lookup_symbol_name(address)?;
137        let demangled_name = if self.enable_demangling {
138            self.demangle_symbol(&symbol_name)
139        } else {
140            None
141        };
142
143        let (filename, line_number, column) = if self.enable_line_info {
144            self.lookup_line_info(address)
145        } else {
146            (None, None, None)
147        };
148
149        let module_name = self.lookup_module_name(address);
150        let offset = self.calculate_offset(address, &symbol_name);
151
152        Some(ResolvedFrame {
153            instruction_pointer: address,
154            symbol_name,
155            demangled_name,
156            filename,
157            line_number,
158            column,
159            module_name,
160            offset,
161        })
162    }
163
164    fn lookup_symbol_name(&self, _address: usize) -> Option<String> {
165        // Real implementation would use debug info (DWARF, PDB, dSYM)
166        // This feature is not yet implemented
167        None
168    }
169
170    fn demangle_symbol(&self, _mangled: &str) -> Option<String> {
171        // Real implementation should use rustc-demangle crate
172        // This feature is not yet implemented
173        None
174    }
175
176    fn lookup_line_info(&self, _address: usize) -> (Option<String>, Option<u32>, Option<u32>) {
177        // Real implementation would use addr2line or debug info
178        // This feature is not yet implemented
179        (None, None, None)
180    }
181
182    fn lookup_module_name(&self, _address: usize) -> Option<String> {
183        // Real implementation would use dladdr or equivalent
184        // This feature is not yet implemented
185        None
186    }
187
188    fn calculate_offset(&self, _address: usize, _symbol_name: &str) -> Option<usize> {
189        // Real implementation would calculate offset within symbol based on debug info
190        // This feature is not yet implemented
191        None
192    }
193}
194
195impl Default for SymbolResolver {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl ResolvedFrame {
202    pub fn display_name(&self) -> String {
203        let name = self.demangled_name.as_ref().unwrap_or(&self.symbol_name);
204
205        if let (Some(file), Some(line)) = (&self.filename, self.line_number) {
206            if let Some(col) = self.column {
207                format!("{} at {}:{}:{}", name, file, line, col)
208            } else {
209                format!("{} at {}:{}", name, file, line)
210            }
211        } else {
212            name.clone()
213        }
214    }
215
216    pub fn short_display(&self) -> String {
217        self.demangled_name
218            .as_ref()
219            .unwrap_or(&self.symbol_name)
220            .clone()
221    }
222
223    pub fn has_line_info(&self) -> bool {
224        self.filename.is_some() && self.line_number.is_some()
225    }
226
227    pub fn is_rust_symbol(&self) -> bool {
228        self.symbol_name.starts_with("_ZN")
229            || self.demangled_name.as_ref().is_some_and(|name| {
230                name.contains("::") || name.starts_with("std::") || name.starts_with("core::")
231            })
232    }
233
234    pub fn is_system_symbol(&self) -> bool {
235        self.module_name.as_ref().is_some_and(|module| {
236            module.contains("libc") || module.contains("libpthread") || module.contains("ld-")
237        })
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_resolved_frame_display() {
247        let frame = ResolvedFrame {
248            instruction_pointer: 0x1234,
249            symbol_name: "_ZN4main17h1234567890abcdefE".to_string(),
250            demangled_name: Some("main".to_string()),
251            filename: Some("src/main.rs".to_string()),
252            line_number: Some(42),
253            column: Some(10),
254            module_name: Some("main_executable".to_string()),
255            offset: Some(16),
256        };
257
258        assert_eq!(frame.display_name(), "main at src/main.rs:42:10");
259        assert_eq!(frame.short_display(), "main");
260        assert!(frame.has_line_info());
261        assert!(frame.is_rust_symbol());
262        assert!(!frame.is_system_symbol());
263    }
264
265    #[test]
266    fn test_resolved_frame_display_no_column() {
267        let frame = ResolvedFrame {
268            instruction_pointer: 0x1234,
269            symbol_name: "test_func".to_string(),
270            demangled_name: Some("test_func".to_string()),
271            filename: Some("lib.rs".to_string()),
272            line_number: Some(10),
273            column: None,
274            module_name: None,
275            offset: None,
276        };
277
278        assert_eq!(frame.display_name(), "test_func at lib.rs:10");
279    }
280
281    #[test]
282    fn test_resolved_frame_display_no_line_info() {
283        let frame = ResolvedFrame {
284            instruction_pointer: 0x5678,
285            symbol_name: "unknown_func".to_string(),
286            demangled_name: None,
287            filename: None,
288            line_number: None,
289            column: None,
290            module_name: None,
291            offset: None,
292        };
293
294        assert_eq!(frame.display_name(), "unknown_func");
295        assert_eq!(frame.short_display(), "unknown_func");
296        assert!(!frame.has_line_info());
297    }
298
299    #[test]
300    fn test_resolved_frame_is_rust_symbol_mangled() {
301        let frame = ResolvedFrame {
302            instruction_pointer: 0x1000,
303            symbol_name: "_ZN4test4main17habcdef123456E".to_string(),
304            demangled_name: None,
305            filename: None,
306            line_number: None,
307            column: None,
308            module_name: None,
309            offset: None,
310        };
311
312        assert!(frame.is_rust_symbol());
313    }
314
315    #[test]
316    fn test_resolved_frame_is_rust_symbol_demangled() {
317        let frame = ResolvedFrame {
318            instruction_pointer: 0x1000,
319            symbol_name: "func".to_string(),
320            demangled_name: Some("std::collections::HashMap::new".to_string()),
321            filename: None,
322            line_number: None,
323            column: None,
324            module_name: None,
325            offset: None,
326        };
327
328        assert!(frame.is_rust_symbol());
329    }
330
331    #[test]
332    fn test_resolved_frame_is_system_symbol_libc() {
333        let frame = ResolvedFrame {
334            instruction_pointer: 0x2000,
335            symbol_name: "malloc".to_string(),
336            demangled_name: None,
337            filename: None,
338            line_number: None,
339            column: None,
340            module_name: Some("libc.so.6".to_string()),
341            offset: None,
342        };
343
344        assert!(frame.is_system_symbol());
345    }
346
347    #[test]
348    fn test_resolved_frame_is_system_symbol_pthread() {
349        let frame = ResolvedFrame {
350            instruction_pointer: 0x3000,
351            symbol_name: "pthread_create".to_string(),
352            demangled_name: None,
353            filename: None,
354            line_number: None,
355            column: None,
356            module_name: Some("libpthread.so.0".to_string()),
357            offset: None,
358        };
359
360        assert!(frame.is_system_symbol());
361    }
362
363    #[test]
364    fn test_resolved_frame_is_system_symbol_ld() {
365        let frame = ResolvedFrame {
366            instruction_pointer: 0x4000,
367            symbol_name: "_start".to_string(),
368            demangled_name: None,
369            filename: None,
370            line_number: None,
371            column: None,
372            module_name: Some("ld-linux-x86-64.so.2".to_string()),
373            offset: None,
374        };
375
376        assert!(frame.is_system_symbol());
377    }
378
379    #[test]
380    fn test_resolved_frame_not_system_symbol() {
381        let frame = ResolvedFrame {
382            instruction_pointer: 0x5000,
383            symbol_name: "my_function".to_string(),
384            demangled_name: None,
385            filename: None,
386            line_number: None,
387            column: None,
388            module_name: Some("my_app".to_string()),
389            offset: None,
390        };
391
392        assert!(!frame.is_system_symbol());
393    }
394
395    #[test]
396    fn test_symbol_resolver_new() {
397        let resolver = SymbolResolver::new();
398        assert!(resolver.symbol_cache.is_empty());
399        assert_eq!(resolver.cache_size(), 0);
400    }
401
402    #[test]
403    fn test_symbol_resolver_default() {
404        let resolver = SymbolResolver::default();
405        assert!(resolver.symbol_cache.is_empty());
406    }
407
408    #[test]
409    fn test_symbol_resolver_with_options() {
410        let resolver = SymbolResolver::with_options(false, false);
411        assert!(!resolver.enable_demangling);
412        assert!(!resolver.enable_line_info);
413    }
414
415    #[test]
416    fn test_symbol_resolver_cache_stats_initial() {
417        let resolver = SymbolResolver::new();
418        let (resolutions, hits, ratio) = resolver.get_cache_stats();
419
420        assert_eq!(resolutions, 0);
421        assert_eq!(hits, 0);
422        assert_eq!(ratio, 0.0);
423    }
424
425    #[test]
426    fn test_symbol_resolver_clear_cache() {
427        let mut resolver = SymbolResolver::new();
428        resolver.resolution_count.store(10, Ordering::Relaxed);
429        resolver.cache_hits.store(5, Ordering::Relaxed);
430
431        resolver.clear_cache();
432
433        let (resolutions, hits, _) = resolver.get_cache_stats();
434        assert_eq!(resolutions, 0);
435        assert_eq!(hits, 0);
436        assert_eq!(resolver.cache_size(), 0);
437    }
438
439    #[test]
440    fn test_symbol_resolver_resolve_frame_no_symbol() {
441        let mut resolver = SymbolResolver::new();
442        let frame = StackFrame::new(0x12345678);
443
444        let result = resolver.resolve_frame(&frame);
445        assert!(result.is_none());
446
447        let (resolutions, hits, _) = resolver.get_cache_stats();
448        assert_eq!(resolutions, 1);
449        assert_eq!(hits, 0);
450    }
451
452    #[test]
453    fn test_symbol_resolver_resolve_batch() {
454        let mut resolver = SymbolResolver::new();
455        let frames = vec![
456            StackFrame::new(0x1000),
457            StackFrame::new(0x2000),
458            StackFrame::new(0x3000),
459        ];
460
461        let results = resolver.resolve_batch(&frames);
462        assert_eq!(results.len(), 3);
463        assert!(results.iter().all(|r| r.is_none()));
464    }
465
466    #[test]
467    fn test_symbol_resolver_resolve_addresses() {
468        let mut resolver = SymbolResolver::new();
469        let addresses = vec![0x1000, 0x2000, 0x3000];
470
471        let results = resolver.resolve_addresses(&addresses);
472        assert_eq!(results.len(), 3);
473        assert!(results.iter().all(|r| r.is_none()));
474    }
475
476    #[test]
477    fn test_symbol_resolver_preload_symbols() {
478        let mut resolver = SymbolResolver::new();
479        let addresses = vec![0x1000, 0x2000];
480
481        resolver.preload_symbols(&addresses);
482        assert_eq!(resolver.cache_size(), 0);
483    }
484
485    #[test]
486    fn test_resolved_frame_clone() {
487        let frame = ResolvedFrame {
488            instruction_pointer: 0x1234,
489            symbol_name: "test".to_string(),
490            demangled_name: Some("test_demangled".to_string()),
491            filename: Some("test.rs".to_string()),
492            line_number: Some(1),
493            column: Some(2),
494            module_name: Some("test_mod".to_string()),
495            offset: Some(3),
496        };
497
498        let cloned = frame.clone();
499        assert_eq!(cloned.instruction_pointer, frame.instruction_pointer);
500        assert_eq!(cloned.symbol_name, frame.symbol_name);
501    }
502
503    #[test]
504    fn test_resolved_frame_debug() {
505        let frame = ResolvedFrame {
506            instruction_pointer: 0x1234,
507            symbol_name: "test".to_string(),
508            demangled_name: None,
509            filename: None,
510            line_number: None,
511            column: None,
512            module_name: None,
513            offset: None,
514        };
515
516        let debug_str = format!("{:?}", frame);
517        assert!(debug_str.contains("ResolvedFrame"));
518        assert!(debug_str.contains("instruction_pointer"));
519    }
520
521    #[test]
522    fn test_resolved_frame_core_symbol() {
523        let frame = ResolvedFrame {
524            instruction_pointer: 0x1000,
525            symbol_name: "core_func".to_string(),
526            demangled_name: Some("core::ptr::drop_in_place".to_string()),
527            filename: None,
528            line_number: None,
529            column: None,
530            module_name: None,
531            offset: None,
532        };
533
534        assert!(frame.is_rust_symbol());
535    }
536}