use std::path::Path;
use std::process::Command;
use std::time::Instant;
use crate::cold_start_driver::{cached_autumn_web, repoint_autumn_web};
use crate::dev_loop_scaling::{
GeneratedApp, apply_handler_edit, build_scaling_report, emit_scaling_report,
generate_synthetic_app, load_baseline, parse_sizes, run_scaling_dry_run,
};
fn scaffold_app(app: &GeneratedApp, project_dir: &Path, autumn_web: &Path) -> Result<(), String> {
std::fs::write(project_dir.join("Cargo.toml"), &app.cargo_toml)
.map_err(|e| format!("write Cargo.toml: {e}"))?;
repoint_autumn_web(project_dir, autumn_web)?;
for (relpath, content) in &app.files {
let dest = project_dir.join(relpath);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
}
std::fs::write(&dest, content).map_err(|e| format!("write {}: {e}", dest.display()))?;
}
Ok(())
}
fn run_cargo_build(project_dir: &Path, timed: bool) -> Result<u64, String> {
let target_dir = project_dir.join("target");
let mut cmd = Command::new("cargo");
cmd.args(["build", "--target-dir"])
.arg(&target_dir)
.current_dir(project_dir)
.env_remove("CARGO_TARGET_DIR")
.env_remove("CARGO_BUILD_TARGET_DIR")
.env_remove("CARGO_BUILD_TARGET");
let start = Instant::now();
let output = cmd
.output()
.map_err(|e| format!("cargo build spawn failed: {e}"))?;
let elapsed = if timed {
u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX)
} else {
0
};
if !output.status.success() {
return Err(format!(
"cargo build failed (exit: {}):\n{}",
output.status,
String::from_utf8_lossy(&output.stderr)
));
}
Ok(elapsed)
}
fn measure_size(n: usize, runs: u32, autumn_web: &Path) -> Result<Vec<u64>, String> {
let tmp = tempfile::tempdir().map_err(|e| format!("create temp dir: {e}"))?;
let project_dir = tmp.path().to_path_buf();
let app = generate_synthetic_app(n);
scaffold_app(&app, &project_dir, autumn_web)?;
eprintln!(" N={n}: warm build (untimed)…");
run_cargo_build(&project_dir, false)?;
let handlers_path = project_dir.join("src/handlers.rs");
let mut samples = Vec::with_capacity(runs as usize);
for run in 1..=runs {
let handlers_src = std::fs::read_to_string(&handlers_path)
.map_err(|e| format!("read handlers.rs: {e}"))?;
let edited = apply_handler_edit(&handlers_src, run);
std::fs::write(&handlers_path, &edited).map_err(|e| format!("write handlers.rs: {e}"))?;
eprintln!(" N={n} run {run}/{runs}: timed incremental build…");
let ms = run_cargo_build(&project_dir, true)?;
eprintln!(" → {ms} ms");
samples.push(ms);
}
Ok(samples)
}
fn measure_scaling(
sizes: &[usize],
runs: u32,
autumn_web: &Path,
) -> Result<Vec<(usize, Vec<u64>)>, String> {
let mut results = Vec::with_capacity(sizes.len());
let mut failures = Vec::new();
for &n in sizes {
eprintln!("Measuring N={n}…");
match measure_size(n, runs, autumn_web) {
Ok(samples) => results.push((n, samples)),
Err(e) => {
eprintln!(" N={n}: measurement failed, skipping this size: {e}");
failures.push(n);
}
}
}
if results.is_empty() {
return Err(format!(
"every size failed to measure ({} attempted: {failures:?})",
sizes.len()
));
}
if !failures.is_empty() {
eprintln!(
"Warning: {} size(s) failed and were skipped: {failures:?}",
failures.len()
);
}
Ok(results)
}
#[allow(clippy::fn_params_excessive_bools)]
pub fn run_scaling(
sizes_csv: &str,
runs: u32,
output: Option<&str>,
json: bool,
fail_on_regression: bool,
dry_run: bool,
baseline_path: Option<&str>,
) -> i32 {
if dry_run {
return run_scaling_dry_run();
}
if runs == 0 {
eprintln!("Error: --runs must be at least 1 for a scaling measurement.");
return 1;
}
let sizes = match parse_sizes(sizes_csv) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: invalid --sizes: {e}");
return 1;
}
};
let Some(autumn_web) = cached_autumn_web() else {
eprintln!(
"Error: could not locate the workspace autumn-web crate; \
set AUTUMN_BENCH_AUTUMN_WEB_PATH to its directory."
);
return 1;
};
eprintln!(
"autumn dev-loop-bench --scaling: warming and measuring {} size(s), {} run(s) each",
sizes.len(),
runs
);
eprintln!(
"This scaffolds a synthetic Autumn app for each N and runs timed incremental builds — \
expect several minutes total.\n"
);
let measurements = match measure_scaling(&sizes, runs, &autumn_web) {
Ok(m) => m,
Err(e) => {
eprintln!("Error: scaling measurement failed: {e}");
return 1;
}
};
let baseline = match load_baseline(baseline_path) {
Ok(b) => b,
Err(e) => {
eprintln!("Error: {e}");
return 1;
}
};
let report = build_scaling_report(&measurements, baseline.as_ref());
emit_scaling_report(&report, json, output, fail_on_regression)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scaffold_app_writes_cargo_toml_and_sources() {
use crate::dev_loop_scaling::generate_synthetic_app;
let tmp = tempfile::tempdir().expect("tempdir");
let autumn_web = tmp.path().join("autumn-web");
std::fs::create_dir_all(&autumn_web).expect("mkdir");
let app = generate_synthetic_app(2);
scaffold_app(&app, tmp.path(), &autumn_web).expect("scaffold must succeed");
assert!(
tmp.path().join("Cargo.toml").is_file(),
"Cargo.toml must exist"
);
let manifest =
std::fs::read_to_string(tmp.path().join("Cargo.toml")).expect("read manifest");
assert!(
manifest.contains("[patch.crates-io]"),
"patch section must be appended"
);
for (relpath, _) in &app.files {
assert!(
tmp.path().join(relpath).is_file(),
"{relpath} must be written"
);
}
}
#[test]
fn run_scaling_dry_run_returns_zero() {
assert_eq!(
run_scaling("1,25,50,100", 3, None, false, false, true, None),
0
);
}
#[test]
fn run_scaling_rejects_zero_runs() {
let exit = run_scaling("1,100", 0, None, false, false, false, None);
assert_eq!(exit, 1, "zero runs must be rejected");
}
#[test]
fn run_scaling_rejects_invalid_sizes() {
let exit = run_scaling("abc", 1, None, false, false, false, None);
assert_eq!(exit, 1, "invalid sizes must be rejected");
}
#[test]
#[ignore = "compiles a synthetic app from scratch; run with --ignored"]
fn scaling_live_n1_measures_positive_duration() {
let autumn_web = cached_autumn_web().expect("must locate autumn-web");
let samples = measure_size(1, 1, &autumn_web).expect("must succeed");
assert_eq!(samples.len(), 1);
assert!(samples[0] > 0, "measured duration must be positive");
}
}