Skip to content

Commit 57d796a

Browse files
committed
Add interactive HTML export format
Implements a new HTML exporter that generates a standalone, interactive report with multiple visualizations: - Summary table with relative performance metrics - Boxplots, histograms and time progression charts - Advanced statistics and parameter analysis The report uses embedded JavaScript (Plotly.js) for interactive visualizations
1 parent 3cedcc3 commit 57d796a

File tree

6 files changed

+1062
-0
lines changed

6 files changed

+1062
-0
lines changed

src/cli.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,15 @@ fn build_command() -> Command {
311311
.help("Export the timing summary statistics as an Emacs org-mode table to the given FILE. \
312312
The output time unit can be changed using the --time-unit option."),
313313
)
314+
.arg(
315+
Arg::new("export-html")
316+
.long("export-html")
317+
.action(ArgAction::Set)
318+
.value_name("FILE")
319+
.value_hint(ValueHint::FilePath)
320+
.help("Export the timing summary statistics as an HTML page with interactive charts to the given FILE. \
321+
The charts include a boxplot of all results, as well as histograms for individual commands."),
322+
)
314323
.arg(
315324
Arg::new("show-output")
316325
.long("show-output")

src/export/html.rs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
use super::Exporter;
2+
use crate::benchmark::benchmark_result::BenchmarkResult;
3+
use crate::benchmark::relative_speed;
4+
use crate::options::SortOrder;
5+
use crate::util::units::Unit;
6+
7+
use anyhow::Result;
8+
use serde_json;
9+
10+
/// HTML exporter for benchmark results.
11+
///
12+
/// Generates a standalone HTML file with interactive visualizations using Plotly.js
13+
#[derive(Default)]
14+
pub struct HtmlExporter {}
15+
16+
impl Exporter for HtmlExporter {
17+
fn serialize(
18+
&self,
19+
results: &[BenchmarkResult],
20+
unit: Option<Unit>,
21+
sort_order: SortOrder,
22+
) -> Result<Vec<u8>> {
23+
// Include static assets
24+
let template = include_str!("html_template.html");
25+
let css = include_str!("html_styles.css");
26+
let js = include_str!("html_renderer.js");
27+
28+
// Build the HTML document with embedded resources
29+
let mut html = template.to_string();
30+
html = html.replace("/* CSS will be embedded here */", css);
31+
html = html.replace("// JS will be embedded here", js);
32+
33+
// Determine the appropriate unit if not specified
34+
let unit = unit.unwrap_or_else(|| determine_unit_from_results(results));
35+
36+
// Compute relative speeds and sort results
37+
let entries = relative_speed::compute(results, sort_order);
38+
39+
// Get the reference command if there is one
40+
let reference_command = entries
41+
.iter()
42+
.find(|e| e.is_reference)
43+
.map_or("", |e| &e.result.command);
44+
45+
// Serialize benchmark data to JSON for JavaScript consumption
46+
let json_data = serde_json::to_string(
47+
&entries
48+
.iter()
49+
.map(|entry| &entry.result)
50+
.collect::<Vec<_>>(),
51+
)?;
52+
53+
// Replace placeholder with benchmark data and unit information
54+
let data_script = format!(
55+
"const benchmarkData = {};\n\
56+
const unitShortName = \"{}\";\n\
57+
const unitName = \"{}\";\n\
58+
const referenceCommand = \"{}\";\n\
59+
const unitFactor = {};",
60+
json_data,
61+
get_unit_short_name(unit),
62+
get_unit_name(unit),
63+
reference_command,
64+
get_unit_factor(unit)
65+
);
66+
67+
html = html.replace("<!-- DATA_PLACEHOLDER -->", &data_script);
68+
69+
Ok(html.into_bytes())
70+
}
71+
}
72+
73+
/// Returns the full name of a time unit
74+
fn get_unit_name(unit: Unit) -> &'static str {
75+
match unit {
76+
Unit::Second => "second",
77+
Unit::MilliSecond => "millisecond",
78+
Unit::MicroSecond => "microsecond",
79+
}
80+
}
81+
82+
/// Returns the abbreviated symbol for a time unit
83+
fn get_unit_short_name(unit: Unit) -> &'static str {
84+
match unit {
85+
Unit::Second => "s",
86+
Unit::MilliSecond => "ms",
87+
Unit::MicroSecond => "μs",
88+
}
89+
}
90+
91+
/// Returns the conversion factor from seconds to the specified unit
92+
fn get_unit_factor(unit: Unit) -> f64 {
93+
match unit {
94+
Unit::Second => 1.0,
95+
Unit::MilliSecond => 1000.0,
96+
Unit::MicroSecond => 1000000.0,
97+
}
98+
}
99+
100+
/// Automatically determines the most appropriate time unit based on benchmark results
101+
fn determine_unit_from_results(results: &[BenchmarkResult]) -> Unit {
102+
results
103+
.first()
104+
.map(|first_result| {
105+
// Choose unit based on the magnitude of the mean time
106+
let mean = first_result.mean;
107+
if mean < 0.001 {
108+
Unit::MicroSecond
109+
} else if mean < 1.0 {
110+
Unit::MilliSecond
111+
} else {
112+
Unit::Second
113+
}
114+
})
115+
.unwrap_or(Unit::Second) // Default to seconds if no results
116+
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
use std::collections::BTreeMap;
122+
123+
#[test]
124+
fn test_html_export() {
125+
// Create sample benchmark results
126+
let results = vec![
127+
create_test_benchmark("test command 1", 1.5, None),
128+
create_test_benchmark_with_param("test command 2", 2.5, "size", "10"),
129+
];
130+
131+
// Create HTML exporter
132+
let exporter = HtmlExporter::default();
133+
134+
// Test with seconds unit
135+
let html = export_and_get_html(&exporter, &results, Unit::Second);
136+
137+
// Verify HTML structure and content
138+
assert_html_structure(&html);
139+
assert_contains_benchmark_data(&html, &results);
140+
assert_unit_information(&html, "s", "second", "1");
141+
142+
// Test with milliseconds unit
143+
let html = export_and_get_html(&exporter, &results, Unit::MilliSecond);
144+
assert_unit_information(&html, "ms", "millisecond", "1000");
145+
}
146+
147+
/// Helper function to create a test benchmark result
148+
fn create_test_benchmark(
149+
command: &str,
150+
mean: f64,
151+
parameters: Option<BTreeMap<String, String>>,
152+
) -> BenchmarkResult {
153+
BenchmarkResult {
154+
command: command.to_string(),
155+
command_with_unused_parameters: command.to_string(),
156+
mean,
157+
stddev: Some(mean * 0.1),
158+
median: mean * 0.99,
159+
min: mean * 0.8,
160+
max: mean * 1.2,
161+
user: mean * 0.9,
162+
system: mean * 0.1,
163+
memory_usage_byte: None,
164+
times: Some(vec![mean * 0.8, mean * 0.9, mean, mean * 1.1, mean * 1.2]),
165+
exit_codes: vec![Some(0); 5],
166+
parameters: parameters.unwrap_or_default(),
167+
}
168+
}
169+
170+
/// Helper function to create a test benchmark with a parameter
171+
fn create_test_benchmark_with_param(
172+
command: &str,
173+
mean: f64,
174+
param_name: &str,
175+
param_value: &str,
176+
) -> BenchmarkResult {
177+
let mut params = BTreeMap::new();
178+
params.insert(param_name.to_string(), param_value.to_string());
179+
create_test_benchmark(command, mean, Some(params))
180+
}
181+
182+
/// Helper function to export benchmark results and get HTML
183+
fn export_and_get_html(
184+
exporter: &HtmlExporter,
185+
results: &[BenchmarkResult],
186+
unit: Unit,
187+
) -> String {
188+
let html_bytes = exporter
189+
.serialize(results, Some(unit), SortOrder::MeanTime)
190+
.expect("HTML export failed");
191+
String::from_utf8(html_bytes).expect("HTML is not valid UTF-8")
192+
}
193+
194+
/// Assert that the HTML has the expected structure
195+
fn assert_html_structure(html: &str) {
196+
assert!(html.contains("<!DOCTYPE html>"));
197+
assert!(html.contains("<html lang=\"en\">"));
198+
assert!(html.contains("<title>Hyperfine Benchmark Results</title>"));
199+
assert!(html.contains("<div class=\"container\">"));
200+
assert!(html.contains("function renderSummaryTable()"));
201+
assert!(html.contains("function renderBoxplot()"));
202+
assert!(html.contains("font-family: Arial, sans-serif"));
203+
}
204+
205+
/// Assert that the HTML contains the benchmark data
206+
fn assert_contains_benchmark_data(html: &str, results: &[BenchmarkResult]) {
207+
assert!(html.contains("const benchmarkData ="));
208+
for result in results {
209+
assert!(html.contains(&result.command));
210+
}
211+
}
212+
213+
/// Assert unit information in the HTML
214+
fn assert_unit_information(html: &str, short_name: &str, name: &str, factor: &str) {
215+
assert!(html.contains(&format!("const unitShortName = \"{}\"", short_name)));
216+
assert!(html.contains(&format!("const unitName = \"{}\"", name)));
217+
assert!(html.contains(&format!("const unitFactor = {}", factor)));
218+
}
219+
}

0 commit comments

Comments
 (0)