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>,
}
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,
_ => return false,
};
*slot = it.next().cloned();
true
}
}
fn bench_command(common: &CommonArgs, extra: &[&str], dir: Option<&Path>) -> Command {
let mut cmd = Command::new("cargo");
cmd.arg("bench");
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>> {
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);
cmd.stdout(Stdio::piped());
let out = cmd.output()?;
if !out.status.success() {
return Err(io::Error::other(format!(
"cargo bench failed with {} (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);
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)
}
#[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 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 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();
}
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(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 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 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>),
}
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(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)?)?;
Ok(path)
}
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
}
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 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())
}