use crate::result::{Measurement, Operation, SuiteMeasurements}; use std::collections::HashSet; use std::io::Write; use std::iter::FromIterator; const COMMIT_DIGITS: usize = 7; #[derive(Debug, Default)] pub struct Reporter { results: Vec, } impl Reporter { pub fn push(&mut self, result: SuiteMeasurements) { self.results.push(result); } pub fn write(&self, f: &mut dyn Write) -> Result<(), std::io::Error> { // Summary tables of all benchmarks. writeln!(f, "# Summaries of all benchmark runs")?; let hosts = self.hosts(); for bench in self.benchmarks() { writeln!(f)?; writeln!(f, "## Benchmark {}", bench)?; for op in [Operation::Backup, Operation::Restore] { for host in hosts.iter() { writeln!(f)?; writeln!(f, "Table: {:?} on host {}, times in ms", op, host)?; writeln!(f)?; let mut want_headings = true; for r in self.results_for(host) { // Column data let cols = self.durations(r, &bench, op); if want_headings { want_headings = false; self.headings(f, &cols)?; } self.rows(f, r, &cols)?; } } } } writeln!(f)?; writeln!(f, "# Individual benchmark runs")?; for host in self.hosts() { writeln!(f)?; writeln!(f, "## Host `{}`", host)?; for r in self.results_for(&host) { writeln!(f)?; writeln!(f, "### Benchmark run {}", r.timestamp())?; writeln!(f)?; writeln!(f, "* CPUs: {}", r.cpus())?; writeln!(f, "* RAM: {} MiB", r.ram() / 1024 / 1024)?; for bench in r.benchmark_names() { writeln!(f)?; writeln!(f, "#### Benchmark `{}`", bench)?; writeln!(f)?; for opm in r.ops().filter(|o| o.name() == bench) { let op = opm.op(); match op { Operation::Backup => { writeln!(f, "* Backup: {} ms", opm.millis())?; } Operation::Restore => { writeln!(f, "* Restore: {} ms", opm.millis())?; } _ => continue, } for m in opm.iter() { match m { Measurement::DurationMs(_) => (), Measurement::TotalFiles(n) => { writeln!(f, " * files: {}", n)?; } &Measurement::TotalData(n) => { writeln!(f, " * data: {} bytes", n)?; } } } } for op in [Operation::Backup, Operation::Restore] { writeln!(f)?; writeln!(f, "Table: {:?}", op)?; writeln!(f)?; let cols = self.durations(r, &bench, op); self.headings(f, &cols)?; self.rows(f, r, &cols)?; } } } } Ok(()) } fn durations(&self, r: &SuiteMeasurements, bench: &str, op: Operation) -> Vec { r.ops() .filter(|r| r.name() == bench) .filter(|r| r.op() == op) .map(|r| r.millis()) .collect() } fn headings(&self, f: &mut dyn Write, durations: &[u128]) -> Result<(), std::io::Error> { // Column headings. let mut headings: Vec = durations .iter() .enumerate() .map(|(i, _)| format!("step {}", i)) .collect(); headings.insert(0, "Commit".to_string()); headings.insert(0, "Version".to_string()); for h in headings.iter() { write!(f, "| {}", h)?; } writeln!(f, "|")?; // Heading separator. for _ in headings.iter() { write!(f, "|----")?; } writeln!(f, "|")?; Ok(()) } fn rows( &self, f: &mut dyn Write, r: &SuiteMeasurements, durations: &[u128], ) -> Result<(), std::io::Error> { write!(f, "| {}", pretty_version(r.obnam_version()))?; write!(f, "| {}", pretty_commit(r.obnam_commit()))?; for ms in durations.iter() { write!(f, "| {}", ms)?; } writeln!(f, "|")?; Ok(()) } fn benchmarks(&self) -> Vec { let mut names = HashSet::new(); for r in self.results.iter() { for opm in r.ops() { names.insert(opm.name()); } } let mut names: Vec = names.iter().map(|x| x.to_string()).collect(); names.sort(); names } fn hosts(&self) -> Vec { let names: HashSet<&str> = HashSet::from_iter(self.results.iter().map(|r| r.hostname())); let mut names: Vec = names.iter().map(|x| x.to_string()).collect(); names.sort(); names } fn results_for<'a>(&'a self, hostname: &'a str) -> impl Iterator { self.results .iter() .filter(move |r| r.hostname() == hostname) } } fn pretty_version(v: &str) -> String { if let Some(v) = v.strip_prefix("obnam-backup ") { v.to_string() } else { v.to_string() } } fn pretty_commit(c: Option<&str>) -> String { if let Some(c) = c { c[..COMMIT_DIGITS].to_string() } else { "".to_string() } }