use std::collections::{BTreeMap, BTreeSet};
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::SystemTime;
use serde_json::{Value, json};
#[derive(Default, Clone)]
pub struct CommonArgs {
pub pkg: Option<String>,
pub filter: Option<String>,
pub backend: Option<String>,
pub samples: Option<String>,
pub features: Option<String>,
pub target: Option<String>,
pub codegen_units: Option<String>,
}
impl CommonArgs {
pub fn try_parse<'a>(&mut self, arg: &str, it: &mut impl Iterator<Item = &'a String>) -> bool {
let slot = match arg {
"-p" | "--package" => &mut self.pkg,
"--filter" => &mut self.filter,
"--backend" => &mut self.backend,
"--samples" => &mut self.samples,
"--features" => &mut self.features,
"--bench" | "--target" => &mut self.target,
"--codegen-units" => &mut self.codegen_units,
_ => return false,
};
*slot = it.next().cloned();
true
}
pub fn codegen_units_env(&self) -> Option<&str> {
match self.codegen_units.as_deref() {
Some("inherit") => None,
Some(n) => Some(n),
None => Some(CANONICAL_CODEGEN_UNITS),
}
}
}
const CANONICAL_CODEGEN_UNITS: &str = "1";
fn bench_toolchain() -> Option<String> {
std::env::var("SOOTHFAST_BENCH_TOOLCHAIN").ok()
}
#[derive(Clone, Copy)]
enum Build {
Measure,
Discover,
}
fn bench_command(
common: &CommonArgs,
extra: &[&str],
dir: Option<&Path>,
target_dir: Option<&Path>,
build: Build,
) -> Command {
let mut cmd = Command::new("cargo");
if let Some(toolchain) = bench_toolchain() {
cmd.arg(format!("+{toolchain}"));
}
cmd.arg("bench");
if let Some(td) = target_dir {
cmd.env("CARGO_TARGET_DIR", td);
}
match build {
Build::Measure => {
if let Some(n) = common.codegen_units_env() {
cmd.env("CARGO_PROFILE_BENCH_CODEGEN_UNITS", n);
}
}
Build::Discover => {
cmd.args(["--profile", "dev"]);
}
}
if let Some(p) = &common.pkg {
cmd.args(["-p", p]);
}
if let Some(f) = &common.features {
cmd.args(["--features", f]);
}
let target = common.target.as_deref().unwrap_or("soothfast");
cmd.args(["--bench", target, "--"]);
if let Some(f) = &common.filter {
cmd.args(["--filter", f]);
}
if let Some(b) = &common.backend {
cmd.args(["--backend", b]);
}
if let Some(s) = &common.samples {
cmd.args(["--samples", s]);
}
cmd.args(extra);
if let Some(d) = dir {
cmd.current_dir(d);
}
cmd
}
pub fn run_bench_in(
common: &CommonArgs,
extra: &[&str],
dir: Option<&Path>,
) -> io::Result<Vec<Value>> {
run_bench_dir(common, extra, dir, None)
}
pub fn run_bench_dir(
common: &CommonArgs,
extra: &[&str],
dir: Option<&Path>,
target_dir: Option<&Path>,
) -> io::Result<Vec<Value>> {
run_records(common, extra, dir, target_dir, Build::Measure)
}
pub fn run_discovery_in(
common: &CommonArgs,
extra: &[&str],
dir: Option<&Path>,
) -> io::Result<Vec<Value>> {
run_records(common, extra, dir, None, Build::Discover)
}
pub fn run_discovery(common: &CommonArgs, extra: &[&str]) -> io::Result<Vec<Value>> {
run_discovery_in(common, extra, None)
}
fn run_records(
common: &CommonArgs,
extra: &[&str],
dir: Option<&Path>,
target_dir: Option<&Path>,
build: Build,
) -> io::Result<Vec<Value>> {
let mut args = vec!["--json"];
args.extend_from_slice(extra);
let target = common.target.as_deref().unwrap_or("soothfast");
let mut cmd = bench_command(common, &args, dir, target_dir, build);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let out = cmd.output()?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
let reason = stderr
.lines()
.find(|l| l.contains("error"))
.or_else(|| stderr.trim().lines().last())
.unwrap_or("no error output");
return Err(io::Error::other(format!(
"cargo bench failed with {}: {reason} (is the [[bench]] name = \"{target}\", harness = false target set up?)",
out.status
)));
}
let stdout = String::from_utf8_lossy(&out.stdout);
let mut records = Vec::new();
for l in stdout.lines().filter(|l| l.starts_with('{')) {
match serde_json::from_str(l) {
Ok(v) => records.push(v),
Err(e) => {
let snippet: String = l.chars().take(200).collect();
return Err(io::Error::other(format!(
"malformed runner record ({e}): {snippet}"
)));
}
}
}
Ok(records)
}
pub fn run_bench_raw(common: &CommonArgs, extra: &[&str]) -> io::Result<String> {
let mut cmd = bench_command(common, extra, None, None, Build::Measure);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let out = cmd.output()?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
let reason = stderr
.lines()
.find(|l| l.starts_with("soothfast runner:"))
.unwrap_or("")
.to_string();
return Err(io::Error::other(format!(
"cargo bench failed with {}{}{}",
out.status,
if reason.is_empty() { "" } else { ": " },
reason
)));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
pub fn run_bench(common: &CommonArgs, extra: &[&str]) -> io::Result<Vec<Value>> {
run_bench_in(common, extra, None)
}
pub fn bench_executable(
common: &CommonArgs,
dir: Option<&Path>,
target_dir: Option<&Path>,
) -> Option<PathBuf> {
let target = common.target.as_deref().unwrap_or("soothfast");
let mut cmd = Command::new("cargo");
cmd.args(["bench", "--no-run", "--message-format=json"]);
if let Some(td) = target_dir {
cmd.env("CARGO_TARGET_DIR", td);
}
if let Some(n) = common.codegen_units_env() {
cmd.env("CARGO_PROFILE_BENCH_CODEGEN_UNITS", n);
}
if let Some(p) = &common.pkg {
cmd.args(["-p", p]);
}
if let Some(f) = &common.features {
cmd.args(["--features", f]);
}
cmd.args(["--bench", target]);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::null());
if let Some(d) = dir {
cmd.current_dir(d);
}
let out = cmd.output().ok()?;
if !out.status.success() {
return None;
}
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| serde_json::from_str::<Value>(l).ok())
.filter(|m| m["reason"] == "compiler-artifact")
.filter(|m| m["target"]["name"] == target)
.filter(|m| {
m["target"]["kind"]
.as_array()
.into_iter()
.flatten()
.any(|k| k == "bench")
})
.find_map(|m| m["executable"].as_str().map(PathBuf::from))
}
#[derive(Default, Debug, Clone)]
pub struct ItemMetrics {
pub fingerprint: String,
pub covers: String,
pub median_ns: Option<f64>,
pub mad_ns: Option<f64>,
pub p99_ns: Option<f64>,
pub instructions: Option<u64>,
pub cycles: Option<u64>,
pub cache_refs: Option<u64>,
pub ir: Option<u64>,
pub allocs: Option<u64>,
pub bytes: Option<u64>,
pub polls: Option<u64>,
pub wakes: Option<u64>,
pub tolerance_pct: Option<f64>,
pub wall_rounds: Vec<f64>,
pub build_ms: Option<u64>,
pub size_bytes: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct AssertionOutcome {
pub id: String,
pub kind: String,
pub ok: bool,
pub detail: String,
}
#[derive(Default, Debug)]
pub struct Run {
pub noise_pct: Option<f64>,
pub build: Option<crate::buildstamp::BuildStamp>,
pub gating_backend: Option<String>,
pub items: BTreeMap<String, ItemMetrics>,
pub assertions: Vec<AssertionOutcome>,
}
pub fn collect(records: &[Value]) -> Run {
let mut run = Run::default();
for rec in records {
match rec["type"].as_str() {
Some("calibration") => run.noise_pct = rec["noise_pct"].as_f64(),
Some("env") => run.gating_backend = rec["gating_backend"].as_str().map(str::to_string),
Some("assertion") => run.assertions.push(AssertionOutcome {
id: rec["id"].as_str().unwrap_or("").to_string(),
kind: rec["kind"].as_str().unwrap_or("").to_string(),
ok: rec["ok"].as_bool().unwrap_or(false),
detail: rec["detail"].as_str().unwrap_or("").to_string(),
}),
Some("result") => {
let Some(id) = rec["id"].as_str() else {
continue;
};
let item = run.items.entry(id.to_string()).or_default();
if let Some(fp) = rec["fingerprint"].as_str() {
item.fingerprint = fp.to_string();
}
if let Some(c) = rec["covers"].as_str() {
item.covers = c.to_string();
}
if let Some(t) = rec["tolerance_pct"].as_f64() {
item.tolerance_pct = Some(t);
}
let m = &rec["metrics"];
match rec["backend"].as_str() {
Some("walltime") => {
item.median_ns = m["median_ns"].as_f64();
item.mad_ns = m["mad_ns"].as_f64();
item.p99_ns = m["p99_ns"].as_f64();
}
Some("perfcnt") => {
item.instructions = m["instructions"].as_u64();
item.cycles = m["cycles"].as_u64();
item.cache_refs = m["cache_refs"].as_u64();
}
Some("callgrind") => item.ir = m["ir"].as_u64(),
Some("alloc") => {
item.allocs = m["allocs"].as_u64();
item.bytes = m["bytes"].as_u64();
}
Some("asyncexec") => {
item.polls = m["polls"].as_u64();
item.wakes = m["wakes"].as_u64();
}
Some("buildcost") => {
item.build_ms = m["build_ms"].as_u64();
item.size_bytes = m["size_bytes"].as_u64();
}
_ => {}
}
}
_ => {}
}
}
run
}
pub fn run_to_items_value(run: &Run) -> Value {
let mut items = serde_json::Map::new();
for (id, m) in &run.items {
let mut entry = json!({ "fingerprint": m.fingerprint, "covers": m.covers });
if let Some(t) = m.tolerance_pct {
entry["tolerance_pct"] = json!(t);
}
if let Some(v) = m.median_ns {
entry["walltime"] = json!({
"median_ns": v,
"mad_ns": m.mad_ns.unwrap_or(0.0),
"p99_ns": m.p99_ns.unwrap_or(0.0),
});
if !m.wall_rounds.is_empty() {
entry["walltime"]["rounds"] = json!(m.wall_rounds);
}
}
if let Some(i) = m.instructions {
entry["perfcnt"] = json!({
"instructions": i,
"cycles": m.cycles.unwrap_or(0),
"cache_refs": m.cache_refs.unwrap_or(0),
});
}
if let Some(ir) = m.ir {
entry["callgrind"] = json!({ "ir": ir });
}
if let (Some(a), Some(b)) = (m.allocs, m.bytes) {
entry["alloc"] = json!({ "allocs": a, "bytes": b });
}
if let (Some(p), Some(w)) = (m.polls, m.wakes) {
entry["asyncexec"] = json!({ "polls": p, "wakes": w });
}
if let (Some(ms), Some(sz)) = (m.build_ms, m.size_bytes) {
entry["buildcost"] = json!({ "build_ms": ms, "size_bytes": sz });
}
items.insert(id.clone(), entry);
}
Value::Object(items)
}
pub fn run_from_items_value(items: &Value) -> Run {
let mut run = Run::default();
let Some(map) = items.as_object() else {
return run;
};
for (id, v) in map {
run.items.insert(
id.clone(),
ItemMetrics {
fingerprint: v["fingerprint"].as_str().unwrap_or("").to_string(),
covers: v["covers"].as_str().unwrap_or("").to_string(),
tolerance_pct: v["tolerance_pct"].as_f64(),
median_ns: v["walltime"]["median_ns"].as_f64(),
mad_ns: v["walltime"]["mad_ns"].as_f64(),
p99_ns: v["walltime"]["p99_ns"].as_f64(),
wall_rounds: v["walltime"]["rounds"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_f64)
.collect(),
instructions: v["perfcnt"]["instructions"].as_u64(),
cycles: v["perfcnt"]["cycles"].as_u64(),
cache_refs: v["perfcnt"]["cache_refs"].as_u64(),
ir: v["callgrind"]["ir"].as_u64(),
allocs: v["alloc"]["allocs"].as_u64(),
bytes: v["alloc"]["bytes"].as_u64(),
polls: v["asyncexec"]["polls"].as_u64(),
wakes: v["asyncexec"]["wakes"].as_u64(),
build_ms: v["buildcost"]["build_ms"].as_u64(),
size_bytes: v["buildcost"]["size_bytes"].as_u64(),
},
);
}
run
}
pub fn workspace_root() -> io::Result<PathBuf> {
let out = Command::new("cargo")
.args(["locate-project", "--workspace", "--message-format", "plain"])
.output()?;
if !out.status.success() {
return Err(io::Error::other("cargo locate-project failed"));
}
let manifest = String::from_utf8_lossy(&out.stdout).trim().to_string();
Ok(PathBuf::from(manifest)
.parent()
.expect("manifest path has a parent")
.to_path_buf())
}
fn baseline_path(name: &str) -> io::Result<PathBuf> {
Ok(workspace_root()?
.join(".soothfast")
.join("baselines")
.join(format!("{name}.json")))
}
pub enum SaveScope {
BenchFull(Option<String>),
BenchFiltered,
Buildcost(Option<String>),
}
impl SaveScope {
pub fn of(common: &CommonArgs) -> SaveScope {
if common.backend.as_deref() == Some("buildcost") {
SaveScope::Buildcost(common.pkg.clone())
} else if common.filter.is_some() {
SaveScope::BenchFiltered
} else {
SaveScope::BenchFull(common.pkg.clone())
}
}
}
pub fn id_pkg(id: &str) -> &str {
id.split("::").next().unwrap_or(id)
}
pub fn save_baseline(name: &str, run: &Run, scope: SaveScope) -> io::Result<PathBuf> {
let new_items = run_to_items_value(run);
let mut doc = load_baseline(name)?.unwrap_or_else(|| json!({ "version": 1, "items": {} }));
let unix_now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
doc["created_unix"] = json!(unix_now);
if let Some(n) = run.noise_pct {
doc["noise_pct"] = json!(n);
}
if let Some(b) = &run.build {
doc["build"] = b.to_json();
}
if let (Some(map), Some(new_map)) = (doc["items"].as_object_mut(), new_items.as_object()) {
match scope {
SaveScope::BenchFull(Some(pkg)) => {
let pkg = pkg.replace('-', "_");
map.retain(|k, _| k.starts_with("buildcost::") || id_pkg(k) != pkg);
}
SaveScope::BenchFull(None) => map.retain(|k, _| k.starts_with("buildcost::")),
SaveScope::Buildcost(Some(pkg)) => map.retain(|k, _| {
k.strip_prefix("buildcost::")
.is_none_or(|rest| rest.split("::").next() != Some(pkg.as_str()))
}),
SaveScope::Buildcost(None) => map.retain(|k, _| !k.starts_with("buildcost::")),
SaveScope::BenchFiltered => {}
}
for (k, v) in new_map {
map.insert(k.clone(), v.clone());
}
}
let replaced: Vec<String> = run.items.keys().cloned().collect();
let mut assertions: Vec<Value> = doc["assertions"]
.as_array()
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|a| {
a["id"]
.as_str()
.is_none_or(|id| !replaced.contains(&id.to_string()))
})
.collect();
for a in &run.assertions {
assertions.push(json!({
"id": a.id, "kind": a.kind, "ok": a.ok, "detail": a.detail,
}));
}
doc["assertions"] = Value::Array(assertions);
let path = baseline_path(name)?;
std::fs::create_dir_all(path.parent().expect("baseline path has a parent"))?;
std::fs::write(&path, serde_json::to_string_pretty(&doc)?)?;
prune_absorbed_accepts(run);
Ok(path)
}
fn prune_absorbed_accepts(run: &Run) {
let Ok(root) = workspace_root() else {
return;
};
let Ok(entries) = crate::gate_lock::read(&root) else {
return;
};
let (pruned, removed) = crate::gate_lock::prune_absorbed(&entries, run);
if removed.is_empty() {
return;
}
if crate::gate_lock::write(&root, &pruned).is_ok() {
for id in removed {
println!("soothfast-gate.lock: {id} absorbed into the new baseline, entry removed");
}
}
}
pub fn load_baseline(name: &str) -> io::Result<Option<Value>> {
let path = baseline_path(name)?;
if !path.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(path)?;
Ok(Some(serde_json::from_str(&text)?))
}
pub fn pkg_dir(pkg: &str) -> io::Result<PathBuf> {
pkg_meta(pkg).map(|m| m.dir)
}
pub struct PkgMeta {
pub dir: PathBuf,
pub version: String,
pub description: Option<String>,
}
pub fn pkg_meta(pkg: &str) -> io::Result<PkgMeta> {
let out = Command::new("cargo")
.args(["metadata", "--no-deps", "--format-version", "1"])
.stdout(Stdio::piped())
.output()?;
if !out.status.success() {
return Err(io::Error::other("cargo metadata failed"));
}
let meta: Value = serde_json::from_slice(&out.stdout)?;
let p = meta["packages"]
.as_array()
.into_iter()
.flatten()
.find(|p| p["name"] == pkg)
.ok_or_else(|| io::Error::other(format!("no workspace package named {pkg:?}")))?;
let dir = p["manifest_path"]
.as_str()
.map(|m| {
PathBuf::from(m)
.parent()
.expect("manifest has parent")
.to_path_buf()
})
.ok_or_else(|| io::Error::other(format!("package {pkg:?} has no manifest path")))?;
Ok(PkgMeta {
dir,
version: p["version"].as_str().unwrap_or("0.0.0").to_string(),
description: p["description"].as_str().map(String::from),
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Visibility {
Public,
Private,
}
const RUSTDOC_FORMAT_VERSION: u64 = 61;
fn rustdoc_toolchain() -> String {
std::env::var("SOOTHFAST_RUSTDOC_TOOLCHAIN").unwrap_or_else(|_| "nightly".to_string())
}
fn warn_on_format_version(doc: &Value) {
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
let Some(found) = doc["format_version"].as_u64() else {
return;
};
if found == RUSTDOC_FORMAT_VERSION || WARNED.swap(true, Ordering::Relaxed) {
return;
}
eprintln!(
"warning: rustdoc JSON format_version {found}, expected {RUSTDOC_FORMAT_VERSION}. \
The extracted surface may differ from the one a committed artifact was \
generated with. Pin a nightly with SOOTHFAST_RUSTDOC_TOOLCHAIN to make it \
reproducible."
);
}
fn local_crate_dirs(pkg: &str, dir: Option<&Path>) -> io::Result<Vec<PathBuf>> {
let mut cmd = Command::new("cargo");
cmd.args(["metadata", "--format-version", "1"]);
cmd.stdout(Stdio::piped());
if let Some(d) = dir {
cmd.current_dir(d);
}
let out = cmd.output()?;
if !out.status.success() {
return Err(io::Error::other("cargo metadata failed"));
}
let meta: Value = serde_json::from_slice(&out.stdout)?;
let packages = meta["packages"].as_array().cloned().unwrap_or_default();
let mut dir_of: BTreeMap<String, PathBuf> = BTreeMap::new();
let mut local: BTreeSet<String> = BTreeSet::new();
for p in &packages {
let Some(id) = p["id"].as_str() else { continue };
if let Some(cd) = p["manifest_path"]
.as_str()
.and_then(|m| Path::new(m).parent())
{
dir_of.insert(id.to_string(), cd.to_path_buf());
}
if p["source"].is_null() {
local.insert(id.to_string());
}
}
let Some(root_id) = packages
.iter()
.find(|p| p["name"] == pkg)
.and_then(|p| p["id"].as_str())
.map(str::to_string)
else {
return Ok(Vec::new());
};
let deps_of: BTreeMap<String, Vec<String>> = meta["resolve"]["nodes"]
.as_array()
.into_iter()
.flatten()
.filter_map(|n| {
let id = n["id"].as_str()?.to_string();
let deps = n["dependencies"]
.as_array()
.into_iter()
.flatten()
.filter_map(|d| d.as_str().map(str::to_string))
.collect();
Some((id, deps))
})
.collect();
let mut seen = BTreeSet::new();
let mut stack = vec![root_id];
let mut dirs = Vec::new();
while let Some(id) = stack.pop() {
if !seen.insert(id.clone()) {
continue;
}
if local.contains(&id)
&& let Some(d) = dir_of.get(&id)
{
dirs.push(d.clone());
}
stack.extend(deps_of.get(&id).cloned().into_iter().flatten());
}
Ok(dirs)
}
fn newest_mtime(path: &Path) -> io::Result<Option<SystemTime>> {
let Ok(meta) = std::fs::metadata(path) else {
return Ok(None);
};
if meta.is_file() {
return Ok(Some(meta.modified()?));
}
let mut newest = meta.modified().ok();
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let name = entry.file_name();
if name == "target" || name == ".git" {
continue;
}
if let Some(t) = newest_mtime(&entry.path())?
&& newest.is_none_or(|n| t > n)
{
newest = Some(t);
}
}
Ok(newest)
}
fn newest_relevant_mtime(pkg: &str, dir: Option<&Path>) -> io::Result<Option<SystemTime>> {
let mut newest: Option<SystemTime> = None;
let mut bump = |t: Option<SystemTime>| {
if let Some(t) = t
&& newest.is_none_or(|n| t > n)
{
newest = Some(t);
}
};
for crate_dir in local_crate_dirs(pkg, dir)? {
bump(newest_mtime(&crate_dir.join("Cargo.toml"))?);
bump(newest_mtime(&crate_dir.join("src"))?);
bump(newest_mtime(&crate_dir.join("build.rs"))?);
}
let root = match dir {
Some(d) => d.to_path_buf(),
None => workspace_root()?,
};
bump(newest_mtime(&root.join("Cargo.lock"))?);
Ok(newest)
}
fn cache_meta_path(json_path: &Path) -> PathBuf {
json_path.with_extension("json.soothfast-cache")
}
fn read_fresh_cache(
json_path: &Path,
pkg: &str,
dir: Option<&Path>,
features: Option<&str>,
visibility: Visibility,
toolchain: &str,
) -> Option<Value> {
let meta_text = std::fs::read_to_string(cache_meta_path(json_path)).ok()?;
let meta: Value = serde_json::from_str(&meta_text).ok()?;
let matches = meta["toolchain"].as_str() == Some(toolchain)
&& meta["features"].as_str() == features
&& meta["private"].as_bool() == Some(visibility == Visibility::Private);
if !matches {
return None;
}
let json_mtime = std::fs::metadata(json_path).ok()?.modified().ok()?;
let newest_src = newest_relevant_mtime(pkg, dir).ok()??;
if newest_src > json_mtime {
return None;
}
let text = std::fs::read_to_string(json_path).ok()?;
serde_json::from_str(&text).ok()
}
fn write_cache_meta(
json_path: &Path,
features: Option<&str>,
visibility: Visibility,
toolchain: &str,
) -> io::Result<()> {
let meta = json!({
"toolchain": toolchain,
"features": features,
"private": visibility == Visibility::Private,
});
std::fs::write(cache_meta_path(json_path), serde_json::to_string(&meta)?)
}
pub fn rustdoc_json_in(
pkg: &str,
dir: Option<&Path>,
features: Option<&str>,
visibility: Visibility,
) -> io::Result<(Value, PathBuf)> {
let toolchain = rustdoc_toolchain();
let root = match dir {
Some(d) => d.to_path_buf(),
None => workspace_root()?,
};
let json_path = target_dir(dir)?
.join("doc")
.join(format!("{}.json", pkg.replace('-', "_")));
if let Some(doc) = read_fresh_cache(&json_path, pkg, dir, features, visibility, &toolchain) {
return Ok((doc, root));
}
let mut cmd = Command::new("cargo");
cmd.args([&format!("+{toolchain}"), "rustdoc", "-p", pkg, "--lib"]);
if let Some(f) = features {
cmd.args(["--features", f]);
}
cmd.args(["--", "--output-format", "json", "-Zunstable-options"]);
if visibility == Visibility::Private {
cmd.arg("--document-private-items");
}
if let Some(d) = dir {
cmd.current_dir(d);
}
let status = cmd.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"cargo +{toolchain} rustdoc failed with {status} \
(is the {toolchain} toolchain installed?)"
)));
}
let text = std::fs::read_to_string(&json_path)
.map_err(|e| io::Error::other(format!("cannot read {}: {e}", json_path.display())))?;
let doc: Value = serde_json::from_str(&text)?;
warn_on_format_version(&doc);
let _ = write_cache_meta(&json_path, features, visibility, &toolchain);
Ok((doc, root))
}
pub fn with_merge_base_worktree<T>(
refname: &str,
f: impl FnOnce(&Path) -> Result<T, String>,
) -> Result<T, String> {
let sha = git(&["merge-base", "HEAD", refname]).map_err(|e| e.to_string())?;
let root = workspace_root().map_err(|e| e.to_string())?;
let wt = root.join(".soothfast").join("worktrees").join(&sha);
let wt_str = wt
.to_str()
.ok_or_else(|| "worktree path is not UTF-8".to_string())?;
if !wt.exists() {
git(&["worktree", "add", "--detach", wt_str, &sha]).map_err(|e| e.to_string())?;
}
let result = f(&wt);
let _ = git(&["worktree", "remove", "--force", wt_str]);
result
}
struct LockPkg {
name: String,
version: String,
registry: bool,
}
fn lock_packages(lock: &str) -> Vec<LockPkg> {
lock.split("[[package]]")
.skip(1)
.filter_map(|block| {
let field = |key: &str| {
block
.lines()
.find_map(|l| l.strip_prefix(key))
.map(|v| v.trim().trim_matches('"').to_string())
};
Some(LockPkg {
name: field("name = ")?,
version: field("version = ")?,
registry: block.lines().any(|l| l.starts_with("source = ")),
})
})
.collect()
}
fn harness_mismatches(head_lock: &str, wt_lock: &str) -> Vec<(String, String, String)> {
let head: BTreeMap<String, LockPkg> = lock_packages(head_lock)
.into_iter()
.map(|p| (p.name.clone(), p))
.collect();
lock_packages(wt_lock)
.into_iter()
.filter(|p| p.registry && (p.name == "soothfast" || p.name.starts_with("soothfast-")))
.filter_map(|p| {
let h = head.get(&p.name).filter(|h| h.registry)?;
(h.version != p.version).then(|| (p.name, p.version, h.version.clone()))
})
.collect()
}
pub fn harness_versions() -> String {
let Ok(root) = workspace_root() else {
return String::new();
};
let Ok(lock) = std::fs::read_to_string(root.join("Cargo.lock")) else {
return String::new();
};
let mut out: Vec<String> = lock_packages(&lock)
.into_iter()
.filter(|p| p.name == "soothfast" || p.name.starts_with("soothfast-"))
.map(|p| format!("{}@{}", p.name, p.version))
.collect();
out.sort();
out.join(",")
}
pub fn tree_is_clean() -> bool {
git(&["status", "--porcelain"]).is_ok_and(|s| s.trim().is_empty())
}
pub fn sync_harness_versions(wt: &Path) -> Result<(), String> {
let head_path = workspace_root()
.map_err(|e| e.to_string())?
.join("Cargo.lock");
let Ok(head) = std::fs::read_to_string(&head_path) else {
return Ok(());
};
for _ in 0..16 {
let Ok(base) = std::fs::read_to_string(wt.join("Cargo.lock")) else {
return Ok(());
};
let Some((name, from, to)) = harness_mismatches(&head, &base).into_iter().next() else {
return Ok(());
};
println!(
"gate: pinning {name} {from} -> {to} in the merge-base worktree (harness must match HEAD)"
);
let out = Command::new("cargo")
.args(["update", "-p", &format!("{name}@{from}"), "--precise", &to])
.current_dir(wt)
.output()
.map_err(|e| e.to_string())?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
eprintln!(
"WARN: could not pin {name} ({}); the reference keeps its own harness — deltas may reflect the harness change itself",
stderr.trim().lines().last().unwrap_or("no error output")
);
return Ok(());
}
}
Ok(())
}
pub fn sync_untracked_cargo_config(wt: &Path) -> Result<(), String> {
let root = workspace_root().map_err(|e| e.to_string())?;
for name in [".cargo/config.toml", ".cargo/config"] {
let src = root.join(name);
if !src.exists() || git(&["ls-files", "--error-unmatch", name]).is_ok() {
continue;
}
let dst = wt.join(name);
std::fs::create_dir_all(dst.parent().expect("config path has a parent"))
.map_err(|e| e.to_string())?;
std::fs::copy(&src, &dst).map_err(|e| e.to_string())?;
}
Ok(())
}
pub fn target_dir(dir: Option<&Path>) -> io::Result<PathBuf> {
let mut cmd = Command::new("cargo");
cmd.args(["metadata", "--no-deps", "--format-version", "1"]);
cmd.stdout(Stdio::piped());
if let Some(d) = dir {
cmd.current_dir(d);
}
let out = cmd.output()?;
if !out.status.success() {
return Err(io::Error::other("cargo metadata failed"));
}
let meta: Value = serde_json::from_slice(&out.stdout)?;
meta["target_directory"]
.as_str()
.map(PathBuf::from)
.ok_or_else(|| io::Error::other("cargo metadata: no target_directory"))
}
pub fn worktree_target_dir() -> io::Result<PathBuf> {
Ok(target_dir(None)?.join("soothfast-worktree"))
}
pub fn git(args: &[&str]) -> io::Result<String> {
let root = workspace_root()?;
git_in(&root, args)
}
pub fn git_in(dir: &Path, args: &[&str]) -> io::Result<String> {
let out = Command::new("git").args(args).current_dir(dir).output()?;
if !out.status.success() {
return Err(io::Error::other(format!(
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
)));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
#[cfg(test)]
mod tests {
use super::{
CommonArgs, ItemMetrics, Run, SaveScope, harness_mismatches, run_from_items_value,
run_to_items_value,
};
#[test]
fn run_from_items_value_round_trips_run_to_items_value() {
let mut run = Run::default();
run.items.insert(
"pkg::bench".into(),
ItemMetrics {
fingerprint: "fp".into(),
covers: "pkg::covered".into(),
median_ns: Some(100.0),
mad_ns: Some(1.0),
p99_ns: Some(120.0),
wall_rounds: vec![95.0, 105.0],
instructions: Some(500),
cycles: Some(600),
cache_refs: Some(10),
ir: Some(700),
allocs: Some(2),
bytes: Some(64),
polls: Some(3),
wakes: Some(3),
tolerance_pct: Some(8.0),
build_ms: None,
size_bytes: None,
},
);
let round_tripped = run_from_items_value(&run_to_items_value(&run));
let original = &run.items["pkg::bench"];
let back = &round_tripped.items["pkg::bench"];
assert_eq!(back.fingerprint, original.fingerprint);
assert_eq!(back.covers, original.covers);
assert_eq!(back.median_ns, original.median_ns);
assert_eq!(back.mad_ns, original.mad_ns);
assert_eq!(back.p99_ns, original.p99_ns);
assert_eq!(back.wall_rounds, original.wall_rounds);
assert_eq!(back.instructions, original.instructions);
assert_eq!(back.cycles, original.cycles);
assert_eq!(back.cache_refs, original.cache_refs);
assert_eq!(back.ir, original.ir);
assert_eq!(back.allocs, original.allocs);
assert_eq!(back.bytes, original.bytes);
assert_eq!(back.polls, original.polls);
assert_eq!(back.wakes, original.wakes);
assert_eq!(back.tolerance_pct, original.tolerance_pct);
}
#[test]
fn save_scope_mirrors_what_the_args_measured() {
let mut common = CommonArgs {
pkg: Some("mylib".into()),
..CommonArgs::default()
};
assert!(matches!(
SaveScope::of(&common), SaveScope::BenchFull(Some(p)) if p == "mylib"
));
common.filter = Some("group::".into());
assert!(matches!(SaveScope::of(&common), SaveScope::BenchFiltered));
common.filter = None;
common.backend = Some("buildcost".into());
assert!(matches!(
SaveScope::of(&common), SaveScope::Buildcost(Some(p)) if p == "mylib"
));
}
const HEAD: &str = r#"
version = 4
[[package]]
name = "serde"
version = "1.0.200"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "soothfast"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"soothfast-measure",
]
[[package]]
name = "soothfast-measure"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
"#;
#[test]
fn pins_family_crates_to_heads_versions() {
let base = HEAD.replace("0.1.7", "0.1.5");
assert_eq!(
harness_mismatches(HEAD, &base),
vec![
("soothfast".into(), "0.1.5".into(), "0.1.7".into()),
("soothfast-measure".into(), "0.1.5".into(), "0.1.7".into()),
]
);
}
#[test]
fn matching_locks_and_foreign_crates_need_nothing() {
assert!(harness_mismatches(HEAD, HEAD).is_empty());
let base = HEAD.replace("1.0.200", "1.0.100");
assert!(harness_mismatches(HEAD, &base).is_empty());
}
#[test]
fn path_dependencies_are_skipped() {
let no_source = HEAD.replace(
"source = \"registry+https://github.com/rust-lang/crates.io-index\"",
"",
);
let base = no_source.replace("0.1.7", "0.1.5");
assert!(harness_mismatches(&no_source, &base).is_empty());
}
}