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#[derive(Debug, Clone)]
26pub struct Callstack<const NF: usize> {
27 frames: [u64; NF],
28}
29
30impl<const NF: usize> Callstack<NF> {
31 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 pub(crate) fn populate_symbol_map(&self, bt: &mut Backtrace, symbol_map: &SymbolMap) {
50 for (i, ip) in self.frames.iter().enumerate() {
52 if *ip == 0 {
53 break;
54 }
55
56 if !symbol_map.contains_key(*ip) {
59 let frame = &bt.frames()[i + TOP_FRAMES_TO_SKIP];
61
62 if frame.symbols().is_empty() {
64 bt.resolve();
65 }
66 let frame = &bt.frames()[i + TOP_FRAMES_TO_SKIP];
67
68 let friendlies = frame.symbols().iter().map(FriendlySymbol::from).collect();
70 symbol_map.insert(*ip, friendlies);
71 }
72 }
73 }
74
75 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 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 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
126pub 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 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 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
207pub 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 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 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 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#[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 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 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 pub fn retained_profiled_bytes(&self) -> u64 {
305 self.allocated_bytes.saturating_sub(self.freed_bytes)
307 }
308
309 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 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 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}