use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
pub fn mbs(bytes: u64, dur: Duration) -> f64 {
let secs = dur.as_secs_f64();
if secs <= 0.0 {
return 0.0;
}
(bytes as f64 / 1_000_000.0) / secs
}
pub fn time_rust<F: FnMut()>(reps: usize, mut f: F) -> Duration {
let reps = reps.max(1);
let mut best = Duration::MAX;
for _ in 0..reps {
let t = Instant::now();
f();
best = best.min(t.elapsed());
}
best
}
pub fn tool_available(tool: &str) -> bool {
let Ok(path) = std::env::var("PATH") else { return false };
std::env::split_paths(&path).any(|dir| {
let p = dir.join(tool);
p.is_file() && is_executable(&p)
})
}
#[cfg(unix)]
fn is_executable(p: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(p)
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(_p: &Path) -> bool {
true
}
pub fn time_command_best<F: FnMut() -> Command>(
tool: &str,
reps: usize,
mut build: F,
) -> Option<Duration> {
if !tool_available(tool) {
return None;
}
let reps = reps.max(1);
let mut best = Duration::MAX;
for _ in 0..reps {
let mut cmd = build();
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
let t = Instant::now();
let status = cmd.status().ok()?;
let elapsed = t.elapsed();
if !status.success() {
return None;
}
best = best.min(elapsed);
}
Some(best)
}
#[derive(Debug, Clone)]
pub struct Comparison {
pub bytes: u64,
pub ours_label: String,
pub ours: Duration,
pub legacy_label: String,
pub legacy: Option<Duration>,
}
impl Comparison {
pub fn into_result(self, name: &str) -> crate::bench::BenchResult {
let mut m = serde_json::Map::new();
let ours_mbs = mbs(self.bytes, self.ours);
m.insert(format!("{}_mbs", self.ours_label), serde_json::json!(round2(ours_mbs)));
if let Some(leg) = self.legacy {
let leg_mbs = mbs(self.bytes, leg);
m.insert(format!("{}_mbs", self.legacy_label), serde_json::json!(round2(leg_mbs)));
let speedup = if leg_mbs > 0.0 { ours_mbs / leg_mbs } else { 0.0 };
m.insert("speedup_x".into(), serde_json::json!(round2(speedup)));
}
crate::bench::BenchResult { name: name.to_string(), metrics: m }
}
}
fn round2(f: f64) -> f64 {
(f * 100.0).round() / 100.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mbs_basic() {
assert!((mbs(1_000_000, Duration::from_secs(1)) - 1.0).abs() < 1e-9);
assert_eq!(mbs(123, Duration::ZERO), 0.0);
}
#[test]
fn missing_tool_is_none() {
assert!(!tool_available("definitely-not-a-real-tool-xyz"));
let d = time_command_best("definitely-not-a-real-tool-xyz", 1, || {
Command::new("definitely-not-a-real-tool-xyz")
});
assert!(d.is_none());
}
#[test]
fn comparison_pairs_metrics_and_speedup() {
let cmp = Comparison {
bytes: 2_000_000,
ours_label: "ljar".into(),
ours: Duration::from_millis(100), legacy_label: "unzip".into(),
legacy: Some(Duration::from_millis(200)), };
let r = cmp.into_result("jar_2000");
assert_eq!(r.name, "jar_2000");
assert_eq!(r.metrics["ljar_mbs"].as_f64().unwrap(), 20.0);
assert_eq!(r.metrics["unzip_mbs"].as_f64().unwrap(), 10.0);
assert_eq!(r.metrics["speedup_x"].as_f64().unwrap(), 2.0);
}
#[test]
fn comparison_without_legacy_is_ours_only() {
let cmp = Comparison {
bytes: 1_000_000,
ours_label: "lgz".into(),
ours: Duration::from_millis(500),
legacy_label: "pigz".into(),
legacy: None,
};
let r = cmp.into_result("gz_x");
assert!(r.metrics.contains_key("lgz_mbs"));
assert!(!r.metrics.contains_key("pigz_mbs"));
assert!(!r.metrics.contains_key("speedup_x"));
}
}