#![allow(clippy::cast_precision_loss, clippy::similar_names)]
mod common;
use common::{
DatasetIntegrityGuard, DiscoveredBinaries, IsolatedDataset, KnownDataset, discover_binaries,
init_test_logging,
};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColdWarmConfig {
pub cold_runs: usize,
pub warm_runs: usize,
pub warmup_runs: usize,
pub drop_caches: bool,
pub cold_run_delay_ms: u64,
}
impl Default for ColdWarmConfig {
fn default() -> Self {
Self {
cold_runs: 3,
warm_runs: 5,
warmup_runs: 2,
drop_caches: std::env::var("BENCH_DROP_CACHES").is_ok(),
cold_run_delay_ms: 100,
}
}
}
impl ColdWarmConfig {
#[must_use]
pub const fn quick() -> Self {
Self {
cold_runs: 2,
warm_runs: 3,
warmup_runs: 1,
drop_caches: false,
cold_run_delay_ms: 50,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Measurement {
pub start_type: String,
pub binary: String,
pub command: String,
pub run_index: usize,
pub duration_ms: f64,
pub exit_code: i32,
pub success: bool,
pub stdout_bytes: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingStats {
pub mean_ms: f64,
pub median_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub stddev_ms: f64,
pub count: usize,
}
impl TimingStats {
#[must_use]
pub fn from_measurements(measurements: &[Measurement]) -> Self {
if measurements.is_empty() {
return Self {
mean_ms: 0.0,
median_ms: 0.0,
min_ms: 0.0,
max_ms: 0.0,
stddev_ms: 0.0,
count: 0,
};
}
let mut values: Vec<f64> = measurements.iter().map(|m| m.duration_ms).collect();
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = values.len();
let mean = values.iter().sum::<f64>() / n as f64;
let median = if n.is_multiple_of(2) {
f64::midpoint(values[n / 2 - 1], values[n / 2])
} else {
values[n / 2]
};
let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
let stddev = variance.sqrt();
Self {
mean_ms: mean,
median_ms: median,
min_ms: values[0],
max_ms: values[n - 1],
stddev_ms: stddev,
count: n,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColdWarmComparison {
pub command: String,
pub cold_stats: TimingStats,
pub warm_stats: TimingStats,
pub cold_warm_ratio: f64,
pub startup_overhead_ms: f64,
pub cold_measurements: Vec<Measurement>,
pub warm_measurements: Vec<Measurement>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinaryComparison {
pub command: String,
pub br_cold_warm: ColdWarmComparison,
pub bd_cold_warm: Option<ColdWarmComparison>,
pub cold_ratio: Option<f64>,
pub warm_ratio: Option<f64>,
pub overhead_ratio: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColdWarmBenchmark {
pub dataset_name: String,
pub issue_count: usize,
pub config: ColdWarmConfig,
pub comparisons: Vec<BinaryComparison>,
pub summary: ColdWarmSummary,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColdWarmSummary {
pub br_avg_cold_warm_ratio: f64,
pub bd_avg_cold_warm_ratio: Option<f64>,
pub br_avg_overhead_ms: f64,
pub bd_avg_overhead_ms: Option<f64>,
pub br_faster_cold_count: usize,
pub br_faster_warm_count: usize,
pub total_commands: usize,
}
fn try_drop_caches() -> bool {
#[cfg(target_os = "linux")]
{
if let Ok(status) = Command::new("sh")
.args(["-c", "sync && echo 3 > /proc/sys/vm/drop_caches"])
.status()
{
return status.success();
}
false
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
fn run_timed(
binary_path: &Path,
args: &[&str],
cwd: &Path,
command_label: &str,
binary_name: &str,
start_type: &str,
run_index: usize,
) -> Measurement {
let start = Instant::now();
let output = Command::new(binary_path)
.args(args)
.current_dir(cwd)
.env("NO_COLOR", "1")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("Failed to run command");
let duration = start.elapsed();
Measurement {
start_type: start_type.to_string(),
binary: binary_name.to_string(),
command: command_label.to_string(),
run_index,
duration_ms: duration.as_secs_f64() * 1000.0,
exit_code: output.status.code().unwrap_or(-1),
success: output.status.success(),
stdout_bytes: output.stdout.len(),
}
}
fn measure_cold(
binary_path: &Path,
args: &[&str],
cwd: &Path,
command_label: &str,
binary_name: &str,
config: &ColdWarmConfig,
) -> Vec<Measurement> {
let mut measurements = Vec::with_capacity(config.cold_runs);
for i in 0..config.cold_runs {
if config.drop_caches {
try_drop_caches();
}
std::thread::sleep(Duration::from_millis(config.cold_run_delay_ms));
let measurement = run_timed(
binary_path,
args,
cwd,
command_label,
binary_name,
"cold",
i,
);
measurements.push(measurement);
}
measurements
}
fn measure_warm(
binary_path: &Path,
args: &[&str],
cwd: &Path,
command_label: &str,
binary_name: &str,
config: &ColdWarmConfig,
) -> Vec<Measurement> {
for _ in 0..config.warmup_runs {
let _ = run_timed(
binary_path,
args,
cwd,
command_label,
binary_name,
"warmup",
0,
);
}
let mut measurements = Vec::with_capacity(config.warm_runs);
for i in 0..config.warm_runs {
let measurement = run_timed(
binary_path,
args,
cwd,
command_label,
binary_name,
"warm",
i,
);
measurements.push(measurement);
}
measurements
}
fn measure_cold_warm(
binary_path: &Path,
args: &[&str],
cwd: &Path,
command_label: &str,
binary_name: &str,
config: &ColdWarmConfig,
) -> ColdWarmComparison {
let cold_measurements =
measure_cold(binary_path, args, cwd, command_label, binary_name, config);
let warm_measurements =
measure_warm(binary_path, args, cwd, command_label, binary_name, config);
let cold_stats = TimingStats::from_measurements(&cold_measurements);
let warm_stats = TimingStats::from_measurements(&warm_measurements);
let cold_warm_ratio = if warm_stats.mean_ms > 0.0 {
cold_stats.mean_ms / warm_stats.mean_ms
} else {
1.0
};
let startup_overhead_ms = cold_stats.mean_ms - warm_stats.mean_ms;
ColdWarmComparison {
command: command_label.to_string(),
cold_stats,
warm_stats,
cold_warm_ratio,
startup_overhead_ms,
cold_measurements,
warm_measurements,
}
}
const BENCHMARK_COMMANDS: &[(&str, &[&str])] = &[
("list", &["list", "--json"]),
("ready", &["ready", "--json"]),
("stats", &["stats", "--json"]),
("sync_status", &["sync", "--status"]),
];
fn benchmark_dataset(
dataset: KnownDataset,
binaries: &DiscoveredBinaries,
config: &ColdWarmConfig,
) -> Result<ColdWarmBenchmark, String> {
let mut guard = DatasetIntegrityGuard::new(dataset)
.map_err(|e| format!("Failed to create integrity guard: {e}"))?;
let before = guard.verify_before();
if !before.passed {
return Err(format!("Source integrity check failed: {}", before.message));
}
let isolated = IsolatedDataset::from_dataset(dataset)
.map_err(|e| format!("Failed to create workspace: {e}"))?;
let dataset_name = dataset.name().to_string();
let issue_count = isolated.metadata.issue_count;
let workspace = isolated.workspace_root();
println!("\nBenchmarking cold/warm: {dataset_name} ({issue_count} issues)");
let bd_path = binaries.bd.as_ref().map(|b| b.path.clone());
let mut comparisons = Vec::new();
for (label, args) in BENCHMARK_COMMANDS {
print!(" {label} ... ");
let br_cold_warm =
measure_cold_warm(&binaries.br.path, args, workspace, label, "br", config);
let bd_cold_warm = bd_path
.as_ref()
.map(|bd| measure_cold_warm(bd, args, workspace, label, "bd", config));
let (cold_ratio, warm_ratio, overhead_ratio) = if let Some(ref bd_cw) = bd_cold_warm {
let cr = if bd_cw.cold_stats.mean_ms > 0.0 {
Some(br_cold_warm.cold_stats.mean_ms / bd_cw.cold_stats.mean_ms)
} else {
None
};
let wr = if bd_cw.warm_stats.mean_ms > 0.0 {
Some(br_cold_warm.warm_stats.mean_ms / bd_cw.warm_stats.mean_ms)
} else {
None
};
let or = if bd_cw.startup_overhead_ms.abs() > 0.1 {
Some(br_cold_warm.startup_overhead_ms / bd_cw.startup_overhead_ms)
} else {
None
};
(cr, wr, or)
} else {
(None, None, None)
};
let comparison = BinaryComparison {
command: label.to_string(),
br_cold_warm,
bd_cold_warm,
cold_ratio,
warm_ratio,
overhead_ratio,
};
println!(
"cold: {:.1}ms, warm: {:.1}ms, overhead: {:.1}ms",
comparison.br_cold_warm.cold_stats.mean_ms,
comparison.br_cold_warm.warm_stats.mean_ms,
comparison.br_cold_warm.startup_overhead_ms
);
comparisons.push(comparison);
}
let after = guard.verify_after();
if !after.passed {
return Err(format!(
"Source dataset was mutated during benchmark: {}",
after.message
));
}
let summary = calculate_summary(&comparisons);
Ok(ColdWarmBenchmark {
dataset_name,
issue_count,
config: config.clone(),
comparisons,
summary,
timestamp: chrono::Utc::now().to_rfc3339(),
})
}
fn calculate_summary(comparisons: &[BinaryComparison]) -> ColdWarmSummary {
let n = comparisons.len();
if n == 0 {
return ColdWarmSummary {
br_avg_cold_warm_ratio: 1.0,
bd_avg_cold_warm_ratio: None,
br_avg_overhead_ms: 0.0,
bd_avg_overhead_ms: None,
br_faster_cold_count: 0,
br_faster_warm_count: 0,
total_commands: 0,
};
}
let br_ratios: Vec<f64> = comparisons
.iter()
.map(|c| c.br_cold_warm.cold_warm_ratio)
.collect();
let br_avg_cold_warm_ratio = br_ratios.iter().sum::<f64>() / n as f64;
let br_overheads: Vec<f64> = comparisons
.iter()
.map(|c| c.br_cold_warm.startup_overhead_ms)
.collect();
let br_avg_overhead_ms = br_overheads.iter().sum::<f64>() / n as f64;
let bd_avg_cold_warm_ratio = {
let bd_ratios: Vec<f64> = comparisons
.iter()
.filter_map(|c| c.bd_cold_warm.as_ref())
.map(|cw| cw.cold_warm_ratio)
.collect();
if bd_ratios.is_empty() {
None
} else {
Some(bd_ratios.iter().sum::<f64>() / bd_ratios.len() as f64)
}
};
let bd_avg_overhead_ms = {
let bd_overheads: Vec<f64> = comparisons
.iter()
.filter_map(|c| c.bd_cold_warm.as_ref())
.map(|cw| cw.startup_overhead_ms)
.collect();
if bd_overheads.is_empty() {
None
} else {
Some(bd_overheads.iter().sum::<f64>() / bd_overheads.len() as f64)
}
};
let br_faster_cold_count = comparisons
.iter()
.filter(|c| c.cold_ratio.is_some_and(|r| r < 1.0))
.count();
let br_faster_warm_count = comparisons
.iter()
.filter(|c| c.warm_ratio.is_some_and(|r| r < 1.0))
.count();
ColdWarmSummary {
br_avg_cold_warm_ratio,
bd_avg_cold_warm_ratio,
br_avg_overhead_ms,
bd_avg_overhead_ms,
br_faster_cold_count,
br_faster_warm_count,
total_commands: n,
}
}
fn print_results(benchmark: &ColdWarmBenchmark) {
let sep = "=".repeat(100);
let dash = "-".repeat(100);
println!("\n{sep}");
println!(
"Cold vs Warm Benchmark: {} ({} issues)",
benchmark.dataset_name, benchmark.issue_count
);
println!("{sep}");
println!(
"\n{:<15} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
"Command", "br cold", "br warm", "br ratio", "bd cold", "bd warm", "bd ratio"
);
println!("{dash}");
for c in &benchmark.comparisons {
let bd_cold = c.bd_cold_warm.as_ref().map_or_else(
|| "-".to_string(),
|cw| format!("{:.1}", cw.cold_stats.mean_ms),
);
let bd_warm = c.bd_cold_warm.as_ref().map_or_else(
|| "-".to_string(),
|cw| format!("{:.1}", cw.warm_stats.mean_ms),
);
let bd_ratio = c.bd_cold_warm.as_ref().map_or_else(
|| "-".to_string(),
|cw| format!("{:.2}x", cw.cold_warm_ratio),
);
println!(
"{:<15} {:>10.1} {:>10.1} {:>10.2}x {:>10} {:>10} {:>10}",
c.command,
c.br_cold_warm.cold_stats.mean_ms,
c.br_cold_warm.warm_stats.mean_ms,
c.br_cold_warm.cold_warm_ratio,
bd_cold,
bd_warm,
bd_ratio
);
}
println!("{dash}");
println!("\nSUMMARY:");
println!(
" br avg cold/warm ratio: {:.2}x, avg overhead: {:.1}ms",
benchmark.summary.br_avg_cold_warm_ratio, benchmark.summary.br_avg_overhead_ms
);
if let (Some(bd_ratio), Some(bd_overhead)) = (
benchmark.summary.bd_avg_cold_warm_ratio,
benchmark.summary.bd_avg_overhead_ms,
) {
println!(" bd avg cold/warm ratio: {bd_ratio:.2}x, avg overhead: {bd_overhead:.1}ms");
println!(
" br faster on cold: {}/{}, br faster on warm: {}/{}",
benchmark.summary.br_faster_cold_count,
benchmark.summary.total_commands,
benchmark.summary.br_faster_warm_count,
benchmark.summary.total_commands
);
}
println!();
}
fn write_results_json(benchmarks: &[ColdWarmBenchmark], output_path: &Path) -> std::io::Result<()> {
let file = File::create(output_path)?;
let writer = BufWriter::new(file);
serde_json::to_writer_pretty(writer, benchmarks)?;
Ok(())
}
#[test]
#[ignore = "run with: cargo test --test bench_cold_warm -- --ignored --nocapture"]
fn cold_warm_full() {
init_test_logging();
println!("\n=== Cold vs Warm Start Benchmark Suite ===\n");
let binaries = match discover_binaries() {
Ok(b) => b,
Err(e) => {
eprintln!("Binary discovery failed: {e}");
panic!("Cannot run benchmarks without br binary");
}
};
println!(
"br: {} ({})",
binaries.br.path.display(),
binaries.br.version
);
if let Some(ref bd) = binaries.bd {
println!("bd: {} ({})", bd.path.display(), bd.version);
} else {
println!("bd: NOT FOUND - will benchmark br only");
}
let config = ColdWarmConfig::default();
println!(
"\nConfig: {} cold runs, {} warm runs, {} warmup runs",
config.cold_runs, config.warm_runs, config.warmup_runs
);
if config.drop_caches {
println!(" Cache dropping: ENABLED");
}
let mut results: Vec<ColdWarmBenchmark> = Vec::new();
for dataset in KnownDataset::all() {
if !dataset.beads_dir().exists() {
println!("\nSkipping {} (not available)", dataset.name());
continue;
}
match benchmark_dataset(*dataset, &binaries, &config) {
Ok(benchmark) => {
print_results(&benchmark);
results.push(benchmark);
}
Err(e) => {
eprintln!("\nFailed to benchmark {}: {}", dataset.name(), e);
}
}
}
if !results.is_empty() {
let output_dir = PathBuf::from("target/benchmark-results");
fs::create_dir_all(&output_dir).expect("create output dir");
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let output_path = output_dir.join(format!("cold_warm_{timestamp}.json"));
if let Err(e) = write_results_json(&results, &output_path) {
eprintln!("Failed to write results: {e}");
} else {
println!("\nResults written to: {}", output_path.display());
}
let latest_path = output_dir.join("cold_warm_latest.json");
let _ = write_results_json(&results, &latest_path);
}
}
#[test]
#[ignore = "run with: cargo test --test bench_cold_warm cold_warm_quick -- --ignored --nocapture"]
fn cold_warm_quick() {
init_test_logging();
println!("\n=== Quick Cold vs Warm Benchmark ===\n");
let binaries = match discover_binaries() {
Ok(b) => b,
Err(e) => {
eprintln!("Binary discovery failed: {e}");
panic!("Cannot run benchmarks without br binary");
}
};
println!("br: {}", binaries.br.path.display());
if let Some(ref bd) = binaries.bd {
println!("bd: {}", bd.path.display());
}
let config = ColdWarmConfig::quick();
match benchmark_dataset(KnownDataset::BeadsRust, &binaries, &config) {
Ok(benchmark) => {
print_results(&benchmark);
}
Err(e) => {
panic!("Failed to benchmark beads_rust: {e}");
}
}
}
#[test]
fn test_timing_stats() {
let measurements = vec![
Measurement {
start_type: "warm".to_string(),
binary: "br".to_string(),
command: "list".to_string(),
run_index: 0,
duration_ms: 10.0,
exit_code: 0,
success: true,
stdout_bytes: 100,
},
Measurement {
start_type: "warm".to_string(),
binary: "br".to_string(),
command: "list".to_string(),
run_index: 1,
duration_ms: 20.0,
exit_code: 0,
success: true,
stdout_bytes: 100,
},
Measurement {
start_type: "warm".to_string(),
binary: "br".to_string(),
command: "list".to_string(),
run_index: 2,
duration_ms: 30.0,
exit_code: 0,
success: true,
stdout_bytes: 100,
},
];
let stats = TimingStats::from_measurements(&measurements);
assert!((stats.mean_ms - 20.0).abs() < 0.01);
assert!((stats.median_ms - 20.0).abs() < 0.01);
assert!((stats.min_ms - 10.0).abs() < 0.01);
assert!((stats.max_ms - 30.0).abs() < 0.01);
assert_eq!(stats.count, 3);
}
#[test]
fn test_cold_warm_comparison_structure() {
let cold = vec![Measurement {
start_type: "cold".to_string(),
binary: "br".to_string(),
command: "list".to_string(),
run_index: 0,
duration_ms: 50.0,
exit_code: 0,
success: true,
stdout_bytes: 100,
}];
let warm = vec![Measurement {
start_type: "warm".to_string(),
binary: "br".to_string(),
command: "list".to_string(),
run_index: 0,
duration_ms: 25.0,
exit_code: 0,
success: true,
stdout_bytes: 100,
}];
let cold_stats = TimingStats::from_measurements(&cold);
let warm_stats = TimingStats::from_measurements(&warm);
let cold_warm_ratio = cold_stats.mean_ms / warm_stats.mean_ms;
let startup_overhead_ms = cold_stats.mean_ms - warm_stats.mean_ms;
assert!((cold_warm_ratio - 2.0).abs() < 0.01);
assert!((startup_overhead_ms - 25.0).abs() < 0.01);
}
#[test]
fn test_summary_calculation() {
let comparisons = vec![BinaryComparison {
command: "list".to_string(),
br_cold_warm: ColdWarmComparison {
command: "list".to_string(),
cold_stats: TimingStats {
mean_ms: 50.0,
median_ms: 50.0,
min_ms: 48.0,
max_ms: 52.0,
stddev_ms: 2.0,
count: 3,
},
warm_stats: TimingStats {
mean_ms: 25.0,
median_ms: 25.0,
min_ms: 24.0,
max_ms: 26.0,
stddev_ms: 1.0,
count: 5,
},
cold_warm_ratio: 2.0,
startup_overhead_ms: 25.0,
cold_measurements: vec![],
warm_measurements: vec![],
},
bd_cold_warm: Some(ColdWarmComparison {
command: "list".to_string(),
cold_stats: TimingStats {
mean_ms: 100.0,
median_ms: 100.0,
min_ms: 98.0,
max_ms: 102.0,
stddev_ms: 2.0,
count: 3,
},
warm_stats: TimingStats {
mean_ms: 50.0,
median_ms: 50.0,
min_ms: 48.0,
max_ms: 52.0,
stddev_ms: 2.0,
count: 5,
},
cold_warm_ratio: 2.0,
startup_overhead_ms: 50.0,
cold_measurements: vec![],
warm_measurements: vec![],
}),
cold_ratio: Some(0.5), warm_ratio: Some(0.5), overhead_ratio: Some(0.5),
}];
let summary = calculate_summary(&comparisons);
assert!((summary.br_avg_cold_warm_ratio - 2.0).abs() < 0.01);
assert!((summary.bd_avg_cold_warm_ratio.unwrap() - 2.0).abs() < 0.01);
assert!((summary.br_avg_overhead_ms - 25.0).abs() < 0.01);
assert!((summary.bd_avg_overhead_ms.unwrap() - 50.0).abs() < 0.01);
assert_eq!(summary.br_faster_cold_count, 1);
assert_eq!(summary.br_faster_warm_count, 1);
assert_eq!(summary.total_commands, 1);
}