Skip to main content

ying_profiler/
callstack.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::hash::Hasher;
4
5use backtrace::BacktraceSymbol;
6use once_cell::sync::Lazy;
7use regex::Regex;
8#[cfg(feature = "profile-spans")]
9use tracing::Span;
10use wyhash::WyHash;
11
12use super::*;
13use crate::histogram::MillisHistogram;
14
15pub(crate) const MAX_NUM_FRAMES: usize = 30;
16
17pub type StdCallstack = Callstack<MAX_NUM_FRAMES>;
18
19/// An optimized Callstack struct that represents a single stack trace.
20/// No symbols are explicitly held here - the major savings is that
21/// we use an external dictionary to store symbols, because the same IPs
22/// are used over and over in many stack traces.
23///
24/// To reduce allocations, we only keep MAX_NUM_FRAMES frames.
25#[derive(Debug, Clone)]
26pub struct Callstack<const NF: usize> {
27    frames: [u64; NF],
28}
29
30impl<const NF: usize> Callstack<NF> {
31    /// Creates a Callback from a backtrace::Backtrace, preferably unresolved for speed
32    pub fn from_backtrace_unresolved(bt: &Backtrace) -> Self {
33        let mut cb = Self { frames: [0; NF] };
34        for i in TOP_FRAMES_TO_SKIP..(bt.frames().len().min(NF)) {
35            cb.frames[i - TOP_FRAMES_TO_SKIP] = bt.frames()[i].ip() as u64;
36        }
37        cb
38    }
39
40    pub fn compute_hash(&self) -> u64 {
41        let mut hasher = WyHash::with_seed(17);
42        hasher.write(unsafe { (self.frames).align_to::<u8>().1 });
43        hasher.finish()
44    }
45
46    /// Goes through the IPs stored and ensures that the symbol map has resolved symbols for
47    /// all of them.  If it does not, resolves the backtrace symbols and updates the symbol map.
48    /// Potentially very expensive due to resolving IPs
49    pub(crate) fn populate_symbol_map(&self, bt: &mut Backtrace, symbol_map: &SymbolMap) {
50        // For each IP in our trace that is not zero
51        for (i, ip) in self.frames.iter().enumerate() {
52            if *ip == 0 {
53                break;
54            }
55
56            // This is a concurrent hash map. It's OK for the contains/insert to not be atomic,
57            // because for each IP the symbol should be identical, so multiple inserts are idempotent.
58            if !symbol_map.contains_key(*ip) {
59                // IP not there. Get the corresponding frame from the backtrace
60                let frame = &bt.frames()[i + TOP_FRAMES_TO_SKIP];
61
62                // Get the symbol out.  Resolve the backtrace if necessary
63                if frame.symbols().is_empty() {
64                    bt.resolve();
65                }
66                let frame = &bt.frames()[i + TOP_FRAMES_TO_SKIP];
67
68                // Convert frame symbols into FriendlySymbols and add to symbol map
69                let friendlies = frame.symbols().iter().map(FriendlySymbol::from).collect();
70                symbol_map.insert(*ip, friendlies);
71            }
72        }
73    }
74
75    /// Obtains a DecoratedCallstack for display.
76    /// `println!("{}", cb.with_symbols(symbols));`
77    /// Set expand_frame to true to print out stack details with   > symbols
78    pub(crate) fn with_symbols<'s, 'm>(
79        &'s self,
80        symbols: &'m SymbolMap,
81        expand_frame: bool,
82    ) -> DecoratedCallstack<'s, 'm, NF> {
83        DecoratedCallstack {
84            cb: self,
85            symbols,
86            filename_info: false,
87            filter_poll: true,
88            expand_frame,
89            write_header: true,
90        }
91    }
92
93    /// Obtains a DecoratedCallstack for display with both symbol and filename/lineno info.
94    /// `println!("{}", cb.with_symbols_and_filename(symbols));`
95    pub(crate) fn with_symbols_and_filename<'s, 'm>(
96        &'s self,
97        symbols: &'m SymbolMap,
98        expand_frame: bool,
99    ) -> DecoratedCallstack<'s, 'm, NF> {
100        DecoratedCallstack {
101            cb: self,
102            symbols,
103            filename_info: true,
104            filter_poll: true,
105            expand_frame,
106            write_header: true,
107        }
108    }
109
110    /// Obtains a DecoratedCallstack for display with symbols with no inline expansion and no header.
111    pub(crate) fn with_symbols_no_inline_header<'s, 'm>(
112        &'s self,
113        symbols: &'m SymbolMap,
114    ) -> DecoratedCallstack<'s, 'm, NF> {
115        DecoratedCallstack {
116            cb: self,
117            symbols,
118            filename_info: false,
119            filter_poll: true,
120            expand_frame: false,
121            write_header: false,
122        }
123    }
124}
125
126/// [DecoratedCallstack] enables detailed stack trace printouts.
127/// Each callstack consists of multiple frames, parent frame calls the child frame so on.
128/// Each frame may expand to include multiple symbols, especially due to inlining.
129/// - `filename_info` - if True, prints out source filename info
130/// - `filter_poll` - if True, skips symbols in the frame which have `::poll::` in them
131/// - `expand_frame` - if False, does not print out inlined symbols at all
132/// - `write_header` - if True, adds "Callback <hash = 0x..>" header as the first line
133pub struct DecoratedCallstack<'cb, 's, const NF: usize> {
134    cb: &'cb Callstack<NF>,
135    symbols: &'s SymbolMap,
136    filename_info: bool,
137    filter_poll: bool,
138    expand_frame: bool,
139    write_header: bool,
140}
141
142impl<'cb, 's, const NF: usize> fmt::Display for DecoratedCallstack<'cb, 's, NF> {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        if self.write_header {
145            writeln!(f, "Callback <hash = 0x{:0x}>", self.cb.compute_hash())?;
146        }
147        for ip in &self.cb.frames {
148            self.symbols.with_value(*ip, |maybe_symbols| {
149                let Some(symbols) = maybe_symbols else {
150                    return Ok(());
151                };
152                if symbols.is_empty() {
153                    return Ok(());
154                }
155                writeln!(f, "  {}", stringify_symbol(&symbols[0], self.filename_info))?;
156                // Don't expand inlined `::poll::` subcalls, they aren't interesting
157                if self.expand_frame && !symbols[0].is_poll {
158                    for s in &symbols[1..] {
159                        if self.filter_poll && s.is_poll {
160                            continue;
161                        }
162                        writeln!(f, "    > {}", stringify_symbol(s, self.filename_info))?;
163                    }
164                }
165                Ok(())
166            })?;
167        }
168        Ok(())
169    }
170}
171
172fn stringify_symbol(s: &FriendlySymbol, include_filename: bool) -> String {
173    if include_filename {
174        format!(
175            "{}\n\t({:?}:{})",
176            s.friendly_name, s.shorter_filename, s.line_no
177        )
178    } else {
179        s.friendly_name.to_string()
180    }
181}
182
183struct SymbolRegexes {
184    name_end_re: Regex,
185    // List of common patterns in filenames that can be shortened
186    filename_res: Vec<(Regex, &'static str)>,
187}
188
189static SYMBOL_REGEXES: Lazy<SymbolRegexes> = Lazy::new(|| {
190    let filename_res = vec![
191        (Regex::new(r"^/rustc/\w+/library/").unwrap(), "RUST:"),
192        (
193            Regex::new(r"^/Users/\w+/.cargo/registry/src/github.com-\w+/").unwrap(),
194            "Cargo:",
195        ),
196        (
197            Regex::new(r"^/home/\w+/.cargo/registry/src/github.com-\w+/").unwrap(),
198            "Cargo:",
199        ),
200    ];
201    SymbolRegexes {
202        name_end_re: Regex::new(r"(::\w+)$").expect("Error constructing regex"),
203        filename_res,
204    }
205});
206
207/// A wrapper around BacktraceSymbol with cleaned up, demangled symbol names
208/// and shortened filename and line number as well.
209///
210/// The shorter filename has common patterns like /Users/*/.cargo/registry/src/github.com-..../
211/// and /rustc/..../library substituted out for better readability.
212pub struct FriendlySymbol {
213    friendly_name: String,
214    is_poll: bool,
215    shorter_filename: String,
216    line_no: u32,
217}
218
219impl From<&BacktraceSymbol> for FriendlySymbol {
220    fn from(s: &BacktraceSymbol) -> Self {
221        // Get demangled name and strip the final ::<hex>
222        let friendly_name = if let Some(symbolname) = s.name() {
223            let demangled = format!("{}", symbolname);
224            SYMBOL_REGEXES
225                .name_end_re
226                .replace(&demangled, "")
227                .into_owned()
228        } else {
229            "<none>".into()
230        };
231
232        let is_poll = friendly_name.contains("::poll::");
233
234        // Get filename and convert common patterns
235        let shorter_filename = if let Some(p) = s.filename() {
236            let filename = p.to_str().unwrap_or_default();
237            let mut new_filename = None;
238            for (re, abbrev) in &SYMBOL_REGEXES.filename_res {
239                let replaced = re.replace(filename, *abbrev);
240                if let Cow::Owned(_) = replaced {
241                    // This means string was replaced
242                    new_filename = Some(replaced.into_owned());
243                    break;
244                }
245            }
246            new_filename.unwrap_or_else(|| filename.to_owned())
247        } else {
248            String::new()
249        };
250
251        let line_no = s.lineno().unwrap_or(0);
252
253        Self {
254            friendly_name,
255            is_poll,
256            shorter_filename,
257            line_no,
258        }
259    }
260}
261
262#[derive(Copy, Clone, PartialEq, Debug)]
263pub enum Measurement {
264    AllocatedBytes,
265    RetainedBytes,
266}
267
268/// Central struct collecting stats about each stack trace
269#[derive(Debug, Clone)]
270pub struct StackStats {
271    stack: StdCallstack,
272    pub allocated_bytes: u64,
273    pub num_allocations: u64,
274    pub freed_bytes: u64,
275    pub num_frees: u64,
276    hist: MillisHistogram,
277    #[cfg(feature = "profile-spans")]
278    span: Span,
279}
280
281impl StackStats {
282    // Constructor not public.  Only this crate should create new stats.
283    pub(crate) fn new(stack: StdCallstack, initial_alloc_bytes: Option<u64>) -> Self {
284        Self {
285            stack,
286            allocated_bytes: initial_alloc_bytes.unwrap_or(0),
287            num_allocations: initial_alloc_bytes.map(|_| 1).unwrap_or(0),
288            freed_bytes: 0,
289            num_frees: 0,
290            hist: MillisHistogram::new(),
291            #[cfg(feature = "profile-spans")]
292            span: Span::current(),
293        }
294    }
295
296    /// Update stats when an allocation is freed
297    pub(crate) fn update_free_stats(&mut self, size: u64, alloc_time_ms: u64) {
298        self.num_frees += 1;
299        self.freed_bytes += size;
300        self.hist.add_sample(alloc_time_ms);
301    }
302
303    /// The number of "retained" bytes as seen by this stack from sampling
304    pub fn retained_profiled_bytes(&self) -> u64 {
305        // NOTE: saturating_sub here is really important, freed could be slightly bigger than allocated
306        self.allocated_bytes.saturating_sub(self.freed_bytes)
307    }
308
309    /// Create a rich multi-line report of this StackStats
310    /// * profiler: The `&YING_ALLOC` or global static defined to enable this profiler
311    /// * with_filenames - if True, include source filename in stack trace
312    /// * expand_frame - if True, include inlined symbols for each frame in each stack trace
313    pub fn rich_report(
314        &self,
315        profiler: &YingProfiler,
316        with_filenames: bool,
317        expand_frame: bool,
318    ) -> String {
319        let profiled_alloc_bytes = YingProfiler::profiled_bytes_allocated();
320        let pct = (self.allocated_bytes as f64) * 100.0 / (profiled_alloc_bytes as f64);
321        let mut report = format!(
322            "{} profiled bytes allocated ({pct:.2}%) ({} allocations)\n",
323            self.allocated_bytes, self.num_allocations
324        );
325        let retained = self.retained_profiled_bytes();
326        let _ = writeln!(
327            &mut report,
328            "  {} profiled bytes retained  ({} frees)",
329            retained, self.num_frees
330        );
331        let retained_pct_allocs = (retained as f64) * 100.0 / (self.allocated_bytes as f64);
332        let retained_pct_all =
333            (retained as f64) * 100.0 / YingProfiler::profiled_bytes_retained() as f64;
334        let _ = writeln!(
335            &mut report,
336            "    ({retained_pct_all:.2}% of all retained profiled allocs) ({retained_pct_allocs:.2}% of allocated bytes)",
337        );
338        let _ = writeln!(&mut report, "  {}", self.hist);
339
340        #[cfg(feature = "profile-spans")]
341        if !self.span.is_disabled() {
342            let _ = writeln!(&mut report, "\ttracing span id: {:?}", self.span.id());
343        }
344
345        // TODO: this won't be needed once we upgrade from dashmap to something which does atomic reads
346        // Also try to make locking or accesses more fine grained
347        profiler.lock_out_profiler(|| {
348            let decorated_stack = if with_filenames {
349                self.stack
350                    .with_symbols_and_filename(&profiler.get_state().symbol_map, expand_frame)
351            } else {
352                self.stack
353                    .with_symbols(&profiler.get_state().symbol_map, expand_frame)
354            };
355            let _ = writeln!(&mut report, "{}", decorated_stack);
356        });
357        report
358    }
359
360    /// Creates a really simple dtrace-compatible multi line string report, with a single measurement at the end.
361    /// An empty line will be appended at the end.
362    /// DTrace reports always have inlining > turned off.
363    ///
364    /// * profiler: The `&YING_ALLOC` or global static defined to enable this profiler
365    /// * measurement - enum for what to measure, allocated bytes or retained bytes
366    pub fn dtrace_report(&self, profiler: &YingProfiler, measurement: Measurement) -> String {
367        let metric = match measurement {
368            Measurement::AllocatedBytes => self.allocated_bytes,
369            Measurement::RetainedBytes => self.retained_profiled_bytes(),
370        };
371
372        let mut report = String::new();
373        profiler.lock_out_profiler(|| {
374            let decorated_stack = self
375                .stack
376                .with_symbols_no_inline_header(&profiler.get_state().symbol_map);
377            let _ = write!(&mut report, "{}", decorated_stack);
378        });
379
380        let _ = writeln!(&mut report, "  {}", metric);
381        report
382    }
383}