1use std::fmt::Write as _;
2
3#[derive(Clone)]
4struct Frame {
5 depth: usize,
7 name: String,
8 file_and_line: String,
9}
10
11#[inline(never)]
16pub fn capture() -> String {
17 let mut frames = vec![];
18 let mut depth = 0;
19
20 backtrace::trace(|frame| {
21 backtrace::resolve_frame(frame, |symbol| {
23 let mut file_and_line = symbol.filename().map(shorten_source_file_path);
24
25 if let Some(file_and_line) = &mut file_and_line
26 && let Some(line_nr) = symbol.lineno()
27 {
28 write!(file_and_line, ":{line_nr}").ok();
29 }
30 let file_and_line = file_and_line.unwrap_or_default();
31
32 let name = symbol
33 .name()
34 .map(|name| clean_symbol_name(name.to_string()))
35 .unwrap_or_default();
36
37 frames.push(Frame {
38 depth,
39 name,
40 file_and_line,
41 });
42 });
43
44 depth += 1; true });
48
49 if frames.is_empty() {
50 return
51 "Failed to capture a backtrace. A common cause of this is compiling with panic=\"abort\" (https://github.com/rust-lang/backtrace-rs/issues/397)".to_owned();
52 }
53
54 let mut min_depth = 0;
56 let mut max_depth = usize::MAX;
57
58 for frame in &frames {
59 if frame.name.starts_with("egui::callstack::capture") {
60 min_depth = frame.depth + 1;
61 }
62 if frame.name.starts_with("egui::context::Context::run") {
63 max_depth = frame.depth;
64 }
65 }
66
67 fn is_start_name(name: &str) -> bool {
69 name == "main"
70 || name == "_main"
71 || name.starts_with("eframe::run_native")
72 || name.starts_with("egui::context::Context::run")
73 }
74
75 let mut has_kept_any_start_names = false;
76
77 frames.reverse(); frames.retain(|frame| {
81 if is_start_name(&frame.name) && !has_kept_any_start_names {
83 has_kept_any_start_names = true;
84 return true;
85 }
86
87 if frame.depth < min_depth || max_depth < frame.depth {
88 return false;
89 }
90
91 let skip_prefixes = [
93 "egui::",
95 "<egui::",
96 "<F as egui::widgets::Widget>",
97 "egui_plot::",
98 "egui_extras::",
99 "core::ptr::drop_in_place<egui::ui::Ui>",
100 "eframe::",
101 "core::ops::function::FnOnce::call_once",
102 "<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once",
103 ];
104 for prefix in skip_prefixes {
105 if frame.name.starts_with(prefix) {
106 return false;
107 }
108 }
109 true
110 });
111
112 let mut deepest_depth = 0;
113 let mut widest_file_line = 0;
114 for frame in &frames {
115 deepest_depth = frame.depth.max(deepest_depth);
116 widest_file_line = frame.file_and_line.len().max(widest_file_line);
117 }
118
119 let widest_depth = deepest_depth.to_string().len();
120
121 let mut formatted = String::new();
122
123 if !frames.is_empty() {
124 let mut last_depth = frames[0].depth;
125
126 for frame in &frames {
127 let Frame {
128 depth,
129 name,
130 file_and_line,
131 } = frame;
132
133 if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth {
134 writeln!(formatted, "{:widest_depth$} …", "").ok();
136 }
137
138 writeln!(
139 formatted,
140 "{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}"
141 )
142 .ok();
143
144 last_depth = frame.depth;
145 }
146 }
147
148 formatted
149}
150
151fn clean_symbol_name(mut s: String) -> String {
152 if let Some(h) = s.rfind("::h") {
156 let hex = &s[h + 3..];
157 if hex.len() == 16 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
158 s.truncate(h);
159 }
160 }
161
162 s
163}
164
165#[test]
166fn test_clean_symbol_name() {
167 assert_eq!(
168 clean_symbol_name("my_crate::my_function::h3bedd97b1e03baa5".to_owned()),
169 "my_crate::my_function"
170 );
171}
172
173fn shorten_source_file_path(path: &std::path::Path) -> String {
180 let components: Vec<_> = path.iter().map(|path| path.to_string_lossy()).collect();
183
184 let mut src_idx = None;
185 for (i, c) in components.iter().enumerate() {
186 if c == "src" {
187 src_idx = Some(i);
188 }
189 }
190
191 if let Some(src_idx) = src_idx {
193 let first_index = src_idx.saturating_sub(1);
195
196 let mut output = components[first_index].to_string();
197 for component in &components[first_index + 1..] {
198 output.push('/');
199 output.push_str(component);
200 }
201 output
202 } else {
203 path.display().to_string()
205 }
206}
207
208#[test]
209fn test_shorten_path() {
210 for (before, after) in [
211 (
212 "/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
213 "tokio-1.24.1/src/runtime/runtime.rs",
214 ),
215 ("crates/rerun/src/main.rs", "rerun/src/main.rs"),
216 (
217 "/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
218 "core/src/ops/function.rs",
219 ),
220 ("/weird/path/file.rs", "/weird/path/file.rs"),
221 ] {
222 use std::str::FromStr as _;
223 let before = std::path::PathBuf::from_str(before).unwrap();
224 assert_eq!(shorten_source_file_path(&before), after);
225 }
226}