wgpu_benchmark/
print.rs

1use std::collections::HashMap;
2use std::io::Write;
3
4use termcolor::{Color, ColorSpec, StandardStream, WriteColor};
5
6use crate::SubBenchResult;
7
8#[derive(Default, Clone)]
9struct Delta {
10    throughput_change_str: String,
11    throughput_change: f64,
12    time_change_str: String,
13    time_change: f64,
14}
15
16impl Delta {
17    fn new(previous: &SubBenchResult, current: &SubBenchResult) -> Self {
18        let prev_throughput = previous.throughput_per_second();
19        let curr_throughput = current.throughput_per_second();
20        let delta_throughput = if prev_throughput != 0.0 {
21            (curr_throughput - prev_throughput) / prev_throughput * 100.0
22        } else {
23            0.0
24        };
25        let throughput_change = format!(" ({delta_throughput:+.2}%)");
26
27        let prev_time = previous.avg_duration_per_iteration;
28        let curr_time = current.avg_duration_per_iteration;
29        let delta_time = if prev_time.as_nanos() != 0 {
30            (curr_time.as_secs_f64() - prev_time.as_secs_f64()) / prev_time.as_secs_f64() * 100.0
31        } else {
32            0.0
33        };
34
35        let time_change = format!("{delta_time:+.2}%; ");
36
37        Delta {
38            throughput_change_str: throughput_change,
39            throughput_change: delta_throughput,
40            time_change_str: time_change,
41            time_change: delta_time,
42        }
43    }
44}
45
46/// Get a color spec for the given change percentage.
47///
48/// Positive changes are red (regression), negative changes are green (improvement).
49/// This represents changes for time durations. For throughput changes, the sign should be inverted
50/// before passing to this method.
51fn get_change_color(percent_change: f64) -> ColorSpec {
52    let mut color_spec = ColorSpec::new();
53    if percent_change > 3.0 {
54        color_spec.set_fg(Some(Color::Red));
55    } else if percent_change < -3.0 {
56        color_spec.set_fg(Some(Color::Green));
57    } else {
58        color_spec.set_fg(Some(Color::Yellow));
59    }
60    if percent_change.abs() > 15.0 {
61        color_spec.set_intense(true);
62    }
63    color_spec
64}
65
66pub fn print_results(
67    stdout: &mut StandardStream,
68    results: &[SubBenchResult],
69    previous_results: Option<&[SubBenchResult]>,
70) {
71    let mut deltas = HashMap::new();
72    if let Some(previous_results) = previous_results {
73        for result in results {
74            if let Some(previous_result) = previous_results.iter().find(|r| r.name == result.name) {
75                deltas.insert(result.name.clone(), Delta::new(previous_result, result));
76            }
77        }
78    }
79
80    let longest_throughput_change_len = deltas
81        .values()
82        .map(|d| d.throughput_change_str.len())
83        .max()
84        .unwrap_or(0);
85    let longest_time_change_len = deltas
86        .values()
87        .map(|d| d.time_change_str.len())
88        .max()
89        .unwrap_or(0);
90
91    let longest_name_len = results.iter().map(|r| r.name.len()).max().unwrap_or(0);
92    let duration_strings: Vec<String> = results
93        .iter()
94        .map(|r| format!("{:.3?}", r.avg_duration_per_iteration))
95        .collect();
96    let longest_duration_len = duration_strings.iter().map(|s| s.len()).max().unwrap_or(0);
97
98    let iterations_strings: Vec<String> = results
99        .iter()
100        .map(|r| format!("{}", r.iterations))
101        .collect();
102    let longest_iterations_len = iterations_strings
103        .iter()
104        .map(|s| s.len())
105        .max()
106        .unwrap_or(0);
107
108    let throughput_strings: Vec<String> = results
109        .iter()
110        .map(|r| {
111            let throughput_per_second = r.throughput_count_per_iteration as f64
112                / r.avg_duration_per_iteration.as_secs_f64();
113            human_scale(throughput_per_second)
114        })
115        .collect();
116    let longest_throughput_len = throughput_strings
117        .iter()
118        .map(|s| s.len())
119        .max()
120        .unwrap_or(0);
121
122    let longest_throughput_unit_len = results
123        .iter()
124        .map(|r| r.throughput_unit.len())
125        .max()
126        .unwrap_or(0);
127
128    for (i, result) in results.iter().enumerate() {
129        let delta = deltas.get(&result.name).cloned().unwrap_or_default();
130        let time_color = get_change_color(delta.time_change);
131        let throughput_color = get_change_color(-delta.throughput_change);
132
133        stdout
134            .set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))
135            .unwrap();
136        write!(stdout, "    {:>longest_name_len$}: ", result.name).unwrap();
137
138        stdout.set_color(&time_color).unwrap();
139        write!(stdout, "{:>longest_duration_len$} ", duration_strings[i],).unwrap();
140        stdout.reset().unwrap();
141        write!(stdout, "(").unwrap();
142        stdout.set_color(&time_color).unwrap();
143        write!(
144            stdout,
145            "{:>longest_time_change_len$}",
146            delta.time_change_str
147        )
148        .unwrap();
149        stdout.reset().unwrap();
150
151        write!(
152            stdout,
153            "over {:>longest_iterations_len$} iter) ",
154            result.iterations,
155        )
156        .unwrap();
157
158        stdout.set_color(&throughput_color).unwrap();
159        write!(stdout, "{:>longest_throughput_len$}", throughput_strings[i]).unwrap();
160        stdout.reset().unwrap();
161        write!(
162            stdout,
163            " {:>longest_throughput_unit_len$}/s",
164            result.throughput_unit,
165        )
166        .unwrap();
167        stdout.set_color(&throughput_color).unwrap();
168        writeln!(
169            stdout,
170            "{:>longest_throughput_change_len$}",
171            delta.throughput_change_str
172        )
173        .unwrap();
174    }
175    println!();
176}
177
178fn human_scale(value: f64) -> String {
179    const PREFIXES: &[&str] = &["", "K", "M", "G", "T", "P"];
180
181    if value == 0.0 {
182        return "0".to_string();
183    }
184
185    let abs_value = value.abs();
186    let exponent = (abs_value.log10() / 3.0).floor() as usize;
187    let prefix_index = exponent.min(PREFIXES.len() - 1);
188
189    let scaled = value / 10_f64.powi((prefix_index * 3) as i32);
190
191    // Determine decimal places for 3 significant figures
192    let decimal_places = if scaled.abs() >= 100.0 {
193        0
194    } else if scaled.abs() >= 10.0 {
195        1
196    } else {
197        2
198    };
199
200    format!(
201        "{:.prec$}{}",
202        scaled,
203        PREFIXES[prefix_index],
204        prec = decimal_places
205    )
206}