use std::time::Instant;
use crate::ansi;
use crate::replay::recorder::*;
use crate::terminal::TerminalState;
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
pub events: u64,
pub total_bytes: u64,
pub total_parse_ms: f64,
pub wall_time_ms: f64,
pub parse_throughput_mbps: f64,
pub peak_dirty_rows: usize,
pub final_grid_rows: usize,
pub final_grid_cols: usize,
pub final_scrollback: usize,
pub total_frames_estimate: u64,
pub max_frame_spike_ms: f64,
pub avg_frame_ms: f64,
pub avg_parse_per_event_ms: f64,
pub alloc_count: u64,
pub bytes_allocated: u64,
pub memory_growth: i64,
}
impl Default for BenchmarkResult {
fn default() -> Self {
Self {
events: 0,
total_bytes: 0,
total_parse_ms: 0.0,
wall_time_ms: 0.0,
parse_throughput_mbps: 0.0,
peak_dirty_rows: 0,
final_grid_rows: 0,
final_grid_cols: 0,
final_scrollback: 0,
total_frames_estimate: 0,
max_frame_spike_ms: 0.0,
avg_frame_ms: 0.0,
avg_parse_per_event_ms: 0.0,
alloc_count: 0,
bytes_allocated: 0,
memory_growth: 0,
}
}
}
pub fn run_benchmark(events: &[ReplayEvent], cols: u16, rows: u16) -> BenchmarkResult {
let mut terminal = TerminalState::new(cols as usize, rows as usize, 10_000);
let mut parser = ansi::Parser::new();
let mut result = BenchmarkResult::default();
let start = Instant::now();
let mut max_spike = 0.0;
let mut total_spikes = 0u64;
let mut spike_count = 0u64;
for event in events {
if let ReplayEventKind::PtyBytes(data) = &event.kind {
let t0 = Instant::now();
parser.advance(data, &mut terminal);
let dt = t0.elapsed().as_secs_f64() * 1000.0;
result.total_parse_ms += dt;
result.events += 1;
result.total_bytes += data.len() as u64;
let dc = terminal.grid.dirty_count();
if dc > result.peak_dirty_rows {
result.peak_dirty_rows = dc;
}
if dt > max_spike {
max_spike = dt;
}
total_spikes += (dt * 1000.0) as u64;
spike_count += 1;
}
}
let elapsed = start.elapsed().as_secs_f64();
result.wall_time_ms = elapsed * 1000.0;
let seconds = (result.total_parse_ms / 1000.0).max(f64::EPSILON);
result.parse_throughput_mbps = (result.total_bytes as f64 / seconds) / (1024.0 * 1024.0);
result.final_grid_rows = terminal.grid.rows();
result.final_grid_cols = terminal.grid.cols();
result.final_scrollback = terminal.scrollback.len();
result.max_frame_spike_ms = max_spike;
result.avg_frame_ms = if spike_count > 0 {
total_spikes as f64 / spike_count as f64 / 1000.0
} else {
0.0
};
result.avg_parse_per_event_ms = if result.events > 0 {
result.total_parse_ms / result.events as f64
} else {
0.0
};
result.total_frames_estimate = 0;
#[cfg(feature = "profiling")]
{
result.alloc_count =
crate::replay::profiling::CountingAllocator::<std::alloc::System>::alloc_count();
result.bytes_allocated =
crate::replay::profiling::CountingAllocator::<std::alloc::System>::bytes_allocated();
result.memory_growth =
crate::replay::profiling::CountingAllocator::<std::alloc::System>::bytes_current();
}
result
}
pub fn print_benchmark(r: &BenchmarkResult) {
println!("=== Benchmark Results ===");
println!(" Events processed: {}", r.events);
println!(
" Total bytes: {} ({:.2} KB)",
r.total_bytes,
r.total_bytes as f64 / 1024.0
);
println!(
" Parse time: {:.2} ms total, {:.4} ms/event avg",
r.total_parse_ms, r.avg_parse_per_event_ms
);
println!(" Wall time: {:.2} ms", r.wall_time_ms);
println!(" Throughput: {:.2} MB/s", r.parse_throughput_mbps);
println!(" Peak dirty rows: {}", r.peak_dirty_rows);
println!(" Max frame spike: {:.4} ms", r.max_frame_spike_ms);
println!(" Avg parse/event: {:.4} ms", r.avg_frame_ms);
println!(
" Final grid: {}x{}",
r.final_grid_cols, r.final_grid_rows
);
println!(" Scrollback lines: {}", r.final_scrollback);
if r.alloc_count > 0 {
println!(" Allocations: {}", r.alloc_count);
println!(" Bytes allocated: {}", r.bytes_allocated);
println!(" Memory growth: {} bytes", r.memory_growth);
}
}
pub fn print_machine_readable(r: &BenchmarkResult) {
print!(
"BENCHMARK_RESULT events={} bytes={} parse_ms={:.4} throughput_mbps={:.4} \
peak_dirty={} grid={}x{} scrollback={} max_spike_ms={:.4} avg_parse_ms={:.4} wall_ms={:.4}",
r.events,
r.total_bytes,
r.total_parse_ms,
r.parse_throughput_mbps,
r.peak_dirty_rows,
r.final_grid_cols,
r.final_grid_rows,
r.final_scrollback,
r.max_frame_spike_ms,
r.avg_parse_per_event_ms,
r.wall_time_ms,
);
if r.alloc_count > 0 {
print!(
" alloc_count={} bytes_allocated={} memory_growth={}",
r.alloc_count, r.bytes_allocated, r.memory_growth
);
}
println!();
}
pub fn store_benchmark(
label: &str,
result: &BenchmarkResult,
) -> Result<(), Box<dyn std::error::Error>> {
use std::io::Write;
std::fs::create_dir_all("benchmarks")?;
let timestamp = chrono_now();
let path = format!("benchmarks/{}.json", timestamp);
let json = serde_json::json!({
"timestamp": timestamp,
"label": label,
"events": result.events,
"total_bytes": result.total_bytes,
"parse_ms": result.total_parse_ms,
"throughput_mbps": result.parse_throughput_mbps,
"wall_time_ms": result.wall_time_ms,
"peak_dirty_rows": result.peak_dirty_rows,
"grid": format!("{}x{}", result.final_grid_cols, result.final_grid_rows),
"scrollback_lines": result.final_scrollback,
"max_frame_spike_ms": result.max_frame_spike_ms,
"avg_parse_per_event_ms": result.avg_parse_per_event_ms,
});
let file = std::fs::File::create(&path)?;
let mut writer = std::io::BufWriter::new(file);
writer.write_all(serde_json::to_string_pretty(&json)?.as_bytes())?;
println!("Stored benchmark result to {}", path);
Ok(())
}
fn chrono_now() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let days = secs / 86400;
let y = 1970 + (days as f64 / 365.25) as u64;
format!("{}-{:02}-{:02}", y, 1, 1)
}