aboutsummaryrefslogtreecommitdiff
path: root/src/html/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/html/mod.rs')
-rwxr-xr-xsrc/html/mod.rs837
1 files changed, 837 insertions, 0 deletions
diff --git a/src/html/mod.rs b/src/html/mod.rs
new file mode 100755
index 0000000..02ff0de
--- /dev/null
+++ b/src/html/mod.rs
@@ -0,0 +1,837 @@
+use crate::report::{make_filename_safe, BenchmarkId, MeasurementData, Report, ReportContext};
+use crate::stats::bivariate::regression::Slope;
+
+use crate::estimate::Estimate;
+use crate::format;
+use crate::fs;
+use crate::measurement::ValueFormatter;
+use crate::plot::{PlotContext, PlotData, Plotter};
+use crate::SavedSample;
+use criterion_plot::Size;
+use serde::Serialize;
+use std::cell::RefCell;
+use std::cmp::Ordering;
+use std::collections::{BTreeSet, HashMap};
+use std::path::{Path, PathBuf};
+use tinytemplate::TinyTemplate;
+
+const THUMBNAIL_SIZE: Option<Size> = Some(Size(450, 300));
+
+fn debug_context<S: Serialize>(path: &Path, context: &S) {
+ if crate::debug_enabled() {
+ let mut context_path = PathBuf::from(path);
+ context_path.set_extension("json");
+ println!("Writing report context to {:?}", context_path);
+ let result = fs::save(context, &context_path);
+ if let Err(e) = result {
+ error!("Failed to write report context debug output: {}", e);
+ }
+ }
+}
+
+#[derive(Serialize)]
+struct Context {
+ title: String,
+ confidence: String,
+
+ thumbnail_width: usize,
+ thumbnail_height: usize,
+
+ slope: Option<ConfidenceInterval>,
+ r2: ConfidenceInterval,
+ mean: ConfidenceInterval,
+ std_dev: ConfidenceInterval,
+ median: ConfidenceInterval,
+ mad: ConfidenceInterval,
+ throughput: Option<ConfidenceInterval>,
+
+ additional_plots: Vec<Plot>,
+
+ comparison: Option<Comparison>,
+}
+
+#[derive(Serialize)]
+struct IndividualBenchmark {
+ name: String,
+ path: String,
+ regression_exists: bool,
+}
+impl IndividualBenchmark {
+ fn from_id(
+ output_directory: &Path,
+ path_prefix: &str,
+ id: &BenchmarkId,
+ ) -> IndividualBenchmark {
+ let mut regression_path = PathBuf::from(output_directory);
+ regression_path.push(id.as_directory_name());
+ regression_path.push("report");
+ regression_path.push("regression.svg");
+
+ IndividualBenchmark {
+ name: id.as_title().to_owned(),
+ path: format!("{}/{}", path_prefix, id.as_directory_name()),
+ regression_exists: regression_path.is_file(),
+ }
+ }
+}
+
+#[derive(Serialize)]
+struct SummaryContext {
+ group_id: String,
+
+ thumbnail_width: usize,
+ thumbnail_height: usize,
+
+ violin_plot: Option<String>,
+ line_chart: Option<String>,
+
+ benchmarks: Vec<IndividualBenchmark>,
+}
+
+#[derive(Serialize)]
+struct ConfidenceInterval {
+ lower: String,
+ upper: String,
+ point: String,
+}
+
+#[derive(Serialize)]
+struct Plot {
+ name: String,
+ url: String,
+}
+impl Plot {
+ fn new(name: &str, url: &str) -> Plot {
+ Plot {
+ name: name.to_owned(),
+ url: url.to_owned(),
+ }
+ }
+}
+
+#[derive(Serialize)]
+struct Comparison {
+ p_value: String,
+ inequality: String,
+ significance_level: String,
+ explanation: String,
+
+ change: ConfidenceInterval,
+ thrpt_change: Option<ConfidenceInterval>,
+ additional_plots: Vec<Plot>,
+}
+
+fn if_exists(output_directory: &Path, path: &Path) -> Option<String> {
+ let report_path = path.join("report/index.html");
+ if PathBuf::from(output_directory).join(&report_path).is_file() {
+ Some(report_path.to_string_lossy().to_string())
+ } else {
+ None
+ }
+}
+#[derive(Serialize, Debug)]
+struct ReportLink<'a> {
+ name: &'a str,
+ path: Option<String>,
+}
+impl<'a> ReportLink<'a> {
+ // TODO: Would be nice if I didn't have to keep making these components filename-safe.
+ fn group(output_directory: &Path, group_id: &'a str) -> ReportLink<'a> {
+ let path = PathBuf::from(make_filename_safe(group_id));
+
+ ReportLink {
+ name: group_id,
+ path: if_exists(output_directory, &path),
+ }
+ }
+
+ fn function(output_directory: &Path, group_id: &str, function_id: &'a str) -> ReportLink<'a> {
+ let mut path = PathBuf::from(make_filename_safe(group_id));
+ path.push(make_filename_safe(function_id));
+
+ ReportLink {
+ name: function_id,
+ path: if_exists(output_directory, &path),
+ }
+ }
+
+ fn value(output_directory: &Path, group_id: &str, value_str: &'a str) -> ReportLink<'a> {
+ let mut path = PathBuf::from(make_filename_safe(group_id));
+ path.push(make_filename_safe(value_str));
+
+ ReportLink {
+ name: value_str,
+ path: if_exists(output_directory, &path),
+ }
+ }
+
+ fn individual(output_directory: &Path, id: &'a BenchmarkId) -> ReportLink<'a> {
+ let path = PathBuf::from(id.as_directory_name());
+ ReportLink {
+ name: id.as_title(),
+ path: if_exists(output_directory, &path),
+ }
+ }
+}
+
+#[derive(Serialize)]
+struct BenchmarkValueGroup<'a> {
+ value: Option<ReportLink<'a>>,
+ benchmarks: Vec<ReportLink<'a>>,
+}
+
+#[derive(Serialize)]
+struct BenchmarkGroup<'a> {
+ group_report: ReportLink<'a>,
+
+ function_ids: Option<Vec<ReportLink<'a>>>,
+ values: Option<Vec<ReportLink<'a>>>,
+
+ individual_links: Vec<BenchmarkValueGroup<'a>>,
+}
+impl<'a> BenchmarkGroup<'a> {
+ fn new(output_directory: &Path, ids: &[&'a BenchmarkId]) -> BenchmarkGroup<'a> {
+ let group_id = &ids[0].group_id;
+ let group_report = ReportLink::group(output_directory, group_id);
+
+ let mut function_ids = Vec::with_capacity(ids.len());
+ let mut values = Vec::with_capacity(ids.len());
+ let mut individual_links = HashMap::with_capacity(ids.len());
+
+ for id in ids.iter() {
+ let function_id = id.function_id.as_ref().map(String::as_str);
+ let value = id.value_str.as_ref().map(String::as_str);
+
+ let individual_link = ReportLink::individual(output_directory, id);
+
+ function_ids.push(function_id);
+ values.push(value);
+
+ individual_links.insert((function_id, value), individual_link);
+ }
+
+ fn parse_opt(os: &Option<&str>) -> Option<f64> {
+ os.and_then(|s| s.parse::<f64>().ok())
+ }
+
+ // If all of the value strings can be parsed into a number, sort/dedupe
+ // numerically. Otherwise sort lexicographically.
+ if values.iter().all(|os| parse_opt(os).is_some()) {
+ values.sort_unstable_by(|v1, v2| {
+ let num1 = parse_opt(&v1);
+ let num2 = parse_opt(&v2);
+
+ num1.partial_cmp(&num2).unwrap_or(Ordering::Less)
+ });
+ values.dedup_by_key(|os| parse_opt(&os).unwrap());
+ } else {
+ values.sort_unstable();
+ values.dedup();
+ }
+
+ // Sort and dedupe functions by name.
+ function_ids.sort_unstable();
+ function_ids.dedup();
+
+ let mut value_groups = Vec::with_capacity(values.len());
+ for value in values.iter() {
+ let row = function_ids
+ .iter()
+ .filter_map(|f| individual_links.remove(&(*f, *value)))
+ .collect::<Vec<_>>();
+ value_groups.push(BenchmarkValueGroup {
+ value: value.map(|s| ReportLink::value(output_directory, group_id, s)),
+ benchmarks: row,
+ });
+ }
+
+ let function_ids = function_ids
+ .into_iter()
+ .map(|os| os.map(|s| ReportLink::function(output_directory, group_id, s)))
+ .collect::<Option<Vec<_>>>();
+ let values = values
+ .into_iter()
+ .map(|os| os.map(|s| ReportLink::value(output_directory, group_id, s)))
+ .collect::<Option<Vec<_>>>();
+
+ BenchmarkGroup {
+ group_report,
+ function_ids,
+ values,
+ individual_links: value_groups,
+ }
+ }
+}
+
+#[derive(Serialize)]
+struct IndexContext<'a> {
+ groups: Vec<BenchmarkGroup<'a>>,
+}
+
+pub struct Html {
+ templates: TinyTemplate<'static>,
+ plotter: RefCell<Box<dyn Plotter>>,
+}
+impl Html {
+ pub(crate) fn new(plotter: Box<dyn Plotter>) -> Html {
+ let mut templates = TinyTemplate::new();
+ templates
+ .add_template("report_link", include_str!("report_link.html.tt"))
+ .expect("Unable to parse report_link template.");
+ templates
+ .add_template("index", include_str!("index.html.tt"))
+ .expect("Unable to parse index template.");
+ templates
+ .add_template("benchmark_report", include_str!("benchmark_report.html.tt"))
+ .expect("Unable to parse benchmark_report template");
+ templates
+ .add_template("summary_report", include_str!("summary_report.html.tt"))
+ .expect("Unable to parse summary_report template");
+
+ let plotter = RefCell::new(plotter);
+ Html { templates, plotter }
+ }
+}
+impl Report for Html {
+ fn measurement_complete(
+ &self,
+ id: &BenchmarkId,
+ report_context: &ReportContext,
+ measurements: &MeasurementData<'_>,
+ formatter: &dyn ValueFormatter,
+ ) {
+ try_else_return!({
+ let mut report_dir = report_context.output_directory.clone();
+ report_dir.push(id.as_directory_name());
+ report_dir.push("report");
+ fs::mkdirp(&report_dir)
+ });
+
+ let typical_estimate = &measurements.absolute_estimates.typical();
+
+ let time_interval = |est: &Estimate| -> ConfidenceInterval {
+ ConfidenceInterval {
+ lower: formatter.format_value(est.confidence_interval.lower_bound),
+ point: formatter.format_value(est.point_estimate),
+ upper: formatter.format_value(est.confidence_interval.upper_bound),
+ }
+ };
+
+ let data = measurements.data;
+
+ elapsed! {
+ "Generating plots",
+ self.generate_plots(id, report_context, formatter, measurements)
+ }
+
+ let mut additional_plots = vec![
+ Plot::new("Typical", "typical.svg"),
+ Plot::new("Mean", "mean.svg"),
+ Plot::new("Std. Dev.", "SD.svg"),
+ Plot::new("Median", "median.svg"),
+ Plot::new("MAD", "MAD.svg"),
+ ];
+ if measurements.absolute_estimates.slope.is_some() {
+ additional_plots.push(Plot::new("Slope", "slope.svg"));
+ }
+
+ let throughput = measurements
+ .throughput
+ .as_ref()
+ .map(|thr| ConfidenceInterval {
+ lower: formatter
+ .format_throughput(thr, typical_estimate.confidence_interval.upper_bound),
+ upper: formatter
+ .format_throughput(thr, typical_estimate.confidence_interval.lower_bound),
+ point: formatter.format_throughput(thr, typical_estimate.point_estimate),
+ });
+
+ let context = Context {
+ title: id.as_title().to_owned(),
+ confidence: format!(
+ "{:.2}",
+ typical_estimate.confidence_interval.confidence_level
+ ),
+
+ thumbnail_width: THUMBNAIL_SIZE.unwrap().0,
+ thumbnail_height: THUMBNAIL_SIZE.unwrap().1,
+
+ slope: measurements
+ .absolute_estimates
+ .slope
+ .as_ref()
+ .map(time_interval),
+ mean: time_interval(&measurements.absolute_estimates.mean),
+ median: time_interval(&measurements.absolute_estimates.median),
+ mad: time_interval(&measurements.absolute_estimates.median_abs_dev),
+ std_dev: time_interval(&measurements.absolute_estimates.std_dev),
+ throughput,
+
+ r2: ConfidenceInterval {
+ lower: format!(
+ "{:0.7}",
+ Slope(typical_estimate.confidence_interval.lower_bound).r_squared(&data)
+ ),
+ upper: format!(
+ "{:0.7}",
+ Slope(typical_estimate.confidence_interval.upper_bound).r_squared(&data)
+ ),
+ point: format!(
+ "{:0.7}",
+ Slope(typical_estimate.point_estimate).r_squared(&data)
+ ),
+ },
+
+ additional_plots,
+
+ comparison: self.comparison(measurements),
+ };
+
+ let mut report_path = report_context.output_directory.clone();
+ report_path.push(id.as_directory_name());
+ report_path.push("report");
+ report_path.push("index.html");
+ debug_context(&report_path, &context);
+
+ let text = self
+ .templates
+ .render("benchmark_report", &context)
+ .expect("Failed to render benchmark report template");
+ try_else_return!(fs::save_string(&text, &report_path));
+ }
+
+ fn summarize(
+ &self,
+ context: &ReportContext,
+ all_ids: &[BenchmarkId],
+ formatter: &dyn ValueFormatter,
+ ) {
+ let all_ids = all_ids
+ .iter()
+ .filter(|id| {
+ let id_dir = context.output_directory.join(id.as_directory_name());
+ fs::is_dir(&id_dir)
+ })
+ .collect::<Vec<_>>();
+ if all_ids.is_empty() {
+ return;
+ }
+
+ let group_id = all_ids[0].group_id.clone();
+
+ let data = self.load_summary_data(&context.output_directory, &all_ids);
+
+ let mut function_ids = BTreeSet::new();
+ let mut value_strs = Vec::with_capacity(all_ids.len());
+ for id in all_ids {
+ if let Some(ref function_id) = id.function_id {
+ function_ids.insert(function_id);
+ }
+ if let Some(ref value_str) = id.value_str {
+ value_strs.push(value_str);
+ }
+ }
+
+ fn try_parse(s: &str) -> Option<f64> {
+ s.parse::<f64>().ok()
+ }
+
+ // If all of the value strings can be parsed into a number, sort/dedupe
+ // numerically. Otherwise sort lexicographically.
+ if value_strs.iter().all(|os| try_parse(&*os).is_some()) {
+ value_strs.sort_unstable_by(|v1, v2| {
+ let num1 = try_parse(&v1);
+ let num2 = try_parse(&v2);
+
+ num1.partial_cmp(&num2).unwrap_or(Ordering::Less)
+ });
+ value_strs.dedup_by_key(|os| try_parse(&os).unwrap());
+ } else {
+ value_strs.sort_unstable();
+ value_strs.dedup();
+ }
+
+ for function_id in function_ids {
+ let samples_with_function: Vec<_> = data
+ .iter()
+ .by_ref()
+ .filter(|&&(ref id, _)| id.function_id.as_ref() == Some(&function_id))
+ .collect();
+
+ if samples_with_function.len() > 1 {
+ let subgroup_id =
+ BenchmarkId::new(group_id.clone(), Some(function_id.clone()), None, None);
+
+ self.generate_summary(
+ &subgroup_id,
+ &*samples_with_function,
+ context,
+ formatter,
+ false,
+ );
+ }
+ }
+
+ for value_str in value_strs {
+ let samples_with_value: Vec<_> = data
+ .iter()
+ .by_ref()
+ .filter(|&&(ref id, _)| id.value_str.as_ref() == Some(&value_str))
+ .collect();
+
+ if samples_with_value.len() > 1 {
+ let subgroup_id =
+ BenchmarkId::new(group_id.clone(), None, Some(value_str.clone()), None);
+
+ self.generate_summary(
+ &subgroup_id,
+ &*samples_with_value,
+ context,
+ formatter,
+ false,
+ );
+ }
+ }
+
+ let mut all_data = data.iter().by_ref().collect::<Vec<_>>();
+ // First sort the ids/data by value.
+ // If all of the value strings can be parsed into a number, sort/dedupe
+ // numerically. Otherwise sort lexicographically.
+ let all_values_numeric = all_data.iter().all(|(ref id, _)| {
+ id.value_str
+ .as_ref()
+ .map(String::as_str)
+ .and_then(try_parse)
+ .is_some()
+ });
+ if all_values_numeric {
+ all_data.sort_unstable_by(|(a, _), (b, _)| {
+ let num1 = a.value_str.as_ref().map(String::as_str).and_then(try_parse);
+ let num2 = b.value_str.as_ref().map(String::as_str).and_then(try_parse);
+
+ num1.partial_cmp(&num2).unwrap_or(Ordering::Less)
+ });
+ } else {
+ all_data.sort_unstable_by_key(|(id, _)| id.value_str.as_ref());
+ }
+ // Next, sort the ids/data by function name. This results in a sorting priority of
+ // function name, then value. This one has to be a stable sort.
+ all_data.sort_by_key(|(id, _)| id.function_id.as_ref());
+
+ self.generate_summary(
+ &BenchmarkId::new(group_id, None, None, None),
+ &*(all_data),
+ context,
+ formatter,
+ true,
+ );
+ self.plotter.borrow_mut().wait();
+ }
+
+ fn final_summary(&self, report_context: &ReportContext) {
+ let output_directory = &report_context.output_directory;
+ if !fs::is_dir(&output_directory) {
+ return;
+ }
+
+ let mut found_ids = try_else_return!(fs::list_existing_benchmarks(&output_directory));
+ found_ids.sort_unstable_by_key(|id| id.id().to_owned());
+
+ // Group IDs by group id
+ let mut id_groups: HashMap<&str, Vec<&BenchmarkId>> = HashMap::new();
+ for id in found_ids.iter() {
+ id_groups
+ .entry(&id.group_id)
+ .or_insert_with(Vec::new)
+ .push(id);
+ }
+
+ let mut groups = id_groups
+ .into_iter()
+ .map(|(_, group)| BenchmarkGroup::new(output_directory, &group))
+ .collect::<Vec<BenchmarkGroup<'_>>>();
+ groups.sort_unstable_by_key(|g| g.group_report.name);
+
+ try_else_return!(fs::mkdirp(&output_directory.join("report")));
+
+ let report_path = output_directory.join("report").join("index.html");
+
+ let context = IndexContext { groups };
+
+ debug_context(&report_path, &context);
+
+ let text = self
+ .templates
+ .render("index", &context)
+ .expect("Failed to render index template");
+ try_else_return!(fs::save_string(&text, &report_path,));
+ }
+}
+impl Html {
+ fn comparison(&self, measurements: &MeasurementData<'_>) -> Option<Comparison> {
+ if let Some(ref comp) = measurements.comparison {
+ let different_mean = comp.p_value < comp.significance_threshold;
+ let mean_est = &comp.relative_estimates.mean;
+ let explanation_str: String;
+
+ if !different_mean {
+ explanation_str = "No change in performance detected.".to_owned();
+ } else {
+ let comparison = compare_to_threshold(&mean_est, comp.noise_threshold);
+ match comparison {
+ ComparisonResult::Improved => {
+ explanation_str = "Performance has improved.".to_owned();
+ }
+ ComparisonResult::Regressed => {
+ explanation_str = "Performance has regressed.".to_owned();
+ }
+ ComparisonResult::NonSignificant => {
+ explanation_str = "Change within noise threshold.".to_owned();
+ }
+ }
+ }
+
+ let comp = Comparison {
+ p_value: format!("{:.2}", comp.p_value),
+ inequality: (if different_mean { "<" } else { ">" }).to_owned(),
+ significance_level: format!("{:.2}", comp.significance_threshold),
+ explanation: explanation_str,
+
+ change: ConfidenceInterval {
+ point: format::change(mean_est.point_estimate, true),
+ lower: format::change(mean_est.confidence_interval.lower_bound, true),
+ upper: format::change(mean_est.confidence_interval.upper_bound, true),
+ },
+
+ thrpt_change: measurements.throughput.as_ref().map(|_| {
+ let to_thrpt_estimate = |ratio: f64| 1.0 / (1.0 + ratio) - 1.0;
+ ConfidenceInterval {
+ point: format::change(to_thrpt_estimate(mean_est.point_estimate), true),
+ lower: format::change(
+ to_thrpt_estimate(mean_est.confidence_interval.lower_bound),
+ true,
+ ),
+ upper: format::change(
+ to_thrpt_estimate(mean_est.confidence_interval.upper_bound),
+ true,
+ ),
+ }
+ }),
+
+ additional_plots: vec![
+ Plot::new("Change in mean", "change/mean.svg"),
+ Plot::new("Change in median", "change/median.svg"),
+ Plot::new("T-Test", "change/t-test.svg"),
+ ],
+ };
+ Some(comp)
+ } else {
+ None
+ }
+ }
+
+ fn generate_plots(
+ &self,
+ id: &BenchmarkId,
+ context: &ReportContext,
+ formatter: &dyn ValueFormatter,
+ measurements: &MeasurementData<'_>,
+ ) {
+ let plot_ctx = PlotContext {
+ id,
+ context,
+ size: None,
+ is_thumbnail: false,
+ };
+
+ let plot_data = PlotData {
+ measurements,
+ formatter,
+ comparison: None,
+ };
+
+ let plot_ctx_small = plot_ctx.thumbnail(true).size(THUMBNAIL_SIZE);
+
+ self.plotter.borrow_mut().pdf(plot_ctx, plot_data);
+ self.plotter.borrow_mut().pdf(plot_ctx_small, plot_data);
+ if measurements.absolute_estimates.slope.is_some() {
+ self.plotter.borrow_mut().regression(plot_ctx, plot_data);
+ self.plotter
+ .borrow_mut()
+ .regression(plot_ctx_small, plot_data);
+ } else {
+ self.plotter
+ .borrow_mut()
+ .iteration_times(plot_ctx, plot_data);
+ self.plotter
+ .borrow_mut()
+ .iteration_times(plot_ctx_small, plot_data);
+ }
+
+ self.plotter
+ .borrow_mut()
+ .abs_distributions(plot_ctx, plot_data);
+
+ if let Some(ref comp) = measurements.comparison {
+ try_else_return!({
+ let mut change_dir = context.output_directory.clone();
+ change_dir.push(id.as_directory_name());
+ change_dir.push("report");
+ change_dir.push("change");
+ fs::mkdirp(&change_dir)
+ });
+
+ try_else_return!({
+ let mut both_dir = context.output_directory.clone();
+ both_dir.push(id.as_directory_name());
+ both_dir.push("report");
+ both_dir.push("both");
+ fs::mkdirp(&both_dir)
+ });
+
+ let comp_data = plot_data.comparison(&comp);
+
+ self.plotter.borrow_mut().pdf(plot_ctx, comp_data);
+ self.plotter.borrow_mut().pdf(plot_ctx_small, comp_data);
+ if measurements.absolute_estimates.slope.is_some()
+ && comp.base_estimates.slope.is_some()
+ {
+ self.plotter.borrow_mut().regression(plot_ctx, comp_data);
+ self.plotter
+ .borrow_mut()
+ .regression(plot_ctx_small, comp_data);
+ } else {
+ self.plotter
+ .borrow_mut()
+ .iteration_times(plot_ctx, comp_data);
+ self.plotter
+ .borrow_mut()
+ .iteration_times(plot_ctx_small, comp_data);
+ }
+ self.plotter.borrow_mut().t_test(plot_ctx, comp_data);
+ self.plotter
+ .borrow_mut()
+ .rel_distributions(plot_ctx, comp_data);
+ }
+
+ self.plotter.borrow_mut().wait();
+ }
+
+ fn load_summary_data<'a>(
+ &self,
+ output_directory: &Path,
+ all_ids: &[&'a BenchmarkId],
+ ) -> Vec<(&'a BenchmarkId, Vec<f64>)> {
+ all_ids
+ .iter()
+ .filter_map(|id| {
+ let entry = output_directory.join(id.as_directory_name()).join("new");
+
+ let SavedSample { iters, times, .. } =
+ try_else_return!(fs::load(&entry.join("sample.json")), || None);
+ let avg_times = iters
+ .into_iter()
+ .zip(times.into_iter())
+ .map(|(iters, time)| time / iters)
+ .collect::<Vec<_>>();
+
+ Some((*id, avg_times))
+ })
+ .collect::<Vec<_>>()
+ }
+
+ fn generate_summary(
+ &self,
+ id: &BenchmarkId,
+ data: &[&(&BenchmarkId, Vec<f64>)],
+ report_context: &ReportContext,
+ formatter: &dyn ValueFormatter,
+ full_summary: bool,
+ ) {
+ let plot_ctx = PlotContext {
+ id,
+ context: report_context,
+ size: None,
+ is_thumbnail: false,
+ };
+
+ try_else_return!(
+ {
+ let mut report_dir = report_context.output_directory.clone();
+ report_dir.push(id.as_directory_name());
+ report_dir.push("report");
+ fs::mkdirp(&report_dir)
+ },
+ || {}
+ );
+
+ self.plotter.borrow_mut().violin(plot_ctx, formatter, data);
+
+ let value_types: Vec<_> = data.iter().map(|&&(ref id, _)| id.value_type()).collect();
+ let mut line_path = None;
+
+ if value_types.iter().all(|x| x == &value_types[0]) {
+ if let Some(value_type) = value_types[0] {
+ let values: Vec<_> = data.iter().map(|&&(ref id, _)| id.as_number()).collect();
+ if values.iter().any(|x| x != &values[0]) {
+ self.plotter
+ .borrow_mut()
+ .line_comparison(plot_ctx, formatter, data, value_type);
+ line_path = Some(plot_ctx.line_comparison_path());
+ }
+ }
+ }
+
+ let path_prefix = if full_summary { "../.." } else { "../../.." };
+ let benchmarks = data
+ .iter()
+ .map(|&&(ref id, _)| {
+ IndividualBenchmark::from_id(&report_context.output_directory, path_prefix, id)
+ })
+ .collect();
+
+ let context = SummaryContext {
+ group_id: id.as_title().to_owned(),
+
+ thumbnail_width: THUMBNAIL_SIZE.unwrap().0,
+ thumbnail_height: THUMBNAIL_SIZE.unwrap().1,
+
+ violin_plot: Some(plot_ctx.violin_path().to_string_lossy().into_owned()),
+ line_chart: line_path.map(|p| p.to_string_lossy().into_owned()),
+
+ benchmarks,
+ };
+
+ let mut report_path = report_context.output_directory.clone();
+ report_path.push(id.as_directory_name());
+ report_path.push("report");
+ report_path.push("index.html");
+ debug_context(&report_path, &context);
+
+ let text = self
+ .templates
+ .render("summary_report", &context)
+ .expect("Failed to render summary report template");
+ try_else_return!(fs::save_string(&text, &report_path,), || {});
+ }
+}
+
+enum ComparisonResult {
+ Improved,
+ Regressed,
+ NonSignificant,
+}
+
+fn compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult {
+ let ci = &estimate.confidence_interval;
+ let lb = ci.lower_bound;
+ let ub = ci.upper_bound;
+
+ if lb < -noise && ub < -noise {
+ ComparisonResult::Improved
+ } else if lb > noise && ub > noise {
+ ComparisonResult::Regressed
+ } else {
+ ComparisonResult::NonSignificant
+ }
+}