use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct TimedResult {
pub mean_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub stddev_ms: f64,
pub samples: usize,
pub stdout: String,
pub stderr: String,
pub success: bool,
pub exit_code: i32,
}
impl TimedResult {
pub fn failed() -> Self {
TimedResult {
mean_ms: 0.0,
min_ms: 0.0,
max_ms: 0.0,
stddev_ms: 0.0,
samples: 0,
stdout: String::new(),
stderr: "not run".into(),
success: false,
exit_code: -1,
}
}
}
pub fn mati_bin() -> PathBuf {
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
let candidate = dir.join("mati");
if candidate.exists() {
return candidate;
}
}
}
let debug = PathBuf::from("target/debug/mati");
if debug.exists() {
return debug;
}
PathBuf::from("mati")
}
pub fn run_timed(bin: &Path, args: &[&str], cwd: &Path, n: usize) -> TimedResult {
assert!(n > 0, "run_timed: n must be > 0");
let mut times: Vec<f64> = Vec::with_capacity(n);
let mut last_stdout = String::new();
let mut last_stderr = String::new();
let mut last_code = 0i32;
for _ in 0..n {
let t0 = Instant::now();
let out = Command::new(bin)
.args(args)
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("NO_COLOR", "1")
.output()
.unwrap_or_else(|e| panic!("failed to run {:?}: {}", bin, e));
let elapsed_ms = t0.elapsed().as_secs_f64() * 1_000.0;
times.push(elapsed_ms);
last_stdout = String::from_utf8_lossy(&out.stdout).into_owned();
last_stderr = String::from_utf8_lossy(&out.stderr).into_owned();
last_code = out.status.code().unwrap_or(-1);
}
aggregate(times, last_stdout, last_stderr, last_code)
}
pub fn run_once(bin: &Path, args: &[&str], cwd: &Path) -> TimedResult {
run_timed(bin, args, cwd, 1)
}
pub fn run_parallel_gets(bin: &Path, keys: &[String], cwd: &Path) -> TimedResult {
if keys.is_empty() {
return TimedResult::failed();
}
let t0 = Instant::now();
let mut all_ok = true;
for key in keys {
let out = Command::new(bin)
.args(["get", key])
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("NO_COLOR", "1")
.output()
.unwrap_or_else(|e| panic!("get failed: {}", e));
if !out.status.success() {
all_ok = false;
}
}
let total_ms = t0.elapsed().as_secs_f64() * 1_000.0;
TimedResult {
mean_ms: total_ms / keys.len() as f64, min_ms: total_ms / keys.len() as f64,
max_ms: total_ms / keys.len() as f64,
stddev_ms: 0.0,
samples: keys.len(),
stdout: String::new(),
stderr: String::new(),
success: all_ok,
exit_code: if all_ok { 0 } else { 1 },
}
}
pub fn run_edit_hook(bin: &Path, file_path: &Path, cwd: &Path, n: usize) -> TimedResult {
if !file_path.exists() || !file_path.is_file() {
return TimedResult::failed();
}
let original = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(_) => return TimedResult::failed(),
};
let comment = match file_path.extension().and_then(|e| e.to_str()) {
Some("py") | Some("toml") | Some("yaml") | Some("yml") => "#",
_ => "//",
};
let mut times: Vec<f64> = Vec::with_capacity(n);
let mut last_stdout = String::new();
let mut last_stderr = String::new();
let mut last_code = 0i32;
for i in 0..n {
let modified = format!(
"{}\n{} bench_real edit {}\n",
original.trim_end(),
comment,
i
);
let _ = std::fs::write(file_path, &modified);
let t0 = Instant::now();
let out = Command::new(bin)
.args(["edit-hook", file_path.to_str().unwrap_or("")])
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("NO_COLOR", "1")
.output()
.unwrap_or_else(|e| panic!("failed to run edit-hook: {}", e));
times.push(t0.elapsed().as_secs_f64() * 1_000.0);
last_stdout = String::from_utf8_lossy(&out.stdout).into_owned();
last_stderr = String::from_utf8_lossy(&out.stderr).into_owned();
last_code = out.status.code().unwrap_or(-1);
}
let _ = std::fs::write(file_path, &original);
aggregate(times, last_stdout, last_stderr, last_code)
}
fn aggregate(times: Vec<f64>, stdout: String, stderr: String, exit_code: i32) -> TimedResult {
let n = times.len() as f64;
let mean = times.iter().sum::<f64>() / n;
let min = times.iter().cloned().fold(f64::INFINITY, f64::min);
let max = times.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let variance = times.iter().map(|t| (t - mean).powi(2)).sum::<f64>() / n;
TimedResult {
mean_ms: mean,
min_ms: min,
max_ms: max,
stddev_ms: variance.sqrt(),
samples: times.len(),
stdout,
stderr,
success: exit_code == 0,
exit_code,
}
}