extern crate rustc_serialize;
extern crate docopt;
#[macro_use]
extern crate lazy_static;
extern crate regex;
#[macro_use]
extern crate prettytable;
use std::io::{self, BufRead, Read};
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process;
use docopt::Docopt;
use prettytable::Table;
use prettytable::format;
use benchmark::{Benchmarks, Benchmark};
use error::{Result, Error};
mod benchmark;
mod error;
macro_rules! eprintln {
($($tt:tt)*) => {{
use std::io::{Write, stderr};
writeln!(&mut stderr(), $($tt)*).unwrap();
}}
}
const USAGE: &'static str = r#"
Compares Rust micro-benchmark results.
Usage:
cargo-benchcmp [options] <old> <new>
cargo-benchcmp [options] <old> <new> <file>
cargo-benchcmp -h | --help
cargo-benchcmp --version
The first version takes two files and compares the common benchmarks.
The second version takes two benchmark name prefixes and one benchmark output
file, and compares the common benchmarks (as determined by comparing the
benchmark names with their prefixes stripped). Benchmarks not matching either
prefix are ignored completely.
If benchmark output is sent on stdin, then the second version is used and the
third file parameter is not needed.
Options:
-h, --help Show this help message and exit.
--version Show the version.
--threshold <n> Show only comparisons with a percentage change greater
than this threshold.
--variance Show the variance of each benchmark.
--improvements Show only improvements.
--regressions Show only regressions.
"#;
#[derive(Debug, RustcDecodable)]
struct Args {
arg_old: String,
arg_new: String,
arg_file: Option<String>,
flag_threshold: Option<u8>,
flag_variance: bool,
flag_improvements: bool,
flag_regressions: bool,
}
fn main() {
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.version(Some(version())).decode())
.unwrap_or_else(|e| e.exit());
if let Err(e) = args.run() {
eprintln!("{}", e);
process::exit(1);
}
}
impl Args {
fn run(&self) -> Result<()> {
let (name_old, name_new) = self.names();
let benches = try!(self.parse_benchmarks()).paired();
let mut output = Table::new();
output.set_format(*format::consts::FORMAT_CLEAN);
output.add_row(row![
d->"name",
format!("{} ns/iter", name_old),
format!("{} ns/iter", name_new),
r->"diff ns/iter",
r->"diff %"
]);
for c in benches.comparisons() {
let abs_per = (c.diff_ratio * 100f64).abs().trunc() as u8;
if self.flag_threshold.map_or(false, |t| abs_per < t)
|| self.flag_regressions && c.diff_ns <= 0
|| self.flag_improvements && c.diff_ns >= 0 {
continue;
}
output.add_row(c.to_row(self.flag_variance));
}
output.printstd();
if !benches.missing_old().is_empty() {
let missed = benches
.missing_old().iter().map(|b| b.name.to_string())
.collect::<Vec<String>>().join(", ");
eprintln!("WARNING: benchmarks in old but not in new: {}", missed);
}
if !benches.missing_new().is_empty() {
let missed = benches
.missing_new().iter().map(|b| b.name.to_string())
.collect::<Vec<String>>().join(", ");
eprintln!("WARNING: benchmarks in new but not in old: {}", missed);
}
Ok(())
}
fn parse_benchmarks(&self) -> Result<Benchmarks> {
if let Some(ref one_file) = self.arg_file {
if one_file == "-" {
let mut buf = String::new();
let stdin = io::stdin();
try!(stdin.lock().read_to_string(&mut buf));
self.parse_buf_benchmarks(&buf)
} else {
self.parse_file_benchmarks(one_file)
}
} else {
self.parse_old_new_benchmarks()
}
}
fn parse_old_new_benchmarks(&self) -> Result<Benchmarks> {
let bold = io::BufReader::new(try!(open_file(&self.arg_old)));
let bnew = io::BufReader::new(try!(open_file(&self.arg_new)));
let mut benches = Benchmarks::new();
for line in bold.lines() {
let line = try!(line);
if let Ok(bench) = line.parse() {
benches.add_old(bench);
}
}
for line in bnew.lines() {
let line = try!(line);
if let Ok(bench) = line.parse() {
benches.add_new(bench);
}
}
Ok(benches)
}
fn parse_file_benchmarks<P>(
&self,
file: P,
) -> Result<Benchmarks>
where P: AsRef<Path> {
let mut buf = String::new();
try!(try!(File::open(file)).read_to_string(&mut buf));
self.parse_buf_benchmarks(&buf)
}
fn parse_buf_benchmarks(&self, buf: &str) -> Result<Benchmarks> {
let mut benches = Benchmarks::new();
for line in buf.lines() {
let mut bench: Benchmark = match line.parse() {
Err(_) => continue,
Ok(bench) => bench,
};
if bench.name.starts_with(&self.arg_old) {
bench.name = bench.name[self.arg_old.len()..].to_string();
benches.add_old(bench);
} else if bench.name.starts_with(&self.arg_new) {
bench.name = bench.name[self.arg_new.len()..].to_string();
benches.add_new(bench);
}
}
Ok(benches)
}
fn names(&self) -> (String, String) {
let arg_old =
if self.arg_old.is_empty() {
"old".to_string()
} else {
self.arg_old.to_string()
};
let arg_new =
if self.arg_new.is_empty() {
"new".to_string()
} else {
self.arg_new.to_string()
};
let (old, new) = (Path::new(&arg_old), Path::new(&arg_new));
if old.iter().count() <= 1 || new.iter().count() <= 1 {
return (arg_old.clone(), arg_new.clone());
}
let (mut uold, mut unew) = (vec![], vec![]);
for (o, n) in old.iter().rev().zip(new.iter().rev()) {
uold.push(o.to_string_lossy().into_owned());
unew.push(n.to_string_lossy().into_owned());
if o != n {
break;
}
}
if uold.is_empty() || unew.is_empty() {
return (arg_old.clone(), arg_new.clone());
}
uold.reverse();
unew.reverse();
let pold: PathBuf = uold.into_iter().collect();
let pnew: PathBuf = unew.into_iter().collect();
(pold.display().to_string(), pnew.display().to_string())
}
}
fn version() -> String {
let (maj, min, pat) = (
option_env!("CARGO_PKG_VERSION_MAJOR"),
option_env!("CARGO_PKG_VERSION_MINOR"),
option_env!("CARGO_PKG_VERSION_PATCH"),
);
match (maj, min, pat) {
(Some(maj), Some(min), Some(pat)) =>
format!("{}.{}.{}", maj, min, pat),
_ => "".to_owned(),
}
}
fn open_file<P: AsRef<Path>>(path: P) -> Result<File> {
File::open(&path).map_err(|err| {
Error::OpenFile {
path: path.as_ref().to_path_buf(),
err: err,
}
})
}