Skip to main content

forge/bench/
mod.rs

1//! Bench subcommand: run simulations across specs and measure throughput.
2
3mod charts;
4mod history;
5pub(crate) mod report;
6mod runner;
7
8mod types;
9
10use anyhow::Result;
11use clap::Args;
12use wowlab_common::output;
13use wowlab_fs::{directory, path::PathBuf};
14
15use crate::run::{FightDurationSeconds, IterationCount, RunParameters, RunParametersError};
16
17const DEFAULT_ITERATIONS: u32 = 500_000;
18const DEFAULT_DURATION: u32 = 300;
19
20#[derive(Args, Debug)]
21pub(crate) struct BenchArgs {
22    /// Spec slugs to benchmark; if omitted, benchmarks all DPS specs with a rotation script.
23    #[arg(value_name = "SPEC")]
24    pub specs: Vec<String>,
25
26    /// Iterations per spec.
27    #[arg(long, short = 'n', default_value_t = DEFAULT_ITERATIONS)]
28    pub iterations: u32,
29
30    /// Encounter duration in seconds.
31    #[arg(long, short = 'd', default_value_t = DEFAULT_DURATION)]
32    pub duration: u32,
33
34    /// Path to the benchmark history JSON file.
35    #[arg(long, default_value = "benchmarks/history.json")]
36    pub history: PathBuf,
37
38    /// Path to write the HTML report.
39    #[arg(long, default_value = "benchmarks/report.html")]
40    pub output: PathBuf,
41
42    /// Skip writing to history (dry run).
43    #[arg(long)]
44    pub no_save: bool,
45
46    /// Only generate the report from existing history (skip running benchmarks).
47    #[arg(long)]
48    pub report_only: bool,
49
50    /// Skip thread scaling benchmarks.
51    #[arg(long)]
52    pub no_scaling: bool,
53}
54
55impl TryFrom<&BenchArgs> for RunParameters {
56    type Error = RunParametersError;
57
58    fn try_from(args: &BenchArgs) -> Result<Self, Self::Error> {
59        let fight_duration = FightDurationSeconds::try_from(args.duration)?;
60        let iterations = IterationCount::try_from(args.iterations)?;
61
62        Ok(Self::new(iterations, fight_duration))
63    }
64}
65
66pub(crate) fn run(args: &BenchArgs) -> Result<()> {
67    let mut hist = history::load(&args.history);
68
69    if !args.report_only {
70        let parameters = RunParameters::try_from(args)?;
71        let specs = runner::discover_specs(&args.specs)?;
72
73        if specs.is_empty() {
74            anyhow::bail!("No matching specs found with rotation scripts");
75        }
76
77        let (results, rt) = runner::run_benchmarks(&specs, parameters)?;
78
79        let scaling = if args.no_scaling {
80            vec![]
81        } else {
82            runner::run_scaling(&rt, &specs, &results, parameters)
83        };
84
85        let bench_run = runner::build_bench_run(results, scaling);
86
87        if args.no_save {
88            hist.runs.push(bench_run);
89        } else {
90            hist.runs.push(bench_run);
91            history::save(&args.history, &hist)?;
92        }
93    }
94
95    if hist.runs.is_empty() {
96        output::detail("No benchmark data to report.");
97
98        return Ok(());
99    }
100
101    report::generate(&args.output, &hist)?;
102    let abs_path = directory::canonicalize(&args.output).unwrap_or_else(|_| args.output.clone());
103    let file_url = format!("file://{}", abs_path.display());
104
105    output::success(&format!("Report written to {file_url}"));
106
107    Ok(())
108}