use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Instant;
use crate::invoke::{ItemMetrics, Run};
pub fn measure(pkg: &str, matrix: &str, target_dir: Option<&Path>) -> Result<Run, String> {
measure_in(pkg, matrix, None, target_dir)
}
pub fn measure_in(
pkg: &str,
matrix: &str,
dir: Option<&Path>,
target_dir: Option<&Path>,
) -> Result<Run, String> {
warm_dependencies(pkg, dir, target_dir)?;
let mut run = Run::default();
for combo in matrix.split(';').map(str::trim).filter(|c| !c.is_empty()) {
let clean = cargo(&["clean", "--release", "-p", pkg], dir, target_dir)
.status()
.map_err(|e| e.to_string())?;
if !clean.success() {
return Err(format!("cargo clean failed for {pkg}"));
}
let mut cmd = cargo(
&[
"build",
"--release",
"-p",
pkg,
"--message-format",
"json-render-diagnostics",
],
dir,
target_dir,
);
match combo {
"default" => {}
"none" => {
cmd.arg("--no-default-features");
}
features => {
cmd.args(["--features", features]);
}
}
cmd.stdout(Stdio::piped());
let t0 = Instant::now();
let out = cmd.output().map_err(|e| e.to_string())?;
let build_ms = t0.elapsed().as_millis() as u64;
if !out.status.success() {
return Err(format!(
"cargo build failed for {pkg} with features {combo:?}"
));
}
let stdout = String::from_utf8_lossy(&out.stdout);
let size_bytes = stdout
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.filter(|v| v["reason"] == "compiler-artifact")
.filter(|v| {
v["target"]["name"]
.as_str()
.is_some_and(|n| n == pkg || n == pkg.replace('-', "_"))
})
.flat_map(|v| {
v["filenames"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|f| f.as_str().map(String::from))
.collect::<Vec<_>>()
})
.unwrap_or_default()
})
.filter_map(|f| std::fs::metadata(f).ok().map(|m| m.len()))
.max()
.unwrap_or(0);
let id = format!("buildcost::{pkg}::{combo}");
println!("{id:<44} buildcost build_ms={build_ms} size_bytes={size_bytes}");
run.items.insert(
id,
ItemMetrics {
build_ms: Some(build_ms),
size_bytes: Some(size_bytes),
..Default::default()
},
);
}
Ok(run)
}
fn cargo(args: &[&str], dir: Option<&Path>, target_dir: Option<&Path>) -> Command {
let mut cmd = Command::new("cargo");
cmd.args(args);
if let Some(d) = dir {
cmd.current_dir(d);
}
if let Some(td) = target_dir {
cmd.env("CARGO_TARGET_DIR", td);
}
cmd
}
fn warm_dependencies(
pkg: &str,
dir: Option<&Path>,
target_dir: Option<&Path>,
) -> Result<(), String> {
let warm = cargo(&["build", "--release", "-p", pkg], dir, target_dir)
.status()
.map_err(|e| e.to_string())?;
if warm.success() {
return Ok(());
}
Err(format!("warmup build failed for {pkg}"))
}