dwarves-gui/src/trace_replay.rs
//! **The 3D replay of a traced run** — the room plays a recorded execution back:
//! a function's head GLOWS while it is open on some thread (its colour the
//! thread's), each call runs as a PULSE along the spline from the caller's head,
//! and a TIMELINE under the map scrubs to any time (the open stacks of that
//! moment come from the server, `stacks: true`).
//!
//! The server side is `Viz.CodeMap` lod `trace_runs` (the traced runs of a
//! repo) and lod `trace` (a window of one run's events, each on a head of THIS
//! map, an entry with its caller's head). This module is the pure half: parsing,
//! the replay clock, and what is drawn — the tab holds a [`TraceUi`] and wires
//! the asks ([`crate::coderoom3d`]).
use egui::Color32;
use serde_json::Value;
use crate::coderoom3d::{arc_points, lift, Line, Quad, Scene};
use crate::room::RoomAnswer;
/// A pulse runs its spline in this many UI seconds.
pub const PULSE_S: f64 = 0.5;
/// A glow quad's half width (world units).
pub const GLOW_R: f32 = 4.5;
/// Events asked per window page.
pub const PAGE: u64 = 4096;
// ------------------------------------------------------------------ the answers
/// One traced run (lod `trace_runs`).
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RunRow {
pub run_id: String,
pub target: String,
pub commit: String,
pub source: String,
pub events: u64,
pub threads: u64,
pub duration_ns: u64,
/// Of the run's events, how many landed on functions this room knows.
pub mapped_events: u64,
}
/// A head of the trace answer: where a traced function stands on this map.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TraceHead {
pub card: u64,
/// Its head's place on the plane; the layout the window was asked with.
pub at: [f32; 2],
pub stable_id: String,
pub path: String,
pub symbol: String,
}
/// **Append a later page**: its heads are interned per answer, so each of its
/// events is re-pointed at the held window's heads (grown as needed).
pub fn append_page(held: &mut TraceView, page: TraceView) {
let to_held: Vec<usize> = page
.heads
.into_iter()
.map(|h| match held.heads.iter().position(|x| x.stable_id == h.stable_id) {
Some(i) => i,
None => {
held.heads.push(h);
held.heads.len() - 1
}
})
.collect();
let point = |i: usize| to_held.get(i).copied().unwrap_or(i);
held.events.extend(page.events.into_iter().map(|e| TraceEvent { head: point(e.head), caller: e.caller.map(point), ..e }));
held.unplaced += page.unplaced;
held.more = page.more;
held.next_after_seq = page.next_after_seq;
}
/// One event of the window: `[seq, t_ns, thread, exit, head, caller]`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TraceEvent {
pub seq: u64,
pub t_ns: u64,
pub thread: u64,
pub exit: bool,
pub head: usize,
/// An entry's caller head (the innermost placed fn open on its thread).
pub caller: Option<usize>,
}
/// A landed window of one run.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TraceView {
pub run_id: String,
pub from_ns: u64,
pub events: Vec<TraceEvent>,
pub heads: Vec<TraceHead>,
pub unplaced: u64,
/// The window has more events past its last page.
pub more: bool,
pub next_after_seq: Option<u64>,
/// Each thread's open heads at `from_ns` (outermost first), when asked.
pub stacks: Vec<(u64, Vec<usize>)>,
}
/// The runs of lod `trace_runs`.
pub fn runs_of(a: &RoomAnswer) -> Result<Vec<RunRow>, String> {
if !a.is_measured() {
return Err(format!("[{}] {}", a.state, a.evidence_text()));
}
let s = |v: &Value| v.as_str().unwrap_or("").to_string();
let n = |v: &Value| v.as_u64().unwrap_or(0);
Ok(a.body["runs"]
.as_array()
.into_iter()
.flatten()
.map(|r| RunRow {
run_id: s(&r["run_id"]),
target: s(&r["target"]),
commit: s(&r["commit"]),
source: s(&r["source"]),
events: n(&r["events"]),
threads: n(&r["threads"]),
duration_ns: n(&r["duration_ns"]),
mapped_events: n(&r["mapped_events"]),
})
.collect())
}
/// The window of lod `trace`; pages after the first APPEND (`after_seq` asks).
pub fn trace_of(a: &RoomAnswer) -> Result<TraceView, String> {
if !a.is_measured() {
return Err(format!("[{}] {}", a.state, a.evidence_text()));
}
let b = &a.body;
let s = |v: &Value| v.as_str().unwrap_or("").to_string();
let heads = b["heads"]
.as_array()
.into_iter()
.flatten()
.map(|h| TraceHead {
card: h["card"].as_u64().unwrap_or(0),
at: [h["at"][0].as_f64().unwrap_or(0.0) as f32, h["at"][1].as_f64().unwrap_or(0.0) as f32],
stable_id: s(&h["stable_id"]),
path: s(&h["path"]),
symbol: s(&h["symbol"]),
})
.collect();
let events = b["events"]
.as_array()
.into_iter()
.flatten()
.filter_map(|e| {
Some(TraceEvent {
seq: e[0].as_u64()?,
t_ns: e[1].as_u64()?,
thread: e[2].as_u64()?,
exit: e[3].as_u64()? == 1,
head: e[4].as_u64()? as usize,
caller: e[5].as_u64().map(|c| c as usize),
})
})
.collect();
let stacks = b["stacks"]
.as_array()
.into_iter()
.flatten()
.filter_map(|r| {
Some((r["thread"].as_u64()?, r["heads"].as_array()?.iter().filter_map(|h| h.as_u64().map(|x| x as usize)).collect()))
})
.collect();
Ok(TraceView {
run_id: s(&b["run_id"]),
from_ns: b["from_ns"].as_u64().unwrap_or(0),
events,
heads,
unplaced: b["unplaced"].as_u64().unwrap_or(0),
more: b["more"].as_bool().unwrap_or(false),
next_after_seq: b["next_after_seq"].as_u64(),
stacks,
})
}
// ------------------------------------------------------------------ the replay
/// An open function on some thread: its head glows.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Glow {
pub head: usize,
pub thread: u64,
pub since_ns: u64,
}
/// A call under way: a dot runs the spline caller → callee.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Pulse {
pub from: usize,
pub to: usize,
pub thread: u64,
/// UI seconds when it left.
pub born: f64,
}
/// The replay's state on the tab.
#[derive(Default)]
pub struct TraceUi {
/// The strip is shown (the 🎬 toggle).
pub open: bool,
pub repo: String,
pub runs: Vec<RunRow>,
pub runs_err: Option<String>,
pub picked: Option<RunRow>,
pub view: Option<TraceView>,
pub err: Option<String>,
pub playing: bool,
/// Trace nanoseconds per UI second.
pub speed: f32,
/// The replay clock (trace time).
pub t_ns: f64,
/// Events applied so far (an index into the view's events).
pub applied: usize,
/// Every thread's open heads, in enter order.
pub glows: Vec<Glow>,
pub pulses: Vec<Pulse>,
}
/// A thread's colour: stable per id, never grey.
pub fn thread_color(thread: u64) -> Color32 {
let h = thread.to_le_bytes().iter().fold(2166136261u32, |h, b| (h ^ *b as u32).wrapping_mul(16777619));
// the golden-angle walk keeps near ids far apart in hue
let hue = ((thread as f32 * 137.508) + (h % 360) as f32) % 360.0;
Color32::from(egui::ecolor::Hsva::new(hue / 360.0, 0.8, 0.95, 1.0))
}
/// **Advance the replay** by `dt` UI seconds (at `now` on the UI clock): the
/// clock moves when playing, the events up to it are applied — an enter glows
/// its head and sends a pulse from its caller, an exit ends the glow — and
/// spent pulses go. True = the page is exhausted and more events exist (ask the
/// next page with `next_after_seq`).
pub fn advance(t: &mut TraceUi, dt: f64, now: f64) -> bool {
if t.playing {
t.t_ns += dt * t.speed as f64;
if let Some(r) = &t.picked {
t.t_ns = t.t_ns.min(r.duration_ns as f64);
}
}
t.pulses.retain(|p| now - p.born < PULSE_S);
let Some(v) = &t.view else { return false };
while let Some(e) = v.events.get(t.applied) {
if e.t_ns as f64 > t.t_ns {
break;
}
if e.exit {
if let Some(i) = t.glows.iter().rposition(|g| g.thread == e.thread && g.head == e.head) {
t.glows.remove(i);
}
} else {
t.glows.push(Glow { head: e.head, thread: e.thread, since_ns: e.t_ns });
if let Some(from) = e.caller {
if t.pulses.len() >= 128 {
t.pulses.remove(0);
}
t.pulses.push(Pulse { from, to: e.head, thread: e.thread, born: now });
}
}
t.applied += 1;
}
t.applied == v.events.len() && v.more && v.next_after_seq.is_some()
}
/// **Scrub to `to_ns`**: the applied state is dropped, the clock set; the tab
/// asks a fresh window from here with `stacks: true` and seeds [`land_stacks`].
pub fn scrub(t: &mut TraceUi, to_ns: u64) {
t.t_ns = to_ns as f64;
t.applied = 0;
t.glows.clear();
t.pulses.clear();
t.view = None;
}
/// The scrubbed-to window landed: its stacks become the open glows.
pub fn land_stacks(t: &mut TraceUi) {
t.applied = 0;
t.glows.clear();
t.pulses.clear();
let Some(v) = &t.view else { return };
let at = v.from_ns;
for (thread, heads) in &v.stacks {
t.glows.extend(heads.iter().map(|&head| Glow { head, thread: *thread, since_ns: at }));
}
}
/// **What the replay draws**: a translucent quad in the thread's colour on every
/// glowing head (the innermost of a thread brightest), and each pulse as a short
/// lit segment at its fraction of the arc caller → callee.
pub fn trace_scene(sc: &mut Scene, t: &TraceUi, now: f64) {
let Some(v) = &t.view else { return };
for (i, g) in t.glows.iter().enumerate() {
let Some(h) = v.heads.get(g.head) else { continue };
// the innermost of a thread (its last glow) brightest
let top = !t.glows[i + 1..].iter().any(|x| x.thread == g.thread);
let c = thread_color(g.thread);
sc.quads.push(Quad {
origin: [h.at[0] - GLOW_R, 0.07, h.at[1] - GLOW_R],
u: [GLOW_R * 2.0, 0.0, 0.0],
v: [0.0, 0.0, GLOW_R * 2.0],
// the thread's rgb kept whole: only the alpha says inner/outer
color: Color32::from_rgba_premultiplied(c.r(), c.g(), c.b(), if top { 140 } else { 77 }),
});
}
for p in &t.pulses {
let (Some(f), Some(to)) = (v.heads.get(p.from), v.heads.get(p.to)) else { continue };
let frac = (((now - p.born) / PULSE_S) as f32).clamp(0.0, 1.0);
let (a, b) = pulse_at(f.at, to.at, frac);
sc.lines.push(Line { a, b, width_px: 3.5, color: thread_color(p.thread) });
}
}
/// Where a pulse stands at `frac` of its arc (the scene test's oracle).
pub fn pulse_at(from: [f32; 2], to: [f32; 2], frac: f32) -> ([f32; 3], [f32; 3]) {
let pts = arc_points(lift([from[0], 0.0, from[1]], 0.15), lift([to[0], 0.0, to[1]], 0.15), 24, 0.18);
let k = ((frac * 24.0) as usize).min(23);
(pts[k], pts[k + 1])
}
/// **The strip**: the traced runs of the repo (one row each — target, commit,
/// events, threads, length); a picked run gets ▶/⏸, the speed, the clock, and
/// the SCRUBBER — a bar over the run's length whose drag sets `scrubbed`.
/// `picked` lands a clicked run's id; refusals are said in place.
pub fn strip(ui: &mut egui::Ui, t: &mut TraceUi, picked: &mut Option<String>, scrubbed: &mut Option<u64>) {
ui.horizontal_wrapped(|ui| {
if let Some(e) = &t.runs_err {
ui.label(egui::RichText::new(e.clone()).small().color(Color32::LIGHT_RED));
return;
}
if t.runs.is_empty() {
ui.label(egui::RichText::new("no traced runs of this repo yet — a Runa XRay Trace step files one").small());
return;
}
for r in &t.runs {
let label = format!("▶ {} · {} · {} ev · {} thr · {:.1} ms", r.target, &r.commit.get(..8).unwrap_or(&r.commit), r.events, r.threads, r.duration_ns as f64 / 1e6);
let on = t.picked.as_ref().is_some_and(|p| p.run_id == r.run_id);
if ui.selectable_label(on, label).on_hover_text(format!("{} of {} events land on this map", r.mapped_events, r.events)).clicked() && !on {
*picked = Some(r.run_id.clone());
}
}
});
let Some(r) = t.picked.clone() else { return };
ui.horizontal(|ui| {
if ui.selectable_label(t.playing, if t.playing { "⏸" } else { "▶" }).clicked() {
t.playing = !t.playing;
}
for (label, per_s) in [("slow", r.duration_ns as f32 / 60.0), ("1:10", r.duration_ns as f32 / 10.0), ("fast", r.duration_ns as f32 / 2.0)] {
if ui.selectable_label((t.speed - per_s).abs() < 1.0, label).clicked() {
t.speed = per_s;
}
}
ui.label(egui::RichText::new(format!("{:.2} / {:.2} ms", t.t_ns / 1e6, r.duration_ns as f64 / 1e6)).small().monospace());
if let Some(e) = &t.err {
ui.label(egui::RichText::new(e.clone()).small().color(Color32::LIGHT_RED));
}
if let Some(v) = &t.view {
if v.unplaced > 0 {
ui.label(egui::RichText::new(format!("{} events off this map", v.unplaced)).small());
}
}
});
// the scrubber: the run's whole length; a drag sets the clock, a release asks the stacks
let (resp, p) = ui.allocate_painter(egui::vec2(ui.available_width().max(80.0), 14.0), egui::Sense::click_and_drag());
let rect = resp.rect;
p.rect_filled(rect, 3.0, ui.visuals().extreme_bg_color);
let frac = (t.t_ns / r.duration_ns.max(1) as f64).clamp(0.0, 1.0) as f32;
let x = rect.left() + rect.width() * frac;
p.rect_filled(egui::Rect::from_min_max(rect.min, egui::pos2(x, rect.bottom())), 3.0, ui.visuals().selection.bg_fill);
p.line_segment([egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())], egui::Stroke::new(2.0, ui.visuals().strong_text_color()));
if resp.dragged() || resp.clicked() {
if let Some(at) = resp.interact_pointer_pos() {
let f = ((at.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
t.t_ns = f as f64 * r.duration_ns as f64;
}
}
if resp.drag_stopped() || resp.clicked() {
*scrubbed = Some(t.t_ns as u64);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn answer(body: Value) -> RoomAnswer {
RoomAnswer::from_body("Viz.CodeMap", "canned trace", body)
}
fn canned_runs() -> RoomAnswer {
answer(json!({
"state": "complete", "lod": "trace_runs", "repo": "shop",
"runs": [{
"run_id": "r-1", "repo": "shop", "commit": "bbbb1111", "target": "shop-bin", "source": "xray",
"ts_micros": 1, "chunks": 1, "chunks_filed": 1,
"events": 6, "total_events": 6, "truncated": false,
"threads": 2, "duration_ns": 1_000_000,
"functions": 3, "mapped_functions": 3, "local_functions": 3, "mapped_local_functions": 3,
"mapped_events": 6
}],
"evidence": { "notes": [] }
}))
}
/// Two threads: t0 enters checkout (seq 1), checkout calls total (2) and it
/// returns (3); t1 enters render (4). total's enter carries checkout's head
/// as the caller.
fn canned_window(from_ns: u64, stacks: bool) -> RoomAnswer {
let mut body = json!({
"state": "complete", "lod": "trace", "run_id": "r-1",
"from_ns": from_ns, "to_ns": null,
"events": [
[1u64, 100_000u64, 0u64, 0u8, 0usize, null],
[2u64, 300_000u64, 0u64, 0u8, 1usize, 0usize],
[3u64, 500_000u64, 0u64, 1u8, 1usize, null],
[4u64, 600_000u64, 1u64, 0u8, 2usize, null]
],
"unplaced": 1,
"more": false,
"next_after_seq": 4,
"heads": [
{ "card": 0, "at": [15.0, 28.0], "stable_id": "f shop::checkout().", "path": "shop::checkout", "symbol": "checkout" },
{ "card": 1, "at": [26.5, 28.0], "stable_id": "f shop::total().", "path": "shop::total", "symbol": "total" },
{ "card": 9, "at": [40.0, 60.0], "stable_id": "f web::render().", "path": "web::render", "symbol": "render" }
],
"evidence": { "notes": [] }
});
if stacks {
body["stacks"] = json!([{ "thread": 0, "heads": [0] }]);
}
answer(body)
}
fn picked() -> TraceUi {
let mut t = TraceUi { open: true, repo: "shop".into(), speed: 1_000_000.0, ..TraceUi::default() };
t.runs = runs_of(&canned_runs()).unwrap();
t.picked = Some(t.runs[0].clone());
t.view = Some(trace_of(&canned_window(0, false)).unwrap());
t
}
/// **The answers parse whole**: every run field the strip shows, every
/// event with its caller, every head with its place.
#[test]
fn the_trace_answers_parse_whole() {
let runs = runs_of(&canned_runs()).unwrap();
assert_eq!(runs.len(), 1);
let r = &runs[0];
assert_eq!((r.run_id.as_str(), r.target.as_str(), r.events, r.threads, r.duration_ns, r.mapped_events), ("r-1", "shop-bin", 6, 2, 1_000_000, 6));
let v = trace_of(&canned_window(0, true)).unwrap();
assert_eq!(v.events.len(), 4);
assert_eq!(v.unplaced, 1);
assert_eq!(v.events[1], TraceEvent { seq: 2, t_ns: 300_000, thread: 0, exit: false, head: 1, caller: Some(0) });
assert_eq!(v.heads[2].at, [40.0, 60.0]);
assert_eq!(v.heads[1].path, "shop::total");
assert_eq!(v.stacks, vec![(0, vec![0])]);
assert_eq!(v.next_after_seq, Some(4));
assert!(!v.more);
}
/// **The replay applies what the clock passed**: at 400 µs checkout and
/// total glow on thread 0 (total entered with a pulse from checkout's
/// head); at 550 µs total's exit ended its glow; at 700 µs render glows on
/// thread 1 in ANOTHER colour.
#[test]
fn the_replay_glows_entered_heads_and_pulses_their_calls() {
let mut t = picked();
t.playing = true;
assert!(!advance(&mut t, 0.4, 10.0), "no more pages");
assert_eq!(t.applied, 2, "{:?}", t.glows);
assert_eq!(t.glows.iter().map(|g| (g.head, g.thread)).collect::<Vec<_>>(), vec![(0, 0), (1, 0)]);
assert_eq!(t.pulses.iter().map(|p| (p.from, p.to)).collect::<Vec<_>>(), vec![(0, 1)]);
advance(&mut t, 0.15, 10.1);
assert_eq!(t.glows.iter().map(|g| g.head).collect::<Vec<_>>(), vec![0], "total's exit ends its glow");
advance(&mut t, 0.15, 10.2);
assert_eq!(t.glows.iter().map(|g| (g.head, g.thread)).collect::<Vec<_>>(), vec![(0, 0), (2, 1)]);
assert_ne!(thread_color(0), thread_color(1), "thread colours tell threads apart");
// the pulse dies after its run; paused, the clock stands still
advance(&mut t, 0.0, 10.0 + PULSE_S + 0.1);
assert!(t.pulses.is_empty(), "a pulse lives {PULSE_S} UI seconds");
t.playing = false;
let at = t.t_ns;
advance(&mut t, 1.0, 12.0);
assert_eq!(t.t_ns, at, "paused");
}
/// **The clock never passes the run's end**, and a full page asks the next.
#[test]
fn the_clock_stops_at_the_end_and_a_full_page_asks_more() {
let mut t = picked();
t.playing = true;
advance(&mut t, 100.0, 10.0);
assert_eq!(t.t_ns as u64, t.picked.as_ref().unwrap().duration_ns, "clamped to the run");
t.view.as_mut().unwrap().more = true;
assert!(advance(&mut t, 0.0, 10.1), "a spent page with more asks the next");
}
/// **A scrub drops the applied state and the landing stacks seed the glows.**
#[test]
fn a_scrub_lands_the_open_stacks_of_that_moment() {
let mut t = picked();
t.playing = true;
advance(&mut t, 0.7, 10.0);
assert!(!t.glows.is_empty());
scrub(&mut t, 200_000);
assert_eq!((t.t_ns as u64, t.applied, t.glows.len(), t.view.is_none()), (200_000, 0, 0, true));
t.view = Some(trace_of(&canned_window(200_000, true)).unwrap());
land_stacks(&mut t);
assert_eq!(t.glows.iter().map(|g| (g.head, g.thread)).collect::<Vec<_>>(), vec![(0, 0)], "checkout is open at 200 µs");
}
/// **A later page appends re-pointed**: its own heads list is interned per
/// answer, so its events land pointing at the HELD window's heads, grown by
/// the ones it brings along.
#[test]
fn a_later_page_appends_with_its_heads_repointed() {
let mut held = trace_of(&canned_window(0, false)).unwrap();
held.more = true;
let page = TraceView {
run_id: "r-1".into(),
from_ns: 0,
// this page's head 0 is web::render (held head 2), head 1 is new
events: vec![TraceEvent { seq: 5, t_ns: 700_000, thread: 1, exit: false, head: 1, caller: Some(0) }],
heads: vec![
TraceHead { card: 9, at: [40.0, 60.0], stable_id: "f web::render().".into(), path: "web::render".into(), symbol: "render".into() },
TraceHead { card: 4, at: [44.0, 61.0], stable_id: "f web::row().".into(), path: "web::row".into(), symbol: "row".into() },
],
unplaced: 0,
more: false,
next_after_seq: Some(5),
stacks: Vec::new(),
};
append_page(&mut held, page);
assert_eq!(held.events.len(), 5);
let e = held.events[4];
assert_eq!(held.heads[e.head].path, "web::row", "the new head joined the held list");
assert_eq!(held.heads[e.caller.unwrap()].path, "web::render", "the caller re-pointed at the held head");
assert_eq!(held.heads.len(), 4);
assert!(!held.more);
}
/// **The scene**: every glowing head a translucent quad in its thread's
/// colour at the head's place, the pulse a lit segment ON its arc at its
/// fraction.
#[test]
fn the_scene_holds_the_glow_quads_and_the_pulse_on_its_arc() {
let mut t = picked();
t.playing = true;
advance(&mut t, 0.4, 10.0);
let mut sc = Scene::default();
trace_scene(&mut sc, &t, 10.2);
let quads = sc.quads.len();
assert_eq!(quads, 2, "two glowing heads");
let g = &sc.quads[0];
let head = &t.view.as_ref().unwrap().heads[0];
assert!((g.origin[0] + g.u[0] * 0.5 + g.v[0] * 0.5 - head.at[0]).abs() < 1e-3, "centred on the head");
assert!((g.origin[2] + g.u[2] * 0.5 + g.v[2] * 0.5 - head.at[1]).abs() < 1e-3);
assert!(g.origin[1] > 0.0, "above the plane");
assert_eq!(g.color.r(), thread_color(0).r(), "the thread's colour");
assert!(g.color.a() < 255, "translucent");
let (a, b) = pulse_at(t.view.as_ref().unwrap().heads[0].at, t.view.as_ref().unwrap().heads[1].at, ((10.2 - 10.0) / PULSE_S) as f32);
let seg = sc.lines.iter().find(|l| l.width_px >= 3.0).expect("the pulse segment");
assert!((seg.a[0] - a[0]).abs() < 1e-3 && (seg.a[2] - a[2]).abs() < 1e-3 && (seg.b[0] - b[0]).abs() < 1e-3, "on the arc at its fraction");
assert_eq!(seg.color.r(), thread_color(0).r());
assert!(seg.a[1] > 0.0, "the arc rises above the plane");
}
}