mod implementation;
use implementation::{FailKind, Importance, Output, TestMdata, Timings, consts};
use std::{
fs::OpenOptions,
io::{Read, Write},
num::NonZero,
path::{Path, PathBuf},
process::{Command, Stdio},
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
const DEFAULT_ITER_COUNT: NonZero<usize> = NonZero::new(3).unwrap();
const ITER_COUNT_MUL: NonZero<usize> = NonZero::new(4).unwrap();
static QUIET: AtomicBool = AtomicBool::new(false);
macro_rules! fail {
($output:ident, $name:expr, $kind:expr) => {{
$output.failure($name, None, None, $kind);
continue;
}};
($output:ident, $name:expr, $mdata:expr, $kind:expr) => {{
$output.failure($name, Some($mdata), None, $kind);
continue;
}};
($output:ident, $name:expr, $mdata:expr, $count:expr, $kind:expr) => {{
$output.failure($name, Some($mdata), Some($count), $kind);
continue;
}};
}
enum OutputKind<'a> {
Markdown,
Json(&'a Path),
}
impl OutputKind<'_> {
fn log(&self, output: &Output, t_bin: &str) {
match self {
OutputKind::Markdown => println!("{output}"),
OutputKind::Json(ident) => {
let wspace_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("..")
.join("..");
let runs_dir = PathBuf::from(&wspace_dir).join(consts::RUNS_DIR);
std::fs::create_dir_all(&runs_dir).unwrap();
assert!(
!ident.to_string_lossy().is_empty(),
"FATAL: Empty filename specified!"
);
let test_bin_stripped = Path::new(t_bin)
.file_name()
.unwrap()
.to_str()
.unwrap()
.rsplit_once('-')
.unwrap()
.0;
let mut file_path = runs_dir.join(ident);
file_path
.as_mut_os_string()
.push(format!(".{test_bin_stripped}.json"));
let mut out_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&file_path)
.unwrap();
out_file
.write_all(&serde_json::to_vec(&output).unwrap())
.unwrap();
if !QUIET.load(Ordering::Relaxed) {
eprintln!("JSON output written to {}", file_path.display());
}
}
}
}
}
fn parse_mdata(t_bin: &str, mdata_fn: &str) -> Result<TestMdata, FailKind> {
let mut cmd = Command::new(t_bin);
cmd.args([mdata_fn, "--exact", "--nocapture"]);
let out = cmd
.output()
.expect("FATAL: Could not run test binary {t_bin}");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let mut version = None;
let mut iterations = None;
let mut importance = Importance::default();
let mut weight = consts::WEIGHT_DEFAULT;
for line in stdout
.lines()
.filter_map(|l| l.strip_prefix(consts::MDATA_LINE_PREF))
{
let mut items = line.split_whitespace();
match items.next().ok_or(FailKind::BadMetadata)? {
consts::VERSION_LINE_NAME => {
let v = items
.next()
.ok_or(FailKind::BadMetadata)?
.parse::<u32>()
.map_err(|_| FailKind::BadMetadata)?;
if v > consts::MDATA_VER {
return Err(FailKind::VersionMismatch);
}
version = Some(v);
}
consts::ITER_COUNT_LINE_NAME => {
iterations = Some(
items
.next()
.ok_or(FailKind::BadMetadata)?
.parse::<usize>()
.map_err(|_| FailKind::BadMetadata)?
.try_into()
.map_err(|_| FailKind::BadMetadata)?,
);
}
consts::IMPORTANCE_LINE_NAME => {
importance = match items.next().ok_or(FailKind::BadMetadata)? {
"critical" => Importance::Critical,
"important" => Importance::Important,
"average" => Importance::Average,
"iffy" => Importance::Iffy,
"fluff" => Importance::Fluff,
_ => return Err(FailKind::BadMetadata),
};
}
consts::WEIGHT_LINE_NAME => {
weight = items
.next()
.ok_or(FailKind::BadMetadata)?
.parse::<u8>()
.map_err(|_| FailKind::BadMetadata)?;
}
_ => unreachable!(),
}
}
Ok(TestMdata {
version: version.ok_or(FailKind::BadMetadata)?,
iterations,
importance,
weight,
})
}
fn compare_profiles(args: &[String]) {
let mut save_to = None;
let mut ident_idx = 0;
args.first().inspect(|a| {
if a.starts_with("--save") {
save_to = Some(
a.strip_prefix("--save=")
.expect("FATAL: save param formatted incorrectly"),
);
ident_idx = 1;
}
});
let ident_new = args
.get(ident_idx)
.expect("FATAL: missing identifier for new run");
let ident_old = args
.get(ident_idx + 1)
.expect("FATAL: missing identifier for old run");
let wspace_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let runs_dir = PathBuf::from(&wspace_dir)
.join("..")
.join("..")
.join(consts::RUNS_DIR);
let mut outputs_new = Output::blank();
let mut outputs_old = Output::blank();
for e in runs_dir.read_dir().unwrap() {
let Ok(entry) = e else {
continue;
};
let Ok(metadata) = entry.metadata() else {
continue;
};
if metadata.is_file() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
let read_into = |output: &mut Output| {
let mut elems = name.split('.').skip(1);
let prefix = elems.next().unwrap();
assert_eq!("json", elems.next().unwrap());
assert!(elems.next().is_none());
let mut buffer = Vec::new();
let _ = OpenOptions::new()
.read(true)
.open(entry.path())
.unwrap()
.read_to_end(&mut buffer)
.unwrap();
let o_other: Output = serde_json::from_slice(&buffer).unwrap();
output.merge(o_other, prefix);
};
if name.starts_with(ident_old) {
read_into(&mut outputs_old);
} else if name.starts_with(ident_new) {
read_into(&mut outputs_new);
}
}
}
let res = outputs_new.compare_perf(outputs_old);
if let Some(filename) = save_to {
let mut file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)
.expect("FATAL: couldn't save run results to file");
file.write_all(format!("{res}").as_bytes()).unwrap();
} else {
println!("{res}");
}
}
fn get_tests(t_bin: &str) -> impl ExactSizeIterator<Item = (String, String)> {
let mut cmd = Command::new(t_bin);
cmd.args(["--list", "--format=terse"]);
let out = cmd
.output()
.expect("FATAL: Could not run test binary {t_bin}");
assert!(
out.status.success(),
"FATAL: Cannot do perf check - test binary {t_bin} returned an error"
);
if !QUIET.load(Ordering::Relaxed) {
eprintln!("Test binary ran successfully; starting profile...");
}
let stdout = String::from_utf8_lossy(&out.stdout);
let mut test_list: Vec<_> = stdout
.lines()
.filter_map(|line| {
let line: Vec<_> = line.split_whitespace().collect();
match line[..] {
[t_name, kind] => (kind == "test").then(|| &t_name[..t_name.len() - 1]),
_ => None,
}
})
.filter(|t_name| {
t_name.ends_with(consts::SUF_NORMAL) || t_name.ends_with(consts::SUF_MDATA)
})
.collect();
test_list.sort_unstable();
test_list.dedup();
assert!(
test_list.len().is_multiple_of(2),
"Malformed tests in test binary {t_bin}"
);
let out = test_list
.chunks_exact_mut(2)
.map(|pair| {
if consts::SUF_NORMAL < consts::SUF_MDATA {
(pair[0].to_owned(), pair[1].to_owned())
} else {
(pair[1].to_owned(), pair[0].to_owned())
}
})
.collect::<Vec<_>>();
out.into_iter()
}
#[inline]
fn spawn_and_iterate(t_bin: &str, t_name: &str, count: NonZero<usize>) -> Option<Duration> {
let mut cmd = Command::new(t_bin);
cmd.args([t_name, "--exact"]);
cmd.env(consts::ITER_ENV_VAR, format!("{count}"));
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
let pre = Instant::now();
let out = cmd.spawn().unwrap().wait();
let post = Instant::now();
out.iter().find_map(|s| s.success().then_some(post - pre))
}
fn triage_test(
t_bin: &str,
t_name: &str,
thresh: Duration,
mut step: impl FnMut(NonZero<usize>) -> Option<NonZero<usize>>,
) -> Option<NonZero<usize>> {
let mut iter_count = DEFAULT_ITER_COUNT;
let duration_once = spawn_and_iterate(t_bin, t_name, NonZero::new(1).unwrap())?;
loop {
let duration = spawn_and_iterate(t_bin, t_name, iter_count)?;
if duration.saturating_sub(duration_once) > thresh {
break Some(iter_count);
}
let new = step(iter_count)?;
assert!(
new > iter_count,
"FATAL: step must be monotonically increasing"
);
iter_count = new;
}
}
fn hyp_binary() -> Option<Command> {
const HYP_PATH: &str = "hyperfine";
const HYP_HOME: &str = "~/.cargo/bin/hyperfine";
if Command::new(HYP_PATH).output().is_err() {
if Command::new(HYP_HOME).output().is_err() {
None
} else {
Some(Command::new(HYP_HOME))
}
} else {
Some(Command::new(HYP_PATH))
}
}
fn hyp_profile(t_bin: &str, t_name: &str, iterations: NonZero<usize>) -> Option<Timings> {
let mut perf_cmd = hyp_binary().expect("Couldn't find the Hyperfine binary on the system");
perf_cmd.args([
"--style",
"none",
"--warmup",
"1",
"--export-markdown",
"-",
"--time-unit",
"millisecond",
&format!("{t_bin} --exact {t_name}"),
]);
perf_cmd.env(consts::ITER_ENV_VAR, format!("{iterations}"));
let p_out = perf_cmd.output().unwrap();
if !p_out.status.success() {
return None;
}
let cmd_output = String::from_utf8_lossy(&p_out.stdout);
let results_line = cmd_output.lines().nth(3).unwrap();
let mut res_iter = results_line.split_whitespace();
let mean = Duration::from_secs_f64(res_iter.nth(5).unwrap().parse::<f64>().unwrap() / 1000.);
let stddev = Duration::from_secs_f64(res_iter.nth(1).unwrap().parse::<f64>().unwrap() / 1000.);
Some(Timings { mean, stddev })
}
fn main() {
let args = std::env::args().collect::<Vec<_>>();
let t_bin = args
.get(1)
.expect("FATAL: No test binary or command; this shouldn't be manually invoked!");
if t_bin == "compare" {
compare_profiles(&args[2..]);
return;
}
let mut thresh = Importance::Iffy;
let mut out_kind = OutputKind::Markdown;
for arg in args.iter().skip(2) {
match arg.as_str() {
"--critical" => thresh = Importance::Critical,
"--important" => thresh = Importance::Important,
"--average" => thresh = Importance::Average,
"--iffy" => thresh = Importance::Iffy,
"--fluff" => thresh = Importance::Fluff,
"--quiet" => QUIET.store(true, Ordering::Relaxed),
s if s.starts_with("--json") => {
out_kind = OutputKind::Json(Path::new(
s.strip_prefix("--json=")
.expect("FATAL: Invalid json parameter; pass --json=ident"),
));
}
_ => (),
}
}
if !QUIET.load(Ordering::Relaxed) {
eprintln!("Starting perf check");
}
let mut output = Output::default();
let i = get_tests(t_bin);
let len = i.len();
for (idx, (ref t_name, ref t_mdata)) in i.enumerate() {
if !QUIET.load(Ordering::Relaxed) {
eprint!("\rProfiling test {}/{}", idx + 1, len);
}
let t_name_pretty = t_name.replace(consts::SUF_NORMAL, "");
let t_mdata = match parse_mdata(t_bin, t_mdata) {
Ok(mdata) => mdata,
Err(err) => fail!(output, t_name_pretty, err),
};
if t_mdata.importance < thresh {
fail!(output, t_name_pretty, t_mdata, FailKind::Skipped);
}
let final_iter_count = t_mdata.iterations.or_else(|| {
triage_test(t_bin, t_name, consts::NOISE_CUTOFF, |c| {
if let Some(c) = c.checked_mul(ITER_COUNT_MUL) {
Some(c)
} else {
eprintln!(
"WARNING: Ran nearly usize::MAX iterations of test {t_name_pretty}; skipping"
);
None
}
})
});
let Some(final_iter_count) = final_iter_count else {
fail!(output, t_name_pretty, t_mdata, FailKind::Triage);
};
if let Some(timings) = hyp_profile(t_bin, t_name, final_iter_count) {
output.success(t_name_pretty, t_mdata, final_iter_count, timings);
} else {
fail!(
output,
t_name_pretty,
t_mdata,
final_iter_count,
FailKind::Profile
);
}
}
if !QUIET.load(Ordering::Relaxed) {
if output.is_empty() {
eprintln!("Nothing to do.");
} else {
eprintln!();
}
}
if output.is_empty() {
return;
}
out_kind.log(&output, t_bin);
}