#![allow(dead_code)]
#![allow(clippy::module_inception, clippy::single_match)]
pub mod benchmark;
pub mod format;
pub mod fuzz;
pub mod headless;
pub mod perf;
pub mod profiling;
pub mod recorder;
pub mod regression;
pub mod replay;
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let subcommand = args.get(1).map(|s| s.as_str());
match subcommand {
Some("replay") => handle_replay(&args),
Some("fuzz") => handle_fuzz(&args),
Some("convert") => handle_convert(&args),
Some("profile") => handle_profile(&args),
Some("bench") => handle_bench(&args),
Some("list") => handle_list(),
Some("help") | None => {
print_usage();
Ok(())
}
_ => {
print_usage();
Err(format!("unknown subcommand: {}", subcommand.unwrap_or("")).into())
}
}
}
fn print_usage() {
println!("Panasyn Replay System");
println!();
println!("USAGE:");
println!(" panasyn replay <path> Replay a recording");
println!(" panasyn replay <path> --benchmark Benchmark mode");
println!(" panasyn replay <path> --headless Headless mode with metrics");
println!(" panasyn replay <path> --machine Machine-readable output");
println!(" panasyn replay <path> --regression Regression testing mode");
println!(" panasyn fuzz [n] Run fuzzer (default: 10000 iterations)");
println!(" panasyn fuzz --categories Run categorized fuzzing");
println!(" panasyn convert <jsonl> <ltr> Convert JSONL to LTR format");
println!(" panasyn profile <path> Profile allocations during replay");
println!(
" panasyn bench [path] Benchmark render, shaping, latency, PTY backpressure"
);
println!(" panasyn list List local replay files, if present");
println!();
println!("FLAGS:");
println!(" --headless Parse-only mode, no window/renderer");
println!(" --benchmark Extended performance measurements");
println!(" --machine Machine-readable key=value output");
println!(" --regression Compare against stored snapshot hashes");
println!(" --store Store benchmark result to benchmarks/");
println!(" --fuzz N Run N fuzz iterations");
println!(" --categories Run categorized fuzzing");
println!(" --compress Write compressed LTR format");
println!(" --frames N Number of offscreen render frames for bench mode");
println!(" --pty-bytes N Bytes emitted by the PTY backpressure benchmark");
println!();
println!("FILE FORMATS:");
println!(" .jsonl JSON Lines (legacy)");
println!(" .ltr Panasyn Replay (binary, recommended)");
println!(" .ltr.zst Panasyn Replay (zstd-compressed)");
println!();
println!("REPLAY FILES:");
println!(" .jsonl/.ltr files are local developer artifacts and are not shipped");
}
fn handle_replay(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let path = args
.get(2)
.ok_or_else(|| "Usage: panasyn replay <path> [flags]".to_string())?;
let cols = 80;
let rows = 24;
let is_benchmark = args.contains(&"--benchmark".to_string());
let is_machine = args.contains(&"--machine".to_string());
let is_headless = args.contains(&"--headless".to_string());
let is_regression = args.contains(&"--regression".to_string());
let store = args.contains(&"--store".to_string());
let fuzz_n: Option<usize> = args
.iter()
.position(|a| a == "--fuzz")
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse().ok());
if let Some(n) = fuzz_n {
let report = fuzz::fuzz_iterations(n, cols as u16, rows as u16);
fuzz::print_fuzz_report(&report);
return Ok(());
}
let events = format::read_any(path)?;
if is_regression {
let result = regression::run_regression(&events, cols as u16, rows as u16);
if is_machine {
println!(
"REGRESSION_RESULT {}",
serde_json::to_string(&serde_json::json!({
"events_processed": result.events_processed,
"grid_hash": format!("{:016x}", result.final_grid_hash),
"cursor_row": result.final_cursor.0,
"cursor_col": result.final_cursor.1,
"scrollback_hash": format!("{:016x}", result.scrollback_hash),
"alt_screen": result.alt_screen_state,
}))?
);
} else {
println!("=== Regression Result ===");
println!("{}", result);
}
return Ok(());
}
if is_benchmark {
let result = benchmark::run_benchmark(&events, cols as u16, rows as u16);
if store {
let label = std::path::Path::new(path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
benchmark::store_benchmark(label, &result)?;
}
if is_machine {
benchmark::print_machine_readable(&result);
} else {
benchmark::print_benchmark(&result);
}
return Ok(());
}
if is_headless {
let result = headless::run_headless(&events, cols as u16, rows as u16);
if is_machine {
headless::print_headless_machine(&result);
} else {
headless::print_headless(&result);
}
return Ok(());
}
let result = replay::run_replay(&events, cols as u16, rows as u16);
if is_machine {
println!(
"REPLAY_RESULT events={} bytes={} parse_ms={:.2} cursor=({},{}) scrollback={}",
result.events_processed,
result.total_bytes,
result.parse_time_ms,
result.final_cursor.0,
result.final_cursor.1,
result.scrollback_lines
);
} else {
println!("=== Replay Summary ===");
println!(" Events: {}", result.events_processed);
println!(
" Bytes: {} ({:.2} KB)",
result.total_bytes,
result.total_bytes as f64 / 1024.0
);
println!(" Parse time: {:.2} ms", result.parse_time_ms);
println!(
" Cursor: ({}, {})",
result.final_cursor.0, result.final_cursor.1
);
println!(" Scrollback: {} lines", result.scrollback_lines);
if !result.snapshots.is_empty() {
println!("\n=== Snapshots ===");
for (label, _snap) in &result.snapshots {
println!(" {}: {} chars", label, _snap.len());
}
}
if !result.errors.is_empty() {
println!("\n=== Errors ===");
for e in &result.errors {
println!(" {}", e);
}
}
}
Ok(())
}
fn handle_fuzz(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let cols = 80;
let rows = 24;
let use_categories = args.contains(&"--categories".to_string());
if use_categories {
let reports = fuzz::fuzz_ansi_categories(cols, rows);
fuzz::print_category_reports(&reports);
let total_crashes: usize = reports.iter().map(|r| r.crashes).sum();
if total_crashes > 0 {
return Err(format!("{total_crashes} fuzz category crashes detected").into());
}
return Ok(());
}
let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(10000);
println!(
"Running {} ANSI fuzz iterations on {}x{} grid...",
n, cols, rows
);
let report = fuzz::fuzz_iterations(n, cols, rows);
fuzz::print_fuzz_report(&report);
if report.crashes > 0 {
return Err(format!("{} fuzz crashes detected", report.crashes).into());
}
Ok(())
}
fn handle_convert(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let jsonl_path = args.get(2).ok_or("Usage: panasyn convert <jsonl> <ltr>")?;
let ltr_path = args.get(3).ok_or("Usage: panasyn convert <jsonl> <ltr>")?;
let compress = args.contains(&"--compress".to_string());
let events = recorder::read_jsonl(jsonl_path)?;
if compress {
format::write_ltr_zst(ltr_path, &events)?;
println!("Converted {} -> {} (compressed)", jsonl_path, ltr_path);
} else {
format::write_ltr(ltr_path, &events)?;
println!("Converted {} -> {}", jsonl_path, ltr_path);
}
Ok(())
}
fn handle_profile(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let path = args.get(2).ok_or("Usage: panasyn profile <path>")?;
let events = format::read_any(path)?;
let cols = 80;
let rows = 24;
let (result, allocs) = crate::replay::profiling::track_allocations(|| {
replay::run_replay(&events, cols as u16, rows as u16)
});
println!("=== Profile Results ===");
println!(" Events processed: {}", result.events_processed);
println!(
" Total bytes: {} ({:.2} KB)",
result.total_bytes,
result.total_bytes as f64 / 1024.0
);
println!(" Parse time: {:.2} ms", result.parse_time_ms);
println!(" Allocations: {}", allocs.alloc_count);
println!(
" Bytes allocated: {} ({:.2} KB)",
allocs.bytes_allocated,
allocs.bytes_allocated as f64 / 1024.0
);
if result.total_bytes > 0 {
println!(
" Bytes/byte parsed: {:.2}",
allocs.bytes_allocated as f64 / result.total_bytes as f64
);
}
println!();
profiling::print_profiling_guide();
Ok(())
}
fn handle_bench(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let path = args
.get(2)
.filter(|arg| !arg.starts_with("--"))
.map(String::as_str);
let frames = flag_value(args, "--frames").unwrap_or(30).max(1);
let pty_bytes = flag_value(args, "--pty-bytes").unwrap_or(512 * 1024).max(1);
let machine = args.contains(&"--machine".to_string());
let (label, events) = if let Some(path) = path {
(path, format::read_any(path)?)
} else {
("generated-smoke", default_bench_events())
};
let result = perf::run_suite(&events, 80, 24, frames, pty_bytes)?;
perf::print_suite(label, &result, machine);
Ok(())
}
fn default_bench_events() -> Vec<recorder::ReplayEvent> {
let mut output = String::new();
for index in 1..=1000 {
output.push_str(&format!("{index:04} Panasyn replay smoke\n"));
}
vec![recorder::ReplayEvent::pty_bytes(output.as_bytes())]
}
fn flag_value(args: &[String], flag: &str) -> Option<usize> {
args.iter()
.position(|arg| arg == flag)
.and_then(|index| args.get(index + 1))
.and_then(|value| value.parse().ok())
}
fn handle_list() -> Result<(), Box<dyn std::error::Error>> {
let recordings_dir = std::path::Path::new("recordings");
if !recordings_dir.exists() {
println!("No recordings directory found.");
return Ok(());
}
println!("=== Available Recordings ===");
let mut total = 0usize;
for entry in std::fs::read_dir(recordings_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file()
&& let Some(ext) = path.extension()
&& (ext == "ltr" || ext == "jsonl" || ext == "zst")
{
let size = entry.metadata()?.len();
let Some(name) = path.file_name().map(|name| name.to_string_lossy()) else {
continue;
};
let size_str = if size > 1024 * 1024 {
format!("{:.1} MB", size as f64 / (1024.0 * 1024.0))
} else if size > 1024 {
format!("{:.1} KB", size as f64 / 1024.0)
} else {
format!("{} B", size)
};
println!(" {:40} {}", name, size_str);
total += 1;
}
}
if total == 0 {
println!(" (no recordings found)");
} else {
println!("\nTotal: {} recordings", total);
}
Ok(())
}