obnam_benchmark/
report.rs

1use crate::result::{Measurement, Operation, SuiteMeasurements};
2use std::collections::HashSet;
3use std::io::Write;
4use std::iter::FromIterator;
5
6const COMMIT_DIGITS: usize = 7;
7
8#[derive(Debug, Default)]
9pub struct Reporter {
10    results: Vec<SuiteMeasurements>,
11}
12
13impl Reporter {
14    pub fn push(&mut self, result: SuiteMeasurements) {
15        self.results.push(result);
16    }
17
18    pub fn write(&self, f: &mut dyn Write) -> Result<(), std::io::Error> {
19        // Summary tables of all benchmarks.
20        writeln!(f, "# Summaries of all benchmark runs")?;
21        let hosts = self.hosts();
22        for bench in self.benchmarks() {
23            writeln!(f)?;
24            writeln!(f, "## Benchmark {}", bench)?;
25            for op in [Operation::Backup, Operation::Restore] {
26                for host in hosts.iter() {
27                    writeln!(f)?;
28                    writeln!(f, "Table: {:?} on host {}, times in ms", op, host)?;
29                    writeln!(f)?;
30                    let mut want_headings = true;
31                    for r in self.results_for(host) {
32                        // Column data
33                        let cols = self.durations(r, &bench, op);
34
35                        if want_headings {
36                            want_headings = false;
37                            self.headings(f, &cols)?;
38                        }
39                        self.rows(f, r, &cols)?;
40                    }
41                }
42            }
43        }
44
45        writeln!(f)?;
46        writeln!(f, "# Individual benchmark runs")?;
47        for host in self.hosts() {
48            writeln!(f)?;
49            writeln!(f, "## Host `{}`", host)?;
50            for r in self.results_for(&host) {
51                writeln!(f)?;
52                writeln!(f, "### Benchmark run {}", r.timestamp())?;
53                writeln!(f)?;
54                writeln!(f, "* CPUs: {}", r.cpus())?;
55                writeln!(f, "* RAM: {} MiB", r.ram() / 1024 / 1024)?;
56                for bench in r.benchmark_names() {
57                    writeln!(f)?;
58                    writeln!(f, "#### Benchmark `{}`", bench)?;
59                    writeln!(f)?;
60                    for opm in r.ops().filter(|o| o.name() == bench) {
61                        let op = opm.op();
62                        match op {
63                            Operation::Backup => {
64                                writeln!(f, "* Backup: {} ms", opm.millis())?;
65                            }
66                            Operation::Restore => {
67                                writeln!(f, "* Restore: {} ms", opm.millis())?;
68                            }
69                            _ => continue,
70                        }
71                        for m in opm.iter() {
72                            match m {
73                                Measurement::DurationMs(_) => (),
74                                Measurement::TotalFiles(n) => {
75                                    writeln!(f, "  * files: {}", n)?;
76                                }
77                                &Measurement::TotalData(n) => {
78                                    writeln!(f, "  * data: {} bytes", n)?;
79                                }
80                            }
81                        }
82                    }
83
84                    for op in [Operation::Backup, Operation::Restore] {
85                        writeln!(f)?;
86                        writeln!(f, "Table: {:?}", op)?;
87                        writeln!(f)?;
88                        let cols = self.durations(r, &bench, op);
89                        self.headings(f, &cols)?;
90                        self.rows(f, r, &cols)?;
91                    }
92                }
93            }
94        }
95
96        Ok(())
97    }
98
99    fn durations(&self, r: &SuiteMeasurements, bench: &str, op: Operation) -> Vec<u128> {
100        r.ops()
101            .filter(|r| r.name() == bench)
102            .filter(|r| r.op() == op)
103            .map(|r| r.millis())
104            .collect()
105    }
106
107    fn headings(&self, f: &mut dyn Write, durations: &[u128]) -> Result<(), std::io::Error> {
108        // Column headings.
109        let mut headings: Vec<String> = durations
110            .iter()
111            .enumerate()
112            .map(|(i, _)| format!("step {}", i))
113            .collect();
114        headings.insert(0, "Commit".to_string());
115        headings.insert(0, "Version".to_string());
116        for h in headings.iter() {
117            write!(f, "| {}", h)?;
118        }
119        writeln!(f, "|")?;
120
121        // Heading separator.
122        for _ in headings.iter() {
123            write!(f, "|----")?;
124        }
125        writeln!(f, "|")?;
126        Ok(())
127    }
128
129    fn rows(
130        &self,
131        f: &mut dyn Write,
132        r: &SuiteMeasurements,
133        durations: &[u128],
134    ) -> Result<(), std::io::Error> {
135        write!(f, "| {}", pretty_version(r.obnam_version()))?;
136        write!(f, "| {}", pretty_commit(r.obnam_commit()))?;
137        for ms in durations.iter() {
138            write!(f, "| {}", ms)?;
139        }
140        writeln!(f, "|")?;
141        Ok(())
142    }
143
144    fn benchmarks(&self) -> Vec<String> {
145        let mut names = HashSet::new();
146        for r in self.results.iter() {
147            for opm in r.ops() {
148                names.insert(opm.name());
149            }
150        }
151        let mut names: Vec<String> = names.iter().map(|x| x.to_string()).collect();
152        names.sort();
153        names
154    }
155
156    fn hosts(&self) -> Vec<String> {
157        let names: HashSet<&str> = HashSet::from_iter(self.results.iter().map(|r| r.hostname()));
158        let mut names: Vec<String> = names.iter().map(|x| x.to_string()).collect();
159        names.sort();
160        names
161    }
162
163    fn results_for<'a>(&'a self, hostname: &'a str) -> impl Iterator<Item = &'a SuiteMeasurements> {
164        self.results
165            .iter()
166            .filter(move |r| r.hostname() == hostname)
167    }
168}
169
170fn pretty_version(v: &str) -> String {
171    if let Some(v) = v.strip_prefix("obnam-backup ") {
172        v.to_string()
173    } else {
174        v.to_string()
175    }
176}
177
178fn pretty_commit(c: Option<&str>) -> String {
179    if let Some(c) = c {
180        c[..COMMIT_DIGITS].to_string()
181    } else {
182        "".to_string()
183    }
184}