use crate::linter::lint_shell;
use crate::{Error, Result};
use chrono::Utc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::Instant;
use sysinfo::System;
const VERSION: &str = "1.0.0";
const DEFAULT_WARMUP: usize = 3;
const DEFAULT_ITERATIONS: usize = 10;
#[derive(Debug, Clone)]
pub struct BenchOptions {
pub scripts: Vec<PathBuf>,
pub warmup: usize,
pub iterations: usize,
pub output: Option<PathBuf>,
pub strict: bool,
pub verify_determinism: bool,
pub show_raw: bool,
pub quiet: bool,
pub measure_memory: bool,
pub csv: bool,
pub no_color: bool,
}
impl BenchOptions {
pub fn new(scripts: Vec<PathBuf>) -> Self {
Self {
scripts,
warmup: DEFAULT_WARMUP,
iterations: DEFAULT_ITERATIONS,
output: None,
strict: false,
verify_determinism: false,
show_raw: false,
quiet: false,
measure_memory: false,
csv: false,
no_color: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Environment {
pub cpu: String,
pub ram: String,
pub os: String,
pub hostname: String,
pub bashrs_version: String,
}
impl Environment {
pub fn capture() -> Self {
let mut sys = System::new_all();
sys.refresh_all();
let cpu = sys
.cpus()
.first()
.map_or_else(|| "unknown".to_string(), |cpu| cpu.brand().to_string());
let ram = format!("{}GB", sys.total_memory() / 1024 / 1024 / 1024);
let os = format!(
"{} {}",
System::name().unwrap_or_else(|| "unknown".to_string()),
System::os_version().unwrap_or_else(|| "unknown".to_string())
);
let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());
let bashrs_version = env!("CARGO_PKG_VERSION").to_string();
Self {
cpu,
ram,
os,
hostname,
bashrs_version,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MemoryStatistics {
pub mean_kb: f64,
pub median_kb: f64,
pub min_kb: f64,
pub max_kb: f64,
pub peak_kb: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Statistics {
pub mean_ms: f64,
pub median_ms: f64,
pub stddev_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub variance_ms: f64,
pub mad_ms: f64,
pub geometric_mean_ms: f64,
pub harmonic_mean_ms: f64,
pub outlier_indices: Vec<usize>,
pub memory: Option<MemoryStatistics>,
}
impl Statistics {
pub fn calculate(results: &[f64]) -> Self {
Self::calculate_with_memory(results, None)
}
pub fn calculate_with_memory(results: &[f64], memory_results: Option<&[f64]>) -> Self {
let mean = calculate_mean(results);
let median = calculate_median(results);
let variance = calculate_variance(results, mean);
let stddev = variance.sqrt();
let min = results.iter().copied().fold(f64::INFINITY, f64::min);
let max = results.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let mad = calculate_mad(results);
let outlier_indices = detect_outliers(results, 3.0);
let geometric_mean = calculate_geometric_mean(results);
let harmonic_mean = calculate_harmonic_mean(results);
let memory = memory_results.map(|mem_results| {
let mean_kb = calculate_mean(mem_results);
let median_kb = calculate_median(mem_results);
let min_kb = mem_results.iter().copied().fold(f64::INFINITY, f64::min);
let max_kb = mem_results
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
let peak_kb = max_kb;
MemoryStatistics {
mean_kb,
median_kb,
min_kb,
max_kb,
peak_kb,
}
});
Self {
mean_ms: mean,
median_ms: median,
stddev_ms: stddev,
min_ms: min,
max_ms: max,
variance_ms: variance,
mad_ms: mad,
geometric_mean_ms: geometric_mean,
harmonic_mean_ms: harmonic_mean,
outlier_indices,
memory,
}
}
}
impl MemoryStatistics {
pub fn calculate(memory_kb: &[f64]) -> Self {
let mean_kb = calculate_mean(memory_kb);
let median_kb = calculate_median(memory_kb);
let min_kb = memory_kb.iter().copied().fold(f64::INFINITY, f64::min);
let max_kb = memory_kb.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let peak_kb = max_kb;
Self {
mean_kb,
median_kb,
min_kb,
max_kb,
peak_kb,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Quality {
pub lint_passed: bool,
pub determinism_score: f64,
pub output_identical: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BenchmarkResult {
pub script: String,
pub iterations: usize,
pub warmup: usize,
pub statistics: Statistics,
pub raw_results_ms: Vec<f64>,
pub quality: Quality,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BenchmarkOutput {
pub version: String,
pub timestamp: String,
pub environment: Environment,
pub benchmarks: Vec<BenchmarkResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ComparisonResult {
pub speedup: f64,
pub t_statistic: f64,
pub p_value: f64,
pub is_significant: bool,
}
impl ComparisonResult {
pub fn from_statistics(baseline: &Statistics, current: &Statistics) -> Self {
let baseline_samples = vec![baseline.mean_ms; 10]; let current_samples = vec![current.mean_ms; 10];
compare_benchmarks(&baseline_samples, ¤t_samples)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RegressionResult {
pub is_regression: bool,
pub speedup: f64,
pub is_statistically_significant: bool,
pub change_percent: f64,
}
pub fn bench_command(options: BenchOptions) -> Result<()> {
validate_options(&options)?;
let environment = Environment::capture();
let mut results = Vec::new();
for script in &options.scripts {
let result = benchmark_single_script(script, &options)?;
results.push(result);
}
let output = BenchmarkOutput {
version: VERSION.to_string(),
timestamp: Utc::now().to_rfc3339(),
environment,
benchmarks: results.clone(),
};
if options.csv {
display_csv_results(&results)?;
} else if !options.quiet {
display_results(&results, &output.environment, &options)?;
}
if let Some(output_path) = &options.output {
write_json_output(&output, output_path)?;
}
Ok(())
}
fn validate_options(options: &BenchOptions) -> Result<()> {
if options.scripts.is_empty() {
return Err(Error::Validation(
"No scripts provided for benchmarking".to_string(),
));
}
if options.iterations == 0 {
return Err(Error::Validation(
"Iterations must be at least 1".to_string(),
));
}
for script in &options.scripts {
if !script.exists() {
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Script not found: {}", script.display()),
)));
}
}
Ok(())
}
fn run_warmup(script: &Path, options: &BenchOptions) -> Result<()> {
if !options.quiet {
println!("\n🔥 Warmup ({} iterations)...", options.warmup);
}
for i in 1..=options.warmup {
let time_ms = execute_and_time(script)?;
if !options.quiet {
println!(" ✓ Iteration {}: {:.2}ms", i, time_ms);
}
}
Ok(())
}
fn run_measured_iterations(script: &Path, options: &BenchOptions) -> Result<(Vec<f64>, Vec<f64>)> {
if !options.quiet {
let mem_str = if options.measure_memory {
" + memory"
} else {
""
};
println!(
"\n⏱️ Measuring ({} iterations{})...",
options.iterations, mem_str
);
}
let mut results = Vec::new();
let mut memory_results = Vec::new();
for i in 1..=options.iterations {
let (time_ms, memory_kb) = if options.measure_memory {
execute_and_time_with_memory(script)?
} else {
(execute_and_time(script)?, 0.0)
};
results.push(time_ms);
if options.measure_memory {
memory_results.push(memory_kb);
}
if !options.quiet {
if options.measure_memory {
println!(" ✓ Iteration {}: {:.2}ms, {:.2} KB", i, time_ms, memory_kb);
} else {
println!(" ✓ Iteration {}: {:.2}ms", i, time_ms);
}
}
}
Ok((results, memory_results))
}
fn benchmark_single_script(script: &Path, options: &BenchOptions) -> Result<BenchmarkResult> {
if !options.quiet {
println!("📊 Benchmarking: {}", script.display());
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}
let quality = if options.strict || options.verify_determinism {
run_quality_gates(script, options)?
} else {
Quality {
lint_passed: true,
determinism_score: 1.0,
output_identical: true,
}
};
run_warmup(script, options)?;
let (results, memory_results) = run_measured_iterations(script, options)?;
let statistics = if options.measure_memory {
Statistics::calculate_with_memory(&results, Some(&memory_results))
} else {
Statistics::calculate(&results)
};
Ok(BenchmarkResult {
script: script.to_string_lossy().to_string(),
iterations: options.iterations,
warmup: options.warmup,
statistics,
raw_results_ms: results,
quality,
})
}
fn execute_and_time(script: &Path) -> Result<f64> {
let start = Instant::now();
Command::new("bash")
.arg(script)
.output()
.map_err(Error::Io)?;
let elapsed = start.elapsed();
Ok(elapsed.as_secs_f64() * 1000.0)
}
fn execute_and_time_with_memory(script: &Path) -> Result<(f64, f64)> {
let start = Instant::now();
let output = Command::new("/usr/bin/time")
.arg("-f")
.arg("%M")
.arg("bash")
.arg(script)
.output()
.map_err(Error::Io)?;
let elapsed = start.elapsed();
let time_ms = elapsed.as_secs_f64() * 1000.0;
let stderr = String::from_utf8_lossy(&output.stderr);
let memory_kb = stderr
.lines()
.last()
.and_then(|line| line.trim().parse::<f64>().ok())
.unwrap_or(0.0);
Ok((time_ms, memory_kb))
}
fn run_quality_gates(script: &Path, options: &BenchOptions) -> Result<Quality> {
let mut quality = Quality {
lint_passed: true,
determinism_score: 1.0,
output_identical: true,
};
if options.strict {
let source = fs::read_to_string(script).map_err(Error::Io)?;
let lint_result = lint_shell(&source);
}
}
include!("bench_part2_incl2.rs");