memscope_rs/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        // Mock implementation - real version would use debug info
166        match address % 7 {
167            0 => Some("_ZN4main17h1234567890abcdefE".to_string()),
168            1 => Some("_ZN9allocator8allocate17h9876543210fedcbaE".to_string()),
169            2 => Some("process_data".to_string()),
170            3 => Some("_ZN3std6thread6spawn17habcdef1234567890E".to_string()),
171            4 => Some("handle_request".to_string()),
172            5 => Some("_ZN4core3ptr8drop_in_place17h1111222233334444E".to_string()),
173            _ => Some(format!("unknown_symbol_{:x}", address)),
174        }
175    }
176
177    fn demangle_symbol(&self, mangled: &str) -> Option<String> {
178        // Simple demangling for Rust symbols
179        if mangled.starts_with("_ZN") {
180            // This is a very simplified demangling - real implementation would be more complex
181            if mangled.contains("main") {
182                Some("main".to_string())
183            } else if mangled.contains("allocate") {
184                Some("allocator::allocate".to_string())
185            } else if mangled.contains("spawn") {
186                Some("std::thread::spawn".to_string())
187            } else if mangled.contains("drop_in_place") {
188                Some("core::ptr::drop_in_place".to_string())
189            } else {
190                Some(format!("demangled({})", mangled))
191            }
192        } else {
193            None
194        }
195    }
196
197    fn lookup_line_info(&self, address: usize) -> (Option<String>, Option<u32>, Option<u32>) {
198        // Mock implementation
199        let file = match address % 4 {
200            0 => Some("src/main.rs".to_string()),
201            1 => Some("src/allocator.rs".to_string()),
202            2 => Some("src/process.rs".to_string()),
203            _ => Some("src/lib.rs".to_string()),
204        };
205
206        let line = Some((address % 500) as u32 + 1);
207        let column = Some((address % 80) as u32 + 1);
208
209        (file, line, column)
210    }
211
212    fn lookup_module_name(&self, address: usize) -> Option<String> {
213        // Mock implementation
214        match address % 3 {
215            0 => Some("main_executable".to_string()),
216            1 => Some("libstd.so".to_string()),
217            _ => Some("libcore.so".to_string()),
218        }
219    }
220
221    fn calculate_offset(&self, address: usize, _symbol_name: &str) -> Option<usize> {
222        // Mock implementation - calculate offset within symbol
223        Some(address % 256)
224    }
225}
226
227impl Default for SymbolResolver {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233impl ResolvedFrame {
234    pub fn display_name(&self) -> String {
235        let name = self.demangled_name.as_ref().unwrap_or(&self.symbol_name);
236
237        if let (Some(file), Some(line)) = (&self.filename, self.line_number) {
238            if let Some(col) = self.column {
239                format!("{} at {}:{}:{}", name, file, line, col)
240            } else {
241                format!("{} at {}:{}", name, file, line)
242            }
243        } else {
244            name.clone()
245        }
246    }
247
248    pub fn short_display(&self) -> String {
249        self.demangled_name
250            .as_ref()
251            .unwrap_or(&self.symbol_name)
252            .clone()
253    }
254
255    pub fn has_line_info(&self) -> bool {
256        self.filename.is_some() && self.line_number.is_some()
257    }
258
259    pub fn is_rust_symbol(&self) -> bool {
260        self.symbol_name.starts_with("_ZN")
261            || self.demangled_name.as_ref().is_some_and(|name| {
262                name.contains("::") || name.starts_with("std::") || name.starts_with("core::")
263            })
264    }
265
266    pub fn is_system_symbol(&self) -> bool {
267        self.module_name.as_ref().is_some_and(|module| {
268            module.contains("libc") || module.contains("libpthread") || module.contains("ld-")
269        })
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_basic_resolution() {
279        let mut resolver = SymbolResolver::new();
280        let frame = StackFrame::new(0x12345678);
281
282        let resolved = resolver.resolve_frame(&frame);
283        assert!(resolved.is_some());
284
285        let resolved = resolved.expect("Should resolve");
286        assert_eq!(resolved.instruction_pointer, 0x12345678);
287        assert!(!resolved.symbol_name.is_empty());
288    }
289
290    #[test]
291    fn test_cache_functionality() {
292        let mut resolver = SymbolResolver::new();
293        let frame = StackFrame::new(0x12345678);
294
295        // First resolution should be a cache miss
296        let resolved1 = resolver.resolve_frame(&frame);
297        assert!(resolved1.is_some());
298
299        // Second resolution should be a cache hit
300        let resolved2 = resolver.resolve_frame(&frame);
301        assert!(resolved2.is_some());
302
303        let (resolutions, hits, hit_ratio) = resolver.get_cache_stats();
304        assert_eq!(resolutions, 2);
305        assert_eq!(hits, 1);
306        assert!((hit_ratio - 0.5).abs() < 0.01);
307    }
308
309    #[test]
310    fn test_batch_resolution() {
311        let mut resolver = SymbolResolver::new();
312        let frames = vec![
313            StackFrame::new(0x1000),
314            StackFrame::new(0x2000),
315            StackFrame::new(0x3000),
316        ];
317
318        let resolved = resolver.resolve_batch(&frames);
319        assert_eq!(resolved.len(), 3);
320
321        for result in resolved {
322            assert!(result.is_some());
323        }
324    }
325
326    #[test]
327    fn test_demangling() {
328        let resolver = SymbolResolver::new();
329
330        let demangled = resolver.demangle_symbol("_ZN4main17h1234567890abcdefE");
331        assert_eq!(demangled, Some("main".to_string()));
332
333        let not_mangled = resolver.demangle_symbol("regular_function");
334        assert_eq!(not_mangled, None);
335    }
336
337    #[test]
338    fn test_resolved_frame_display() {
339        let frame = ResolvedFrame {
340            instruction_pointer: 0x1234,
341            symbol_name: "_ZN4main17h1234567890abcdefE".to_string(),
342            demangled_name: Some("main".to_string()),
343            filename: Some("src/main.rs".to_string()),
344            line_number: Some(42),
345            column: Some(10),
346            module_name: Some("main_executable".to_string()),
347            offset: Some(16),
348        };
349
350        assert_eq!(frame.display_name(), "main at src/main.rs:42:10");
351        assert_eq!(frame.short_display(), "main");
352        assert!(frame.has_line_info());
353        assert!(frame.is_rust_symbol());
354        assert!(!frame.is_system_symbol());
355    }
356
357    #[test]
358    fn test_preload_symbols() {
359        let mut resolver = SymbolResolver::new();
360        let addresses = vec![0x1000, 0x2000, 0x3000];
361
362        assert_eq!(resolver.cache_size(), 0);
363
364        resolver.preload_symbols(&addresses);
365        assert_eq!(resolver.cache_size(), 3);
366    }
367
368    #[test]
369    fn test_clear_cache() {
370        let mut resolver = SymbolResolver::new();
371        let frame = StackFrame::new(0x12345678);
372
373        resolver.resolve_frame(&frame);
374        assert!(resolver.cache_size() > 0);
375
376        resolver.clear_cache();
377        assert_eq!(resolver.cache_size(), 0);
378
379        let (resolutions, hits, _) = resolver.get_cache_stats();
380        assert_eq!(resolutions, 0);
381        assert_eq!(hits, 0);
382    }
383}