#[derive(Clone)]
struct Frame {
depth: usize,
name: String,
file_and_line: String,
}
#[inline(never)]
pub fn capture() -> String {
let mut frames = vec![];
let mut depth = 0;
backtrace::trace(|frame| {
backtrace::resolve_frame(frame, |symbol| {
let mut file_and_line = symbol.filename().map(shorten_source_file_path);
if let Some(file_and_line) = &mut file_and_line {
if let Some(line_nr) = symbol.lineno() {
file_and_line.push_str(&format!(":{line_nr}"));
}
}
let file_and_line = file_and_line.unwrap_or_default();
let name = symbol
.name()
.map(|name| clean_symbol_name(name.to_string()))
.unwrap_or_default();
frames.push(Frame {
depth,
name,
file_and_line,
});
});
depth += 1; true });
if frames.is_empty() {
return
"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();
}
let mut min_depth = 0;
let mut max_depth = usize::MAX;
for frame in &frames {
if frame.name.starts_with("egui::callstack::capture") {
min_depth = frame.depth + 1;
}
if frame.name.starts_with("egui::context::Context::run") {
max_depth = frame.depth;
}
}
fn is_start_name(name: &str) -> bool {
name == "main"
|| name == "_main"
|| name.starts_with("eframe::run_native")
|| name.starts_with("egui::context::Context::run")
}
let mut has_kept_any_start_names = false;
frames.reverse(); frames.retain(|frame| {
if is_start_name(&frame.name) && !has_kept_any_start_names {
has_kept_any_start_names = true;
return true;
}
if frame.depth < min_depth || max_depth < frame.depth {
return false;
}
let skip_prefixes = [
"egui::",
"<egui::",
"<F as egui::widgets::Widget>",
"egui_plot::",
"egui_extras::",
"core::ptr::drop_in_place<egui::ui::Ui>",
"eframe::",
"core::ops::function::FnOnce::call_once",
"<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once",
];
for prefix in skip_prefixes {
if frame.name.starts_with(prefix) {
return false;
}
}
true
});
let mut deepest_depth = 0;
let mut widest_file_line = 0;
for frame in &frames {
deepest_depth = frame.depth.max(deepest_depth);
widest_file_line = frame.file_and_line.len().max(widest_file_line);
}
let widest_depth = deepest_depth.to_string().len();
let mut formatted = String::new();
if !frames.is_empty() {
let mut last_depth = frames[0].depth;
for frame in &frames {
let Frame {
depth,
name,
file_and_line,
} = frame;
if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth {
formatted.push_str(&format!("{:widest_depth$} …\n", ""));
}
formatted.push_str(&format!(
"{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n"
));
last_depth = frame.depth;
}
}
formatted
}
fn clean_symbol_name(mut s: String) -> String {
if let Some(h) = s.rfind("::h") {
let hex = &s[h + 3..];
if hex.len() == 16 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
s.truncate(h);
}
}
s
}
#[test]
fn test_clean_symbol_name() {
assert_eq!(
clean_symbol_name("my_crate::my_function::h3bedd97b1e03baa5".to_owned()),
"my_crate::my_function"
);
}
fn shorten_source_file_path(path: &std::path::Path) -> String {
let components: Vec<_> = path.iter().map(|path| path.to_string_lossy()).collect();
let mut src_idx = None;
for (i, c) in components.iter().enumerate() {
if c == "src" {
src_idx = Some(i);
}
}
if let Some(src_idx) = src_idx {
let first_index = src_idx.saturating_sub(1);
let mut output = components[first_index].to_string();
for component in &components[first_index + 1..] {
output.push('/');
output.push_str(component);
}
output
} else {
path.display().to_string()
}
}
#[test]
fn test_shorten_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
use std::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);
}
}