Skip to content

Commit b22c3bf

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 b22c3bf

File tree

6 files changed

+1049
-0
lines changed

6 files changed

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

0 commit comments

Comments
 (0)