pub mod report;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use color_eyre::eyre::{Result, WrapErr as _, bail};
use waterui_preview_protocol::bench::{
BENCH_MAX_CLIP_LAYERS_ENV, BENCH_MAX_GPU_SURFACE_LAYERS_ENV, BENCH_MAX_MEAN_US_ENV,
BENCH_MAX_P95_US_ENV, BENCH_MAX_REBUILD_RATIO_ENV, BENCH_MAX_SCENE_LAYERS_ENV,
BENCH_REPETITIONS_ENV, BENCH_REPORT_DIR_ENV, BENCH_SAMPLES_ENV, BENCH_WARMUPS_ENV,
BenchBudgets, BenchReport, BenchRunConfig,
};
const BENCH_TEST_PREFIX: &str = "waterui_bench_";
fn absolute_path(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path.to_path_buf());
}
let cwd = std::env::current_dir().wrap_err("failed to resolve the current directory")?;
Ok(cwd.join(path))
}
#[derive(Debug, Clone)]
pub struct BenchRunOptions {
pub path: PathBuf,
pub filter: Option<String>,
pub config: BenchRunConfig,
pub report_dir: Option<PathBuf>,
pub budget_caps: BenchBudgets,
}
#[derive(Debug)]
pub struct BenchSuiteRun {
pub reports: Vec<BenchReport>,
pub nextest_succeeded: bool,
}
pub async fn run_bench_suite(options: BenchRunOptions) -> Result<BenchSuiteRun> {
ensure_nextest_installed().await?;
let _temp_dir;
let report_dir = if let Some(dir) = &options.report_dir {
let dir = absolute_path(dir)?;
smol::fs::create_dir_all(&dir)
.await
.wrap_err_with(|| format!("failed to create report directory {}", dir.display()))?;
clear_stale_reports(&dir).await?;
dir
} else {
let temp_dir = tempfile::Builder::new()
.prefix("waterui-bench-")
.tempdir()
.wrap_err("failed to create temporary bench report directory")?;
let path = temp_dir.path().to_path_buf();
_temp_dir = temp_dir;
path
};
let mut command = smol::process::Command::new("cargo");
command
.arg("nextest")
.arg("run")
.arg("--no-fail-fast")
.arg("-E")
.arg(nextest_filter_expression(options.filter.as_deref()))
.current_dir(&options.path)
.env(BENCH_WARMUPS_ENV, options.config.warmups.to_string())
.env(BENCH_SAMPLES_ENV, options.config.samples.to_string())
.env(
BENCH_REPETITIONS_ENV,
options.config.repetitions.to_string(),
)
.env(BENCH_REPORT_DIR_ENV, &report_dir)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.kill_on_drop(true);
apply_budget_cap_envs(&mut command, options.budget_caps);
let status = command
.status()
.await
.wrap_err("failed to run `cargo nextest`")?;
let reports = collect_reports(&report_dir).await?;
if reports.is_empty() && status.success() {
bail!(
"no `#[waterui::bench]` reports were produced under {}; \
the crate has no benches{}",
options.path.display(),
options
.filter
.as_deref()
.map(|filter| format!(" matching `{filter}`"))
.unwrap_or_default()
);
}
Ok(BenchSuiteRun {
reports,
nextest_succeeded: status.success(),
})
}
async fn ensure_nextest_installed() -> Result<()> {
let probe = smol::process::Command::new("cargo")
.arg("nextest")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true)
.status()
.await;
match probe {
Ok(status) if status.success() => Ok(()),
_ => bail!(
"`water bench` requires cargo-nextest. Install it with: cargo install cargo-nextest --locked"
),
}
}
fn nextest_filter_expression(filter: Option<&str>) -> String {
filter.map_or_else(
|| format!("test({BENCH_TEST_PREFIX})"),
|filter| format!("test({BENCH_TEST_PREFIX}) & test({filter})"),
)
}
fn apply_budget_cap_envs(command: &mut smol::process::Command, caps: BenchBudgets) {
let mut set = |name: &str, value: Option<String>| {
if let Some(value) = value {
command.env(name, value);
}
};
set(BENCH_MAX_P95_US_ENV, caps.max_p95_us.map(|v| v.to_string()));
set(
BENCH_MAX_MEAN_US_ENV,
caps.max_mean_us.map(|v| v.to_string()),
);
set(
BENCH_MAX_REBUILD_RATIO_ENV,
caps.max_rebuild_ratio.map(|v| v.to_string()),
);
set(
BENCH_MAX_SCENE_LAYERS_ENV,
caps.max_scene_layers.map(|v| v.to_string()),
);
set(
BENCH_MAX_GPU_SURFACE_LAYERS_ENV,
caps.max_gpu_surface_layers.map(|v| v.to_string()),
);
set(
BENCH_MAX_CLIP_LAYERS_ENV,
caps.max_clip_layers.map(|v| v.to_string()),
);
}
async fn clear_stale_reports(dir: &Path) -> Result<()> {
for path in report_files(dir)? {
smol::fs::remove_file(&path)
.await
.wrap_err_with(|| format!("failed to remove stale bench report {}", path.display()))?;
}
Ok(())
}
async fn collect_reports(dir: &Path) -> Result<Vec<BenchReport>> {
let mut reports = Vec::new();
for path in report_files(dir)? {
let raw = smol::fs::read(&path)
.await
.wrap_err_with(|| format!("failed to read bench report {}", path.display()))?;
let report: BenchReport = serde_json::from_slice(&raw)
.wrap_err_with(|| format!("failed to parse bench report {}", path.display()))?;
reports.push(report);
}
reports.sort_by(|a, b| {
(a.crate_name.as_str(), a.bench_name.as_str())
.cmp(&(b.crate_name.as_str(), b.bench_name.as_str()))
});
Ok(reports)
}
fn report_files(dir: &Path) -> Result<Vec<PathBuf>> {
let entries = std::fs::read_dir(dir)
.wrap_err_with(|| format!("failed to list bench report directory {}", dir.display()))?;
let mut files = Vec::new();
for entry in entries {
let path = entry?.path();
if path
.extension()
.is_some_and(|extension| extension == "json")
{
files.push(path);
}
}
Ok(files)
}
#[cfg(test)]
mod tests {
use super::nextest_filter_expression;
#[test]
fn bench_filter_selects_prefix_only_by_default() {
assert_eq!(nextest_filter_expression(None), "test(waterui_bench_)");
}
#[test]
fn bench_filter_intersects_user_substring() {
assert_eq!(
nextest_filter_expression(Some("scroll")),
"test(waterui_bench_) & test(scroll)"
);
}
}