T20: Debug

- DebugToolbar, BreakpointList, CallStack with folded library frames, ThreadList
- VariablesPanel and WatchPanel, colored by kind, changes washed
- HexViewer at a base address with the byte under the pointer lit on both sides
- Disassembly with source lines, registers and numbers colored, gutter breakpoints
- Flamegraph that zooms by frame, TimelineProfiler that zooms and pans by wheel
- NetworkInspector with a waterfall, a filter and a request's headers and body
- icons: redo-dot, arrow-down-to-dot, arrow-up-from-dot
- gallery: the Debug page
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 02:38:50 -04:00
1 parent 2c961833ac
commit f8bfa29c17
22 files changed
+3240 -14

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
use std::rc::Rc;
use ely_gpui_component::{
debug::{Disassembly, HexViewer, Instruction},
theme::{ActiveTheme, Radius},
};
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
use crate::ui::{keep, section, set};
/// A few hundred bytes of a process's memory, the same each run.
fn memory() -> Rc<Vec<u8>> {
let mut bytes: Vec<u8> = b"\x7fELF\x02\x01\x01\x00".to_vec();
bytes.extend(b"palette\0accent\0fallback\0");
bytes.extend((0u32..256).map(|ix| (ix.wrapping_mul(37) ^ (ix >> 3)) as u8));
bytes.extend(b"Blend toward white by lift\0");
Rc::new(bytes)
}
fn listing() -> Rc<Vec<Instruction>> {
let at = |address: u64,
bytes: &[u8],
mnemonic: &'static str,
operands: &'static str,
comment: Option<&'static str>,
source: Option<&'static str>| Instruction {
address,
bytes: bytes.to_vec(),
mnemonic: mnemonic.into(),
operands: operands.into(),
comment: comment.map(Into::into),
source: source.map(Into::into),
};
Rc::new(vec![
at(
0x1000_4a20,
&[0x55],
"push",
"rbp",
None,
Some("pub fn lookup(&self, name: &str, lift: f32) -> Color {"),
),
at(
0x1000_4a21,
&[0x48, 0x89, 0xe5],
"mov",
"rbp, rsp",
None,
None,
),
at(
0x1000_4a24,
&[0x48, 0x83, 0xec, 0x30],
"sub",
"rsp, 0x30",
None,
None,
),
at(
0x1000_4a28,
&[0x48, 0x89, 0x7d, 0xe8],
"mov",
"qword ptr [rbp - 0x18], rdi",
Some("self"),
None,
),
at(
0x1000_4a2c,
&[0xf3, 0x0f, 0x11, 0x45, 0xe4],
"movss",
"dword ptr [rbp - 0x1c], xmm0",
Some("lift"),
None,
),
at(
0x1000_4a31,
&[0xe8, 0x9a, 0x01, 0x00, 0x00],
"call",
"0x100004bd0",
Some("BTreeMap::get"),
Some(" let base = self.colors.get(name).copied().unwrap_or(self.fallback);"),
),
at(
0x1000_4a36,
&[0x48, 0x85, 0xc0],
"test",
"rax, rax",
None,
None,
),
at(0x1000_4a39, &[0x74, 0x12], "je", "0x100004a4d", None, None),
at(
0x1000_4a3b,
&[0xf3, 0x0f, 0x10, 0x45, 0xe4],
"movss",
"xmm0, dword ptr [rbp - 0x1c]",
None,
Some(" base.lift(lift)"),
),
at(
0x1000_4a40,
&[0xe8, 0x4b, 0x02, 0x00, 0x00],
"call",
"0x100004c90",
Some("Color::lift"),
None,
),
at(
0x1000_4a45,
&[0x48, 0x83, 0xc4, 0x30],
"add",
"rsp, 0x30",
None,
None,
),
at(0x1000_4a49, &[0x5d], "pop", "rbp", None, None),
at(0x1000_4a4a, &[0xc3], "ret", "", None, Some("}")),
])
}
pub fn machine(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let picked = keep("debug-byte", || 8usize, window, cx);
let marks = keep("debug-marks", || vec![5usize], window, cx);
let (now_picked, now_marks) = (*picked.read(cx), marks.read(cx).clone());
let (pick, toggle) = (picked.clone(), marks.clone());
let theme = cx.theme();
let frame = || {
div()
.w(px(840.))
.p_2()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
};
section(
"MemoryViewer / HexViewer / Disassembly",
"Memory as a hex dump, sixteen bytes a row in fours beside the same bytes as text; the byte under the pointer lights on both sides and the picked one reads below as numbers. Machine code with its source lines, registers and numbers colored, the current instruction marked; press the gutter to set a breakpoint.",
cx,
)
.child(
frame().h(px(220.)).child(
HexViewer::new("debug-hex", memory())
.base(0x7ffd_5a20_1000)
.selected(now_picked..now_picked + 4)
.on_select(move |at, _, cx| set(&pick, at, cx)),
),
)
.child(
frame().h(px(300.)).child(
Disassembly::new("debug-disasm", listing())
.current(8)
.breakpoints(now_marks)
.on_breakpoint(move |ix, _, cx| {
let mut next = toggle.read(cx).clone();
match next.iter().position(|mark| *mark == ix) {
Some(at) => drop(next.remove(at)),
None => next.push(ix),
}
set(&toggle, next, cx)
}),
),
)
}
+30
View File
@@ -0,0 +1,30 @@
mod machine;
mod profiling;
mod session;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 20,
slug: "debug",
title: "Debug",
summary: "A paused program: its breakpoints, stack, threads and values, memory and machine code; profiles and network.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[Step::Rest];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(session::session(window, cx))
.child(session::breakpoints_and_stack(window, cx))
.child(session::values(window, cx))
.child(machine::machine(window, cx))
.child(profiling::profiles(window, cx))
.child(profiling::network(window, cx))
.into_any_element()
}
+246
View File
@@ -0,0 +1,246 @@
use std::rc::Rc;
use ely_gpui_component::{
debug::{Flamegraph, NetworkInspector, ProfileFrame, Request, Span, TimelineProfiler},
forms::{Input, TextInput},
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, section, set};
fn frame(name: &str, own: u64, calls: Vec<ProfileFrame>) -> ProfileFrame {
ProfileFrame {
name: name.to_string().into(),
own,
calls,
}
}
/// A frame's worth of an app's time, sampled.
fn profile() -> ProfileFrame {
frame(
"main",
4,
vec![
frame(
"run_loop",
6,
vec![
frame(
"draw",
10,
vec![
frame(
"layout",
38,
vec![
frame("measure_text", 22, vec![]),
frame("taffy::compute", 30, vec![]),
],
),
frame(
"paint",
26,
vec![
frame("shape_line", 18, vec![]),
frame("rasterize", 12, vec![]),
],
),
],
),
frame("dispatch_event", 14, vec![frame("on_click", 9, vec![])]),
],
),
frame("io_thread", 20, vec![frame("read", 16, vec![])]),
],
)
}
fn spans() -> Rc<Vec<Span>> {
let span = |track, name: &str, start, end| Span {
track,
name: name.to_string().into(),
start,
end,
};
Rc::new(vec![
span(0, "event", 0.0, 1.2),
span(0, "layout", 1.2, 5.6),
span(0, "paint", 5.6, 9.1),
span(0, "event", 16.6, 17.3),
span(0, "layout", 17.3, 20.4),
span(0, "paint", 20.4, 23.0),
span(1, "shape", 2.0, 4.8),
span(1, "shape", 18.0, 19.6),
span(2, "gc", 9.5, 12.0),
span(3, "read", 0.5, 14.0),
span(3, "parse", 14.0, 15.2),
])
}
pub fn profiles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let focus = keep("debug-focus", Vec::<usize>::new, window, cx);
let hovered = keep("debug-hovered", || None::<Vec<usize>>, window, cx);
let range = keep("debug-range", || (0.0_f64, 25.0_f64), window, cx);
let picked = keep("debug-span", || Some(1usize), window, cx);
let (now_focus, now_hovered, now_range, now_picked) = (
focus.read(cx).clone(),
hovered.read(cx).clone(),
*range.read(cx),
*picked.read(cx),
);
let (zoom, hover, pan, pick) = (
focus.clone(),
hovered.clone(),
range.clone(),
picked.clone(),
);
let timeline = TimelineProfiler::new(
"debug-timeline",
["Main", "Text", "Memory", "IO"],
spans(),
now_range,
)
.on_range(move |next, _, cx| set(&pan, next, cx))
.on_select(move |ix, _, cx| set(&pick, Some(ix), cx));
let timeline = match now_picked {
Some(ix) => timeline.selected(ix),
None => timeline,
};
section(
"PerformanceProfiler / Flamegraph / TimelineProfiler",
"Where the time went: each function as wide as its samples with its callees under it; press a frame to zoom in and a caller to zoom back out. Work over time on tracks; the wheel zooms around the pointer and a sideways wheel pans.",
cx,
)
.child(
div().w(px(840.)).child(
Flamegraph::new("debug-flame", profile())
.focus(now_focus)
.hovered(now_hovered)
.on_focus(move |path, _, cx| set(&zoom, path, cx))
.on_hover(move |path, _, cx| {
if *hover.read(cx) != path {
set(&hover, path, cx)
}
}),
),
)
.child(div().w(px(840.)).child(timeline))
}
fn requests() -> Vec<Request> {
let request = |method: &'static str,
url: &'static str,
status,
kind: &'static str,
size,
start,
duration,
body: Option<&'static str>| Request {
method: method.into(),
url: url.into(),
status,
kind: kind.into(),
size,
start,
duration,
headers: vec![
(
"content-type".into(),
if kind == "fetch" {
"application/json".into()
} else {
"text/html".into()
},
),
("cache-control".into(), "no-store".into()),
],
body: body.map(Into::into),
};
vec![
request(
"GET",
"https://ely.dev/",
Some(200),
"document",
18_400,
0.0,
120.0,
None,
),
request(
"GET",
"https://ely.dev/static/app.js",
Some(200),
"script",
212_000,
130.0,
240.0,
None,
),
request(
"GET",
"https://ely.dev/static/inter.woff2",
Some(304),
"font",
0,
140.0,
60.0,
None,
),
request(
"GET",
"https://api.ely.dev/v1/quotes?symbol=ELY",
Some(200),
"fetch",
2_300,
380.0,
95.0,
Some("{ \"symbol\": \"ELY\", \"price\": 128.4, \"change\": 0.012 }"),
),
request(
"POST",
"https://api.ely.dev/v1/orders",
Some(503),
"fetch",
180,
420.0,
310.0,
Some("{ \"error\": \"orders are paused\" }"),
),
request(
"GET",
"https://api.ely.dev/v1/stream",
None,
"fetch",
0,
500.0,
400.0,
None,
),
]
}
pub fn network(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let picked = keep("debug-request", || Some(4usize), window, cx);
let query = window.use_keyed_state("debug-network-query", cx, |window, cx| {
TextInput::new(window, cx).placeholder("Filter addresses")
});
let now = *picked.read(cx);
let text: SharedString = query.read(cx).text().to_string().into();
let pick = picked.clone();
let inspector = NetworkInspector::new("debug-network", requests())
.query(text)
.on_select(move |ix, _, cx| set(&pick, Some(ix), cx));
let inspector = match now {
Some(ix) => inspector.selected(ix),
None => inspector,
};
section(
"NetworkInspector",
"Requests in the order they started, with status, kind, size and time, and a waterfall of when each ran; a filter keeps matching addresses. Press one to read its headers and body.",
cx,
)
.child(div().w(px(320.)).child(Input::new(&query)))
.child(div().w(px(840.)).child(inspector))
}
+302
View File
@@ -0,0 +1,302 @@
use ely_gpui_component::{
debug::{
Breakpoint, BreakpointList, CallStack, DebugCommand, DebugState, DebugToolbar, StackFrame,
Thread, ThreadList, ThreadState, ValueKind, Variable, VariablesPanel, Watch, WatchPanel,
},
forms::{InputEvent, TextInput},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, row, section, set};
fn frames() -> Vec<StackFrame> {
let frame = |function: &str, path: &str, line, library| StackFrame {
function: function.to_string().into(),
path: path.to_string().into(),
line,
library,
};
vec![
frame("palette::Palette::lookup", "src/palette.rs", 48, false),
frame("palette::tints::{{closure}}", "src/main.rs", 65, false),
frame(
"core::iter::adapters::map::map_fold",
"core/src/iter/adapters/map.rs",
88,
true,
),
frame(
"core::iter::traits::iterator::Iterator::fold",
"core/src/iter/traits/iterator.rs",
2587,
true,
),
frame(
"alloc::vec::Vec::extend_trusted",
"alloc/src/vec/mod.rs",
3301,
true,
),
frame("palette::main", "src/main.rs", 65, false),
frame("std::rt::lang_start", "std/src/rt.rs", 206, true),
]
}
fn scopes() -> Vec<(SharedString, Vec<Variable>)> {
let value = |name: &str, value: &str, kind, type_name: Option<&str>, children| Variable {
name: name.to_string().into(),
value: value.to_string().into(),
kind,
type_name: type_name.map(|name| name.to_string().into()),
children,
};
vec![
(
"Locals".into(),
vec![
value(
"self",
"&Palette { colors: {…}, fallback: Color }",
ValueKind::Object,
Some("&Palette"),
vec![
value(
"colors",
"{\"accent\": Color, \"fg\": Color}",
ValueKind::Object,
Some("BTreeMap<String, Color>"),
vec![],
),
value(
"fallback",
"Color { red: 0.1, green: 0.1, blue: 0.1 }",
ValueKind::Object,
Some("Color"),
vec![
value("red", "0.1", ValueKind::Number, Some("f32"), vec![]),
value("green", "0.1", ValueKind::Number, Some("f32"), vec![]),
value("blue", "0.1", ValueKind::Number, Some("f32"), vec![]),
],
),
],
),
value("name", "\"accent\"", ValueKind::Text, Some("&str"), vec![]),
value("lift", "0.75", ValueKind::Number, Some("f32"), vec![]),
value("clamped", "false", ValueKind::Bool, Some("bool"), vec![]),
],
),
(
"Statics".into(),
vec![value(
"WHITE",
"Color { red: 1.0, green: 1.0, blue: 1.0 }",
ValueKind::Object,
Some("Color"),
vec![],
)],
),
]
}
pub fn session(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("debug-state", || DebugState::Paused, window, cx);
let said = keep("debug-said", || None::<SharedString>, window, cx);
let (now_state, told) = (*state.read(cx), said.read(cx).clone());
let (command, tell) = (state.clone(), said.clone());
section(
"DebugToolbar",
"The floating row that drives a session: continue or pause, the three steps while paused, restart and stop, each with its key.",
cx,
)
.child(row().child(DebugToolbar::new("debug-toolbar", now_state).on_command(move |which, _, cx| {
let next = match which {
DebugCommand::Pause => DebugState::Paused,
DebugCommand::Continue | DebugCommand::Restart => DebugState::Running,
_ => *command.read(cx),
};
set(&command, next, cx);
set(&tell, Some(format!("{which:?}").into()), cx);
})))
.children(told.map(Caption::new))
}
pub fn breakpoints_and_stack(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let points = keep(
"debug-breakpoints",
|| {
let point = |path: &str, line, enabled, condition: Option<&str>, hits| Breakpoint {
path: path.to_string().into(),
line,
enabled,
condition: condition.map(|text| text.to_string().into()),
hits,
};
vec![
point("src/palette.rs", 48, true, None, 3),
point("src/main.rs", 65, true, Some("step > 2"), 1),
point("src/theme/tokens.rs", 112, false, None, 0),
]
},
window,
cx,
);
let current = keep("debug-frame", || 0usize, window, cx);
let libraries = keep("debug-libraries", || false, window, cx);
let thread = keep("debug-thread", || 1u64, window, cx);
let (now_points, now_current, now_libraries, now_thread) = (
points.read(cx).clone(),
*current.read(cx),
*libraries.read(cx),
*thread.read(cx),
);
let (toggle, remove, pick, show, choose) = (
points.clone(),
points.clone(),
current.clone(),
libraries.clone(),
thread.clone(),
);
let threads = [
Thread {
id: 1,
name: "main".into(),
state: ThreadState::Stopped,
},
Thread {
id: 2,
name: "renderer".into(),
state: ThreadState::Paused,
},
Thread {
id: 3,
name: "io-worker".into(),
state: ThreadState::Paused,
},
];
section(
"BreakpointList / BreakpointGutter / CallStack / ThreadList",
"Every breakpoint with its condition and hits; the stack where the program waits, library frames folded until pressed; and the threads, one picked. The gutter's breakpoints are the editor's (CodeEditor).",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().w(px(400.)).child(
BreakpointList::new("debug-breakpoint-list", now_points)
.on_toggle(move |ix, on, _, cx| {
let mut next = toggle.read(cx).clone();
next[ix].enabled = on;
set(&toggle, next, cx)
})
.on_remove(move |ix, _, cx| {
let mut next = remove.read(cx).clone();
next.remove(ix);
set(&remove, next, cx)
}),
),
)
.child(
div().w(px(400.)).flex().flex_col().gap_4().child(
CallStack::new("debug-stack", frames())
.current(now_current)
.libraries(now_libraries)
.on_pick(move |ix, _, cx| set(&pick, ix, cx))
.on_libraries(move |on, _, cx| set(&show, on, cx)),
)
.child(ThreadList::new("debug-threads", threads).selected(now_thread).on_pick(move |id, _, cx| set(&choose, id, cx))),
),
)
}
pub fn values(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let open = keep(
"debug-open",
|| vec![SharedString::from("Locals"), "Locals/self".into()],
window,
cx,
);
let watches = keep(
"debug-watches",
|| {
vec![
Watch {
expression: "lift * 2.0".into(),
value: Ok(("1.5".into(), ValueKind::Number)),
},
Watch {
expression: "self.colors.len()".into(),
value: Ok(("2".into(), ValueKind::Number)),
},
Watch {
expression: "missing".into(),
value: Err("cannot find value `missing` in this scope".into()),
},
]
},
window,
cx,
);
let field = window.use_keyed_state("debug-watch-field", cx, |window, cx| {
TextInput::new(window, cx).placeholder("Add an expression")
});
window.use_keyed_state("debug-watch-sub", cx, {
let (field, watches) = (field.clone(), watches.clone());
move |_, cx| {
cx.subscribe(&field, move |_, input, event: &InputEvent, cx| {
if *event != InputEvent::Submit {
return;
}
let text = input.read(cx).text().trim().to_string();
if text.is_empty() {
return;
}
let mut next = watches.read(cx).clone();
next.push(Watch {
expression: text.into(),
value: Ok(("0.75".into(), ValueKind::Number)),
});
set(&watches, next, cx);
input.update(cx, |input, cx| input.set_text("", cx));
})
}
});
let (now_open, now_watches) = (open.read(cx).clone(), watches.read(cx).clone());
let (toggle, remove) = (open.clone(), watches.clone());
section(
"VariablesPanel / WatchPanel / ConsoleREPL",
"Variables by scope as a tree, colored by kind, with the value that changed at the last step washed; expressions watched at every step, errors in red. The REPL beside a paused program is the editor's DebugConsole.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().w(px(460.)).child(
VariablesPanel::new("debug-variables", scopes())
.open(now_open)
.changed([SharedString::from("Locals/lift")])
.on_toggle(move |key, on, _, cx| {
let mut next = toggle.read(cx).clone();
next.retain(|kept| kept != key);
if on {
next.push(key.clone());
}
set(&toggle, next, cx)
}),
),
)
.child(
div().w(px(340.)).child(
WatchPanel::new("debug-watch", now_watches, &field).on_remove(move |ix, _, cx| {
let mut next = remove.read(cx).clone();
next.remove(ix);
set(&remove, next, cx)
}),
),
),
)
}
+2
View File
@@ -1,6 +1,7 @@
mod buttons;
mod charts;
mod data;
mod debug;
mod editor;
mod feedback;
mod finance;
@@ -53,6 +54,7 @@ pub const ALL: &[Page] = &[
editor::PAGE,
terminal::PAGE,
git::PAGE,
debug::PAGE,
theme::PAGE,
];