mirror of https://github.com/emilk/egui.git
Emil Ernerfeldt
1 year ago
committed by
GitHub
18 changed files with 463 additions and 125 deletions
@ -0,0 +1,186 @@ |
|||
#[derive(Clone)] |
|||
struct Frame { |
|||
/// `_main` is usually as the deepest depth.
|
|||
depth: usize, |
|||
name: String, |
|||
file_and_line: String, |
|||
} |
|||
|
|||
/// Capture a callstack, skipping the frames that are not interesting.
|
|||
///
|
|||
/// In particular: slips everything before `egui::Context::run`,
|
|||
/// and skipping all frames in the `egui::` namespace.
|
|||
pub fn capture() -> String { |
|||
let mut frames = vec![]; |
|||
let mut depth = 0; |
|||
|
|||
backtrace::trace(|frame| { |
|||
// Resolve this instruction pointer to a symbol name
|
|||
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| name.to_string()) |
|||
.unwrap_or_default(); |
|||
|
|||
frames.push(Frame { |
|||
depth, |
|||
name, |
|||
file_and_line, |
|||
}); |
|||
}); |
|||
|
|||
depth += 1; // note: we can resolve multiple symbols on the same frame.
|
|||
|
|||
true // keep going to the next frame
|
|||
}); |
|||
|
|||
if frames.is_empty() { |
|||
return Default::default(); |
|||
} |
|||
|
|||
// Inclusive:
|
|||
let mut min_depth = 0; |
|||
let mut max_depth = frames.len() - 1; |
|||
|
|||
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; |
|||
} |
|||
} |
|||
|
|||
// Remove frames that are uninteresting:
|
|||
frames.retain(|frame| { |
|||
// Keep some special frames to give the user a sense of chronology:
|
|||
if frame.name == "main" |
|||
|| frame.name == "_main" |
|||
|| frame.name.starts_with("egui::context::Context::run") |
|||
|| frame.name.starts_with("eframe::run_native") |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if frame.depth < min_depth || max_depth < frame.depth { |
|||
return false; |
|||
} |
|||
|
|||
// Remove stuff that isn't user calls:
|
|||
let skip_prefixes = [ |
|||
// "backtrace::", // not needed, since we cut at at egui::callstack::capture
|
|||
"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 |
|||
}); |
|||
|
|||
frames.reverse(); // main on top, i.e. chronological order. Same as Python.
|
|||
|
|||
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 { |
|||
// Show that some frames were elided
|
|||
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 |
|||
} |
|||
|
|||
/// Shorten a path to a Rust source file from a callstack.
|
|||
///
|
|||
/// Example input:
|
|||
/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
|
|||
/// * `crates/rerun/src/main.rs`
|
|||
/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
|
|||
fn shorten_source_file_path(path: &std::path::Path) -> String { |
|||
// Look for `src` and strip everything up to it.
|
|||
|
|||
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); |
|||
} |
|||
} |
|||
|
|||
// Look for the last `src`:
|
|||
if let Some(src_idx) = src_idx { |
|||
// Before `src` comes the name of the crate - let's include that:
|
|||
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 { |
|||
// No `src` directory found - weird!
|
|||
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); |
|||
} |
|||
} |
Loading…
Reference in new issue