use std::cmp;
use std::str::FromStr;
use prettytable::row::Row;
use regex::Regex;
#[derive(Clone, Debug)]
pub struct Benchmarks {
old: Vec<Benchmark>,
new: Vec<Benchmark>,
}
impl Benchmarks {
pub fn new() -> Benchmarks {
Benchmarks { old: vec![], new: vec![] }
}
pub fn add_old(&mut self, b: Benchmark) {
self.old.push(b);
}
pub fn add_new(&mut self, b: Benchmark) {
self.new.push(b);
}
pub fn paired(self) -> PairedBenchmarks {
PairedBenchmarks::from(self)
}
}
#[derive(Clone, Debug)]
pub struct PairedBenchmarks {
cmps: Vec<Comparison>,
unpaired_old: Vec<Benchmark>,
unpaired_new: Vec<Benchmark>,
}
impl From<Benchmarks> for PairedBenchmarks {
fn from(mut benches: Benchmarks) -> PairedBenchmarks {
benches.old.sort();
benches.new.sort();
let ov = Overlap::find(benches.old, benches.new, Benchmark::cmp);
let cmps = ov.overlap.into_iter().map(|(a, b)| a.compare(b)).collect();
PairedBenchmarks {
cmps: cmps,
unpaired_old: ov.left,
unpaired_new: ov.right,
}
}
}
impl PairedBenchmarks {
pub fn comparisons(&self) -> &[Comparison] {
&self.cmps
}
pub fn missing_old(&self) -> &[Benchmark] {
&self.unpaired_old
}
pub fn missing_new(&self) -> &[Benchmark] {
&self.unpaired_new
}
}
#[derive(Clone, Debug)]
pub struct Benchmark {
pub name: String,
pub ns: u64,
pub variance: u64,
pub throughput: Option<u64>,
}
impl Eq for Benchmark {}
impl PartialEq for Benchmark {
fn eq(&self, other: &Benchmark) -> bool {
self.name == other.name
}
}
impl Ord for Benchmark {
fn cmp(&self, other: &Benchmark) -> cmp::Ordering {
self.partial_cmp(other).unwrap()
}
}
impl PartialOrd for Benchmark {
fn partial_cmp(&self, other: &Benchmark) -> Option<cmp::Ordering> {
self.name.partial_cmp(&other.name)
}
}
lazy_static! {
static ref BENCHMARK_REGEX: Regex = Regex::new(r##"(?x)
test\s+(?P<name>\S+) # test mod::test_name
\s+...\sbench:\s+(?P<ns>[0-9,]+)\s+ns/iter # ... bench: 1234 ns/iter
\s+\(\+/-\s+(?P<variance>[0-9,]+)\) # (+/- 4321)
(?:\s+=\s+(?P<throughput>[0-9,]+)\sMB/s)? # = 2314
"##).unwrap();
}
impl FromStr for Benchmark {
type Err = ();
fn from_str(line: &str) -> Result<Benchmark, ()> {
let caps = match BENCHMARK_REGEX.captures(line) {
None => return Err(()),
Some(caps) => caps,
};
let ns = match parse_commas(&caps["ns"]) {
None => return Err(()),
Some(ns) => ns,
};
let variance = match parse_commas(&caps["variance"]) {
None => return Err(()),
Some(variance) => variance,
};
let throughput = caps.name("throughput").and_then(parse_commas);
Ok(Benchmark {
name: caps["name"].to_string(),
ns: ns,
variance: variance,
throughput: throughput,
})
}
}
impl Benchmark {
pub fn compare(self, new: Benchmark) -> Comparison {
let diff_ns = new.ns as i64 - self.ns as i64;
let diff_ratio = diff_ns as f64 / self.ns as f64;
Comparison {
old: self,
new: new,
diff_ns: diff_ns,
diff_ratio: diff_ratio,
}
}
fn fmt_ns(&self, variance: bool) -> String {
let mut res = commafy(self.ns);
if variance {
res = format!("{} (+/- {})", res, self.variance);
}
if let Some(throughput) = self.throughput {
res = format!("{} ({} MB/s)", res, throughput);
}
res
}
}
#[derive(Clone, Debug)]
pub struct Comparison {
pub old: Benchmark,
pub new: Benchmark,
pub diff_ns: i64,
pub diff_ratio: f64,
}
impl Comparison {
pub fn to_row(&self, variance: bool) -> Row {
let name = format!("{}", self.old.name);
let fst_ns = format!("{}", self.old.fmt_ns(variance));
let snd_ns = format!("{}", self.new.fmt_ns(variance));
let diff_ratio = format!("{:.2}%", self.diff_ratio * 100f64);
let diff_ns = {
let diff_ns = commafy(self.diff_ns.abs() as u64);
if self.diff_ns < 0 {
format!("-{}", diff_ns)
} else {
diff_ns
}
};
row![name, fst_ns, snd_ns, r->diff_ns, r->diff_ratio]
}
}
struct Overlap<T> {
left: Vec<T>,
overlap: Vec<(T, T)>,
right: Vec<T>,
}
impl<T> Overlap<T> {
fn find<F>(mut left: Vec<T>, mut right: Vec<T>, mut fun: F) -> Overlap<T>
where F: FnMut(&T, &T) -> cmp::Ordering {
use std::cmp::Ordering::*;
let (mut rleft, mut rright, mut overlap) = (vec![], vec![], vec![]);
loop {
match (left.pop(), right.pop()) {
(None, None) => break,
(None, Some(right_item)) => rright.push(right_item),
(Some(left_item), None) => rleft.push(left_item),
(Some(left_item), Some(right_item)) => {
match fun(&right_item, &left_item) {
Less => {
rleft.push(left_item);
right.push(right_item);
}
Equal => overlap.push((left_item, right_item)),
Greater => {
rright.push(right_item);
left.push(left_item);
}
}
}
}
}
rleft.reverse();
rright.reverse();
overlap.reverse();
Overlap {
left: rleft,
overlap: overlap,
right: rright,
}
}
}
fn parse_commas(s: &str) -> Option<u64> {
drop_commas(s).parse().ok()
}
fn drop_commas(s: &str) -> String {
s.chars().filter(|&b| b != ',').collect()
}
fn commafy(n: u64) -> String {
let mut with_commas = vec![];
let dits: Vec<u8> = n.to_string().into_bytes().into_iter().rev().collect();
let mut dits = &*dits;
loop {
if dits.len() < 3 {
with_commas.extend(dits);
break;
}
let piece = &dits[0..3];
dits = &dits[3..];
with_commas.extend(piece);
if piece.len() == 3 && !dits.is_empty() && dits[0] != b'-' {
with_commas.push(b',');
}
}
with_commas.reverse();
String::from_utf8(with_commas).unwrap()
}