Skip to main content

egui/
callstack.rs

1use std::fmt::Write as _;
2
3#[derive(Clone)]
4struct Frame {
5    /// `_main` is usually as the deepest depth.
6    depth: usize,
7    name: String,
8    file_and_line: String,
9}
10
11/// Capture a callstack, skipping the frames that are not interesting.
12///
13/// In particular: slips everything before `egui::Context::run`,
14/// and skipping all frames in the `egui::` namespace.
15#[inline(never)]
16pub fn capture() -> String {
17    let mut frames = vec![];
18    let mut depth = 0;
19
20    backtrace::trace(|frame| {
21        // Resolve this instruction pointer to a symbol name
22        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; // note: we can resolve multiple symbols on the same frame.
45
46        true // keep going to the next frame
47    });
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    // Inclusive:
55    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    /// Is this the name of some sort of useful entry point?
68    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(); // main on top, i.e. chronological order. Same as Python.
78
79    // Remove frames that are uninteresting:
80    frames.retain(|frame| {
81        // Keep the first "start" frame we can detect (e.g. `main`) to give the user a sense of chronology:
82        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        // Remove stuff that isn't user calls:
92        let skip_prefixes = [
93            // "backtrace::", // not needed, since we cut at egui::callstack::capture
94            "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                // Show that some frames were elided
135                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    // We get a hex suffix (at least on macOS) which is quite unhelpful,
153    // e.g. `my_crate::my_function::h3bedd97b1e03baa5`.
154    // Let's strip that.
155    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
173/// Shorten a path to a Rust source file from a callstack.
174///
175/// Example input:
176/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
177/// * `crates/rerun/src/main.rs`
178/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
179fn shorten_source_file_path(path: &std::path::Path) -> String {
180    // Look for `src` and strip everything up to it.
181
182    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    // Look for the last `src`:
192    if let Some(src_idx) = src_idx {
193        // Before `src` comes the name of the crate - let's include that:
194        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        // No `src` directory found - weird!
204        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}