obnam_benchmark/
report.rs1use 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 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 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 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 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}