use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use crate::dev_loop_bench::{
ChangeClass, DbOutcome, budget_for, build_cold_start_report, cargo_executable_path,
emit_report, format_cold_start_budget_table,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColdStartShape {
Hello,
Db,
}
const fn class_for(shape: ColdStartShape) -> ChangeClass {
match shape {
ColdStartShape::Hello => ChangeClass::ColdStartHello,
ColdStartShape::Db => ChangeClass::ColdStartDb,
}
}
pub fn locate_autumn_web() -> Option<PathBuf> {
if let Ok(p) = std::env::var("AUTUMN_BENCH_AUTUMN_WEB_PATH") {
let pb = PathBuf::from(p);
if pb.join("Cargo.toml").is_file() {
return Some(pb);
}
}
let cwd = std::env::current_dir().ok()?;
for ancestor in cwd.ancestors() {
let candidate = ancestor.join("autumn");
let manifest = candidate.join("Cargo.toml");
if manifest.is_file()
&& let Ok(s) = std::fs::read_to_string(&manifest)
&& s.contains("name = \"autumn-web\"")
{
return Some(candidate);
}
}
None
}
pub fn cached_autumn_web() -> Option<PathBuf> {
static AUTUMN_WEB: OnceLock<Option<PathBuf>> = OnceLock::new();
AUTUMN_WEB
.get_or_init(|| locate_autumn_web().and_then(|p| std::fs::canonicalize(p).ok()))
.clone()
}
pub fn repoint_autumn_web(project_dir: &Path, autumn_web: &Path) -> Result<(), String> {
let manifest = project_dir.join("Cargo.toml");
let mut content =
std::fs::read_to_string(&manifest).map_err(|e| format!("read Cargo.toml: {e}"))?;
let patch = format!(
"\n[patch.crates-io]\nautumn-web = {{ path = {:?} }}\n",
autumn_web.display().to_string()
);
content.push_str(&patch);
std::fs::write(&manifest, content).map_err(|e| format!("write Cargo.toml: {e}"))?;
Ok(())
}
fn measure_cold_start_once(shape: ColdStartShape) -> Result<u64, String> {
let autumn_web = cached_autumn_web().ok_or_else(|| {
"could not locate (or canonicalize) the workspace autumn-web crate; \
set AUTUMN_BENCH_AUTUMN_WEB_PATH to its directory"
.to_string()
})?;
let tmp = tempfile::tempdir().map_err(|e| format!("create temp dir: {e}"))?;
let project_name = "coldstart_app";
let opts = match shape {
ColdStartShape::Hello => crate::new::GenerateOptions {
with_daemon: true,
..Default::default()
},
ColdStartShape::Db => crate::new::GenerateOptions {
with_bundled_pg: true,
..Default::default()
},
};
let start = Instant::now();
crate::new::generate_with_quiet(project_name, tmp.path(), opts)
.map_err(|e| format!("autumn new failed: {e}"))?;
let project_dir = tmp.path().join(project_name);
repoint_autumn_web(&project_dir, &autumn_web)?;
let bin = cold_build(&project_dir, project_name)?;
let port = match std::env::var("AUTUMN_BENCH_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
{
Some(p) => p,
None => reserve_free_port()?,
};
let mut cmd = Command::new(&bin);
cmd.current_dir(&project_dir);
for (key, _) in std::env::vars() {
if key.starts_with("AUTUMN_") {
cmd.env_remove(&key);
}
}
cmd.env("AUTUMN_SERVER__HOST", "127.0.0.1")
.env("AUTUMN_SERVER__PORT", port.to_string());
if matches!(shape, ColdStartShape::Db) {
cmd.env(
"AUTUMN_MANAGED_PG_DATA_DIR",
project_dir.join("managed-pg-data"),
);
}
cmd
.stdout(Stdio::null())
.stderr(Stdio::null());
let mut child = cmd
.spawn()
.map_err(|e| format!("failed to start the built server binary: {e}"))?;
let url = format!("http://127.0.0.1:{port}/");
let deadline = Instant::now() + Duration::from_millis(budget_for(class_for(shape)).max_ms * 2);
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(2))
.build()
.map_err(|e| format!("build http client: {e}"))?;
let mut measured = None;
while Instant::now() < deadline {
if let Ok(Some(status)) = child.try_wait() {
return Err(format!(
"scaffolded server exited before serving (exit: {status}); \
port {port} may already be in use"
));
}
if let Ok(resp) = client.get(&url).send()
&& resp.status().is_success()
&& resp.text().is_ok_and(|body| body.contains(project_name))
{
measured = Some(u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX));
break;
}
std::thread::sleep(Duration::from_millis(100));
}
stop_child(&mut child);
measured.ok_or_else(|| "server did not return HTTP 200 before the deadline".to_string())
}
fn cold_build(project_dir: &Path, project_name: &str) -> Result<PathBuf, String> {
let target_dir = project_dir.join("target");
let mut build = Command::new("cargo");
build
.args(["build", "--message-format=json", "--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");
for var in [
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
] {
build.env(var, "");
}
if let Some(triple) = cached_host_target_triple() {
build.arg("--target").arg(triple);
}
let output = build
.output()
.map_err(|e| format!("cargo build spawn failed: {e}"))?;
if !output.status.success() {
return Err(format!(
"cargo build failed for the scaffolded project:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
cargo_executable_path(&output.stdout, project_name).ok_or_else(|| {
"could not determine the built binary path from cargo's JSON output".to_string()
})
}
fn host_target_triple() -> Option<String> {
let out = Command::new("rustc").arg("-vV").output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
text.lines()
.find_map(|line| line.strip_prefix("host: "))
.map(|triple| triple.trim().to_string())
}
fn cached_host_target_triple() -> Option<String> {
static HOST_TRIPLE: OnceLock<Option<String>> = OnceLock::new();
HOST_TRIPLE.get_or_init(host_target_triple).clone()
}
pub fn reserve_free_port() -> Result<u16, String> {
let listener = std::net::TcpListener::bind(("127.0.0.1", 0))
.map_err(|e| format!("failed to reserve a free port: {e}"))?;
let port = listener
.local_addr()
.map_err(|e| format!("failed to read reserved port: {e}"))?
.port();
Ok(port)
}
pub fn stop_child(child: &mut std::process::Child) {
#[cfg(unix)]
{
if let Err(e) = crate::process::signal_terminate(child.id()) {
eprintln!(" Warning: failed to SIGTERM cold-start server: {e}");
}
if crate::process::wait_with_timeout(child, Duration::from_secs(10)).is_err() {
crate::process::force_kill(child.id());
let _ = child.wait();
}
}
#[cfg(not(unix))]
{
let _ = child.kill();
let _ = child.wait();
}
}
fn measure_cold_start(shape: ColdStartShape, runs: u32) -> Result<Vec<u64>, String> {
let mut samples = Vec::with_capacity(runs as usize);
for i in 1..=runs {
eprintln!(" cold-start {shape:?} run {i}/{runs}…");
let ms = measure_cold_start_once(shape)?;
eprintln!(" → {ms} ms");
samples.push(ms);
}
Ok(samples)
}
#[allow(clippy::fn_params_excessive_bools)]
pub fn run_cold_start(
runs: u32,
output: Option<&str>,
json: bool,
fail_on_regression: bool,
dry_run: bool,
include_db: bool,
) -> i32 {
if dry_run {
print!("{}", format_cold_start_budget_table(include_db));
return 0;
}
if runs == 0 {
eprintln!("Error: --runs must be at least 1 for a cold-start measurement.");
return 1;
}
eprintln!(
"autumn dev-loop-bench --cold-start: measuring onboarding cold start ({runs} run(s))"
);
eprintln!(
"This scaffolds throwaway projects and compiles them from a cold target — \
expect minutes per run.\n"
);
let hello_samples = match measure_cold_start(ColdStartShape::Hello, runs) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: cold-start (hello) measurement failed: {e}");
return 1;
}
};
let db = if include_db {
match measure_cold_start(ColdStartShape::Db, runs) {
Ok(s) => DbOutcome::Measured(s),
Err(e) => {
eprintln!("Warning: informational DB cold-start measurement failed: {e}");
DbOutcome::Failed(e)
}
}
} else {
DbOutcome::NotRequested
};
let report = build_cold_start_report(&hello_samples, &db);
emit_report(&report, json, output, fail_on_regression)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn class_for_maps_each_shape_to_its_change_class() {
assert_eq!(
class_for(ColdStartShape::Hello),
ChangeClass::ColdStartHello
);
assert_eq!(class_for(ColdStartShape::Db), ChangeClass::ColdStartDb);
}
#[test]
fn locate_autumn_web_honours_env_override() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join("Cargo.toml"), "name = \"autumn-web\"\n")
.expect("write manifest");
let found = temp_env::with_var(
"AUTUMN_BENCH_AUTUMN_WEB_PATH",
Some(tmp.path().as_os_str()),
locate_autumn_web,
);
assert_eq!(found.as_deref(), Some(tmp.path()));
}
#[test]
fn repoint_autumn_web_appends_patch_section() {
let tmp = tempfile::tempdir().expect("tempdir");
let project = tmp.path();
std::fs::write(
project.join("Cargo.toml"),
"[package]\nname = \"coldstart_app\"\n",
)
.expect("write manifest");
let web = tmp.path().join("autumn");
repoint_autumn_web(project, &web).expect("repoint should succeed");
let content = std::fs::read_to_string(project.join("Cargo.toml")).expect("read back");
assert!(content.contains("[patch.crates-io]"));
assert!(content.contains("autumn-web = { path ="));
}
#[test]
fn reserve_free_port_returns_nonzero() {
let port = reserve_free_port().expect("should reserve a port");
assert!(port > 0);
}
#[test]
fn run_cold_start_dry_run_returns_zero() {
assert_eq!(run_cold_start(1, None, false, false, true, false), 0);
assert_eq!(run_cold_start(1, None, true, false, true, true), 0);
}
#[test]
fn run_cold_start_rejects_zero_runs() {
let exit = run_cold_start(0, None, false, false, false, false);
assert_eq!(exit, 1, "zero runs must be rejected");
}
#[test]
fn run_cold_start_dry_run_ignores_zero_runs() {
assert_eq!(run_cold_start(0, None, false, false, true, false), 0);
}
#[test]
#[ignore = "compiles a throwaway project from a cold target; run with --ignored"]
fn cold_start_live_hello_measures_a_positive_duration() {
let ms = measure_cold_start_once(ColdStartShape::Hello)
.expect("live cold-start measurement should succeed");
assert!(ms > 0, "measured cold-start duration must be positive");
}
}