w3grs 0.1.0

A Rust port of w3gjs for parsing Warcraft III replay files.
Documentation
use std::{
    env,
    hint::black_box,
    process,
    time::{Duration, Instant},
};

use w3grs::W3GReplay;

fn main() {
    if let Err(error) = run() {
        eprintln!("{error}");
        process::exit(1);
    }
}

fn run() -> Result<(), String> {
    let mut args = env::args().skip(1);
    let replay_path = args.next().ok_or_else(|| {
        "usage: w3grs-bench <replay.w3g|replay.nwg> [iterations] [warmup]".to_string()
    })?;
    let iterations = parse_count(args.next(), 25, "iterations")?;
    let warmup = parse_count(args.next(), 5, "warmup")?;
    let bytes = std::fs::read(&replay_path)
        .map_err(|error| format!("failed to read {replay_path}: {error}"))?;
    let mut parser = W3GReplay::new();

    for _ in 0..warmup {
        let output = parser
            .parse_bytes(black_box(&bytes))
            .map_err(|error| format!("warmup parse failed: {error}"))?;
        black_box(output.players.len());
    }

    let mut samples = Vec::with_capacity(iterations);
    let mut last_players = 0usize;
    for _ in 0..iterations {
        let started = Instant::now();
        let output = parser
            .parse_bytes(black_box(&bytes))
            .map_err(|error| format!("timed parse failed: {error}"))?;
        let elapsed = started.elapsed();
        last_players = output.players.len();
        black_box(&output);
        samples.push(elapsed);
    }

    let stats = Stats::from_samples(&samples);
    println!(
        "{{\"parser\":\"w3grs\",\"iterations\":{iterations},\"warmup\":{warmup},\"totalMs\":{:.6},\"meanMs\":{:.6},\"minMs\":{:.6},\"maxMs\":{:.6},\"lastPlayers\":{last_players}}}",
        stats.total_ms(),
        stats.mean_ms(),
        stats.min_ms(),
        stats.max_ms(),
    );
    Ok(())
}

fn parse_count(value: Option<String>, default: usize, name: &str) -> Result<usize, String> {
    match value {
        Some(value) => value
            .parse::<usize>()
            .map_err(|error| format!("invalid {name} value {value:?}: {error}"))
            .and_then(|value| {
                if value == 0 {
                    Err(format!("{name} must be greater than zero"))
                } else {
                    Ok(value)
                }
            }),
        None => Ok(default),
    }
}

struct Stats {
    total: Duration,
    min: Duration,
    max: Duration,
    count: usize,
}

impl Stats {
    fn from_samples(samples: &[Duration]) -> Self {
        let mut total = Duration::ZERO;
        let mut min = Duration::MAX;
        let mut max = Duration::ZERO;
        for sample in samples {
            total += *sample;
            min = min.min(*sample);
            max = max.max(*sample);
        }
        Self {
            total,
            min,
            max,
            count: samples.len(),
        }
    }

    fn total_ms(&self) -> f64 {
        self.total.as_secs_f64() * 1000.0
    }

    fn mean_ms(&self) -> f64 {
        self.total_ms() / self.count as f64
    }

    fn min_ms(&self) -> f64 {
        self.min.as_secs_f64() * 1000.0
    }

    fn max_ms(&self) -> f64 {
        self.max.as_secs_f64() * 1000.0
    }
}