panasyn 0.1.0

A lightweight GPU-accelerated terminal emulator for macOS and Linux.
use std::time::Instant;

use crate::ansi;
use crate::replay::recorder::*;
use crate::terminal::TerminalState;

/// Extended benchmark results from replaying a recording.
#[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,
        }
    }
}

/// Benchmark replay: measure parse performance without rendering.
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;

    // Try to collect alloc info from profiling module if active
    #[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!();
}

/// Store benchmark results to a JSON file in benchmarks/ directory.
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 {
    // Simple ISO 8601 without chrono dependency
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    let secs = now.as_secs();
    // Format as YYYY-MM-DD
    let days = secs / 86400;
    let y = 1970 + (days as f64 / 365.25) as u64;
    // Approximate month/day (good enough for file naming)
    format!("{}-{:02}-{:02}", y, 1, 1)
}