hyperfine 1.21.0

A command-line benchmarking tool
use crate::benchmark::relative_speed::BenchmarkResultWithRelativeSpeed;
use crate::benchmark::{benchmark_result::BenchmarkResult, relative_speed};
use crate::options::SortOrder;
use crate::quantity::{FormatQuantity, IsUnit, Time, TimeUnit, Zero};

use super::Exporter;
use anyhow::Result;
use std::cmp::Ordering;

pub enum Alignment {
    Left,
    Right,
}

pub trait MarkupExporter {
    fn table_results(
        &self,
        entries: &[BenchmarkResultWithRelativeSpeed],
        unit: TimeUnit,
        reference_pending: bool,
        explicit_reference: bool,
    ) -> String {
        // prepare table header strings
        let notation = format!("[{}]", unit.short_name());

        // prepare table cells alignment
        let cells_alignment = [
            Alignment::Left,
            Alignment::Right,
            Alignment::Right,
            Alignment::Right,
            Alignment::Right,
        ];

        // emit table header format
        let mut table = self.table_header(&cells_alignment);

        // emit table header data
        table.push_str(&self.table_row(&[
            "Command",
            &format!("Mean {notation}"),
            &format!("Min {notation}"),
            &format!("Max {notation}"),
            "Relative",
        ]));

        // emit horizontal line
        table.push_str(&self.table_divider(&cells_alignment));

        for entry in entries {
            let measurement = &entry.result;
            // prepare data row strings
            let cmd_str = measurement
                .command_with_unused_parameters
                .replace('|', "\\|");
            let mean_str = measurement.mean_wall_clock_time().format_value(unit);
            let stddev_str = if let Some(stddev) = measurement.measurements.stddev() {
                format!(" ± {}", stddev.format_value(unit))
            } else {
                "".into()
            };
            let min_str = measurement.measurements.min().format_value(unit);
            let max_str = measurement.measurements.max().format_value(unit);
            // The ratio of two zero times is undefined, even if they compare equal.
            let relative_unavailable = reference_pending
                || (explicit_reference
                    && !entry.is_reference
                    && entry.relative_ordering == Ordering::Equal
                    && measurement.mean_wall_clock_time() == Time::zero());
            let relative = if relative_unavailable {
                "N/A".to_string()
            } else {
                let stddev = match (entry.is_reference, entry.relative_speed_stddev) {
                    (false, Some(stddev)) => format!(" ± {stddev:.2}"),
                    _ => String::new(),
                };
                let label = if explicit_reference {
                    entry.reference_label()
                } else {
                    ""
                };
                format!("{:.2}{stddev}{label}", entry.relative_speed)
            };

            // prepare table row entries
            table.push_str(&self.table_row(&[
                &self.command(&cmd_str),
                &format!("{mean_str}{stddev_str}"),
                &min_str,
                &max_str,
                &relative,
            ]))
        }

        // emit table footer format
        table.push_str(&self.table_footer(&cells_alignment));

        table
    }

    fn table_row(&self, cells: &[&str]) -> String;

    fn table_divider(&self, cell_aligmnents: &[Alignment]) -> String;

    fn table_header(&self, _cell_aligmnents: &[Alignment]) -> String {
        "".to_string()
    }

    fn table_footer(&self, _cell_aligmnents: &[Alignment]) -> String {
        "".to_string()
    }

    fn command(&self, size: &str) -> String;
}

fn determine_unit_from_results(results: &[BenchmarkResult]) -> TimeUnit {
    if let Some(first_result) = results.first() {
        // Use the first BenchmarkResult entry to determine the unit for all entries.
        first_result.mean_wall_clock_time().suitable_unit()
    } else {
        // Default to `Second`.
        TimeUnit::Second
    }
}

impl<T: MarkupExporter> Exporter for T {
    fn serialize(
        &self,
        results: &[BenchmarkResult],
        unit: Option<TimeUnit>,
        sort_order: SortOrder,
        reference_index: Option<usize>,
    ) -> Result<Vec<u8>> {
        let unit = unit.unwrap_or_else(|| determine_unit_from_results(results));
        // Do not report ratios against another benchmark while the selected
        // reference is still pending in an intermediate export.
        let reference_pending = reference_index.is_some_and(|i| i >= results.len());
        let entries = relative_speed::compute(
            results,
            sort_order,
            reference_index.and_then(|i| results.get(i)),
        );

        let table =
            self.table_results(&entries, unit, reference_pending, reference_index.is_some());
        Ok(table.as_bytes().to_vec())
    }
}