use std::collections::{BTreeMap, VecDeque};
use std::fs::File;
use std::io::{self, Cursor};
use std::path::Path;
use std::sync::Arc;
use egui::{Align, Color32, Layout, Rect, Sense, Stroke, Vec2, pos2};
use puffin::{FrameData, FrameView};
use crate::record::read::{Frame, Reader};
use crate::viz::color::health_color;
use crate::viz::flame::flame_chart;
use crate::viz::layout::PlotLayout;
use crate::viz::metric::{LABEL_COL_W, RowSpec, RowView, STAT_COL_W, plot_series};
use crate::viz::state::{FlameView, PanelConfig};
const BARS_H: f32 = 80.0;
struct Row {
name: String,
unit: String,
values: VecDeque<f64>,
}
pub struct ReplayState {
reader: Reader<io::BufReader<File>>,
n_system: usize,
frame_ms_row: Option<usize>,
capacity: usize,
rows: Vec<Row>,
flames: VecDeque<Option<Arc<FrameData>>>,
len: usize,
total_frames: u64,
flame_enabled: bool,
pinned: Option<usize>,
selection: Option<(usize, usize)>,
flame_view: FlameView,
follow: bool,
}
impl ReplayState {
pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
let reader = Reader::open(path)?;
let capacity = PanelConfig::FRAME_MS.history_capacity;
let n_system = reader.columns().len();
let flame_enabled = reader.flame_enabled();
let frame_ms_row = reader.columns().iter().position(|c| c.name == "frame_ms");
let rows = reader
.columns()
.iter()
.map(|c| Row {
name: c.name.clone(),
unit: c.unit.clone(),
values: VecDeque::with_capacity(capacity),
})
.collect();
Ok(Self {
reader,
n_system,
frame_ms_row,
capacity,
rows,
flames: VecDeque::with_capacity(capacity),
len: 0,
total_frames: 0,
flame_enabled,
pinned: None,
selection: None,
flame_view: FlameView::FULL,
follow: true,
})
}
pub fn show(&mut self, ui: &mut egui::Ui) {
if self.follow && self.pinned.is_none() {
self.poll();
}
self.header(ui);
self.bars(ui);
ui.separator();
self.flame(ui);
ui.separator();
self.metric_rows(ui);
}
fn poll(&mut self) {
loop {
let frame = match self.reader.next_frame() {
Ok(Some(frame)) => frame,
Ok(None) => return,
Err(e) => panic!("read .vor: {e}"),
};
let known = self.rows.len() - self.n_system;
let new_cols: Vec<(String, String)> = self.reader.user_columns()[known..]
.iter()
.map(|c| (c.name.clone(), c.unit.clone()))
.collect();
self.push(frame, new_cols);
}
}
fn push(&mut self, frame: Frame, new_cols: Vec<(String, String)>) {
let Frame {
system,
user,
flame,
} = frame;
let present: BTreeMap<String, f64> = user.into_iter().collect();
for (name, unit) in new_cols {
let first = present.get(&name).copied().unwrap_or(0.0);
self.rows.push(Row {
name,
unit,
values: VecDeque::from(vec![first; self.len]),
});
}
assert_eq!(system.len(), self.n_system);
for (i, value) in system.into_iter().enumerate() {
self.rows[i].values.push_back(value);
}
for row in &mut self.rows[self.n_system..] {
let value = present
.get(&row.name)
.copied()
.or_else(|| row.values.back().copied())
.unwrap_or(0.0);
row.values.push_back(value);
}
self.flames.push_back(flame.map(decode_flame));
self.len += 1;
self.total_frames += 1;
if self.len > self.capacity {
for row in &mut self.rows {
row.values.pop_front();
}
self.flames.pop_front();
self.len -= 1;
}
}
fn header(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
if self.pinned.is_some() {
if ui.small_button("â–¶ resume").clicked() {
self.pinned = None;
self.selection = None;
self.flame_view = FlameView::FULL;
}
ui.label("paused · click a bar to inspect · shift-drag to zoom a range");
} else {
ui.checkbox(&mut self.follow, "follow");
ui.label("click a bar to pause & inspect · shift-drag to zoom a range");
}
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.label(format!("{} frames", self.total_frames));
});
});
}
fn bars(&mut self, ui: &mut egui::Ui) {
let PanelConfig {
history_capacity,
bar_good_threshold,
bar_warn_threshold,
} = PanelConfig::FRAME_MS;
let (rect, resp) = ui.allocate_exact_size(
Vec2::new(ui.available_width(), BARS_H),
Sense::click_and_drag(),
);
let painter = ui.painter_at(rect);
painter.rect_filled(rect, 2.0, Color32::from_gray(18));
let Some(fm) = self.frame_ms_row else {
return;
};
let n = self.rows[fm].values.len();
if n == 0 {
return;
}
let shift = ui.input(|i| i.modifiers.shift);
let selecting = shift && resp.dragged();
let layout = PlotLayout::new(
rect,
n,
if selecting { None } else { self.selection },
history_capacity,
);
let bar_w = (layout.slot_w - 1.0).max(1.0);
let y_max = (bar_warn_threshold * 1.5) as f32;
for (value, color) in [
(bar_good_threshold, Color32::from_rgb(46, 204, 113)),
(bar_warn_threshold, Color32::from_rgb(231, 76, 60)),
] {
let y = rect.bottom() - rect.height() * (value as f32 / y_max);
painter.line_segment([pos2(rect.left(), y), pos2(rect.right(), y)], Stroke::new(1.0, color));
}
for i in layout.range.clone() {
let frame_ms = self.rows[fm].values[i];
let x = layout.base_x + (i - layout.range.start) as f32 * layout.slot_w;
let h = (frame_ms as f32 / y_max).clamp(0.02, 1.0) * rect.height();
let bar = Rect::from_min_max(pos2(x, rect.bottom() - h), pos2(x + bar_w, rect.bottom()));
painter.rect_filled(bar, 0.0, health_color(frame_ms, bar_good_threshold, bar_warn_threshold));
}
if selecting && let Some((lo, hi)) = self.selection {
let x0 = layout.base_x + lo.saturating_sub(layout.range.start) as f32 * layout.slot_w;
let x1 = layout.base_x + (hi + 1).saturating_sub(layout.range.start) as f32 * layout.slot_w;
painter.rect_filled(
Rect::from_min_max(pos2(x0, rect.top()), pos2(x1, rect.bottom())),
0.0,
Color32::from_white_alpha(28),
);
}
if let Some(idx) = self.pinned
&& let Some(x) = layout.cursor_x(idx)
{
painter.line_segment(
[pos2(x, rect.top()), pos2(x, rect.bottom())],
Stroke::new(1.5, Color32::WHITE),
);
}
self.bar_interaction(&resp, &layout, fm, shift);
}
fn bar_interaction(&mut self, resp: &egui::Response, layout: &PlotLayout, fm: usize, shift: bool) {
if shift
&& resp.drag_started()
&& let Some(pos) = resp.interact_pointer_pos()
&& let Some(idx) = layout.hover_to_idx(pos.x)
{
self.selection = Some((idx, idx));
self.pinned = Some(idx);
self.flame_view = FlameView::FULL;
}
if shift
&& resp.dragged()
&& let Some(pos) = resp.interact_pointer_pos()
&& let Some(idx) = layout.hover_to_idx(pos.x)
&& let Some((anchor, _)) = self.selection
{
let (lo, hi) = if anchor <= idx { (anchor, idx) } else { (idx, anchor) };
self.selection = Some((lo, hi));
self.pinned = self.slowest(fm, lo, hi).or(Some(idx));
}
if (resp.clicked() || (!shift && resp.dragged()))
&& let Some(pos) = resp.interact_pointer_pos()
&& let Some(idx) = layout.hover_to_idx(pos.x)
{
self.selection = None;
self.pinned = Some(idx);
self.flame_view = FlameView::FULL;
}
}
fn slowest(&self, fm: usize, lo: usize, hi: usize) -> Option<usize> {
let values = &self.rows[fm].values;
(lo..=hi.min(values.len().saturating_sub(1)))
.max_by(|&a, &b| values[a].total_cmp(&values[b]))
}
fn flame(&mut self, ui: &mut egui::Ui) {
if !self.flame_enabled {
ui.label("(flame frames were not captured in this run)");
return;
}
let idx = self.pinned.unwrap_or(self.len.saturating_sub(1));
match self.flames.get(idx).and_then(Option::as_ref) {
Some(frame) => {
let mut view = FrameView::default();
view.add_frame(frame.clone());
flame_chart::<()>(ui, &view, 1, Some(0), None, &mut self.flame_view);
}
None => {
ui.label("(no flame frame captured at this step)");
}
}
}
fn metric_rows(&mut self, ui: &mut egui::Ui) {
let plot_w = (ui.available_width() - LABEL_COL_W - STAT_COL_W - 16.0).max(120.0);
let view = RowView {
pinned: self.pinned,
selection: self.selection,
plot_w,
history_capacity: self.capacity,
};
for row in &self.rows {
let samples: Vec<f64> = row.values.iter().copied().collect();
let spec = RowSpec {
name: &row.name,
unit: &row.unit,
integer: false,
description: "",
};
plot_series(ui, &spec, &samples, view, None);
}
}
}
fn decode_flame(bytes: Vec<u8>) -> Arc<FrameData> {
let mut cursor = Cursor::new(bytes);
Arc::new(FrameData::read_next(&mut cursor).unwrap().unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::format::{Column, Header, Record, VERSION, write_decl, write_frame, write_header};
#[test]
fn ingest_aligns_rows_and_backfills_late_metrics() {
let mut bytes = Vec::new();
write_header(
&mut bytes,
&Header {
version: VERSION,
flame_enabled: false,
columns: vec![
Column { name: "frame_ms".to_owned(), unit: "ms".to_owned() },
Column { name: "memory_mb".to_owned(), unit: "MB".to_owned() },
],
},
)
.unwrap();
let frame = |sys: Vec<f64>, user: Vec<(u16, f64)>| Record {
system: sys,
user,
flame: None,
};
write_frame(&mut bytes, &frame(vec![16.0, 100.0], vec![])).unwrap();
write_decl(&mut bytes, 2, "loss", "").unwrap();
write_frame(&mut bytes, &frame(vec![17.0, 101.0], vec![(2, 0.5)])).unwrap();
write_frame(&mut bytes, &frame(vec![18.0, 102.0], vec![(2, 0.4)])).unwrap();
let path = std::env::temp_dir().join("vor_replay_ingest_test.vor");
std::fs::write(&path, &bytes).unwrap();
let mut state = ReplayState::open(&path).unwrap();
state.poll();
std::fs::remove_file(&path).unwrap();
assert_eq!(state.total_frames, 3);
assert_eq!(state.len, 3);
assert_eq!(state.rows.len(), 3);
let row = |name: &str| {
let r = state.rows.iter().find(|r| r.name == name).unwrap();
r.values.iter().copied().collect::<Vec<_>>()
};
assert_eq!(row("frame_ms"), vec![16.0, 17.0, 18.0]);
assert_eq!(row("memory_mb"), vec![100.0, 101.0, 102.0]);
assert_eq!(row("loss"), vec![0.5, 0.5, 0.4]);
}
}