Export selected metrics side by side in CSV

This commit is contained in:
David Peter authored and David Peter committed 2026-10-07 21:24:59 +02:00
1 parent 25b0b54a5d
commit 067ee70f20
10 files changed
+101 -70

No files matched your search

+1 -1
View File
@@ -69,7 +69,7 @@ fn generate_results(args: &[&'static str]) -> Result<Vec<BenchmarkResult>> {
assert_eq!(options.executor_kind, ExecutorKind::Mock(None));
let commands = Commands::from_cli_arguments(&cli_arguments)?;
let export_manager = ExportManager::from_cli_arguments(&cli_arguments, options.metrics[0])?;
let export_manager = ExportManager::from_cli_arguments(&cli_arguments, &options.metrics)?;
options.validate_against_command_list(&commands)?;
+4 -3
View File
@@ -58,7 +58,7 @@ fn build_command() -> Command {
.hide_default_value(true)
.help("Performance metrics to measure, in order.\n\n\
This is either a preset name, or a comma-separated list of metrics with optional units. \
The first metric is used for non-JSON exports, \
CSV exports all selected metrics; markup exports use the first metric. \
JSON always includes all collected metrics.\n\n\
Presets (use one on its own):\n \
default Wall-clock time and peak RSS\n \
@@ -303,9 +303,10 @@ fn build_command() -> Command {
.action(ArgAction::Set)
.value_name("FILE")
.value_hint(ValueHint::FilePath)
.help("Export the primary metric summary statistics as CSV to the given FILE. If you need \
.help("Export summary statistics for all selected metrics as CSV to the given FILE. If you need \
all metrics for each individual run, use the JSON export format. \
Use the explicitly selected unit, or the base unit when unspecified."),
Column names are prefixed with the metric name. \
Use the explicitly selected units, or base units when unspecified."),
)
.arg(
Arg::new("export-json")
+54 -49
View File
@@ -4,23 +4,34 @@ use super::Exporter;
use crate::benchmark::benchmark_result::BenchmarkResult;
use crate::metric::MetricSelection;
use anyhow::{Context, Result};
use anyhow::Result;
#[derive(Default)]
pub struct CsvExporter {}
impl Exporter for CsvExporter {
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
// CSV never chooses units automatically: an omitted suffix means base units.
let unit = primary.csv_unit();
fn serialize(
&self,
results: &[BenchmarkResult],
metrics: &[MetricSelection],
) -> Result<Vec<u8>> {
// Omit metrics unavailable for all commands (possible with `--metrics all`).
let metrics: Vec<_> = metrics
.iter()
.filter(|selection| {
results
.iter()
.any(|result| selection.metric.summarize(&result.measurements).is_some())
})
.collect();
let mut writer = WriterBuilder::new().from_writer(vec![]);
let mut headers: Vec<String> = [
"command", "metric", "unit", "mean", "stddev", "median", "min", "max",
]
.iter()
.copied()
.map(str::to_owned)
.collect();
let mut headers = vec!["command".to_owned()];
for selection in &metrics {
headers.extend(
["unit", "mean", "stddev", "median", "min", "max"]
.map(|field| format!("{}_{field}", selection.metric.name())),
);
}
if let Some(result) = results.first() {
headers.extend(
result
@@ -32,36 +43,28 @@ impl Exporter for CsvExporter {
writer.write_record(headers)?;
for result in results {
let stats = primary
.metric
.summarize(&result.measurements)
.with_context(|| {
format!(
"Metric '{}' is unavailable for '{}'",
primary.metric.name(),
result.get_name()
)
})?;
let mut fields = vec![
result.get_name().to_owned(),
primary.metric.name().to_owned(),
unit.symbol.to_owned(),
];
// Preserve numeric precision rather than using rounded display values.
fields.extend(
[
Some(stats.mean),
stats.stddev,
Some(stats.median),
Some(stats.min),
Some(stats.max),
]
.map(|value| {
value
.map(|value| (value / unit.scale).to_string())
.unwrap_or_default()
}),
);
let mut fields = vec![result.get_name().to_owned()];
for selection in &metrics {
// CSV never chooses units automatically: an omitted suffix means base units.
let unit = selection.csv_unit();
let stats = selection.metric.summarize(&result.measurements);
fields.push(unit.symbol.to_owned());
// Preserve numeric precision rather than using rounded display values.
fields.extend(
[
stats.map(|stats| stats.mean),
stats.and_then(|stats| stats.stddev),
stats.map(|stats| stats.median),
stats.map(|stats| stats.min),
stats.map(|stats| stats.max),
]
.map(|value| {
value
.map(|value| (value / unit.scale).to_string())
.unwrap_or_default()
}),
);
}
fields.extend(
result
.parameters
@@ -191,15 +194,18 @@ fn test_csv() {
let actual = String::from_utf8(
exporter
.serialize(&results, MetricSelection::default())
.serialize(
&results,
&MetricSelection::parse_list("time_wall_clock,memory_peak_resident").unwrap(),
)
.unwrap(),
)
.unwrap();
insta::assert_snapshot!(actual, @r#"
command,metric,unit,mean,stddev,median,min,max,parameter_bar,parameter_foo
command_a,time_wall_clock,s,9,2.6457513110645907,8,7,12,two,one
command_b,time_wall_clock,s,18,1,18,17,19,seven,one
command,time_wall_clock_unit,time_wall_clock_mean,time_wall_clock_stddev,time_wall_clock_median,time_wall_clock_min,time_wall_clock_max,memory_peak_resident_unit,memory_peak_resident_mean,memory_peak_resident_stddev,memory_peak_resident_median,memory_peak_resident_min,memory_peak_resident_max,parameter_bar,parameter_foo
command_a,s,9,2.6457513110645907,8,7,12,B,1024,0,1024,1024,1024,two,one
command_b,s,18,1,18,17,19,B,1024,0,1024,1024,1024,seven,one
"#);
for (selection, unit, mean) in [
@@ -207,12 +213,11 @@ fn test_csv() {
("memory_peak_resident:KiB", "KiB", "1"),
] {
let output = exporter
.serialize(&results, MetricSelection::parse_list(selection).unwrap()[0])
.serialize(&results, &MetricSelection::parse_list(selection).unwrap())
.unwrap();
let mut reader = csv::Reader::from_reader(output.as_slice());
let row = reader.records().next().unwrap().unwrap();
assert_eq!(&row[1], "memory_peak_resident");
assert_eq!(&row[2], unit);
assert_eq!(&row[3], mean);
assert_eq!(&row[1], unit);
assert_eq!(&row[2], mean);
}
}
+6 -1
View File
@@ -65,7 +65,12 @@ pub struct JsonExporter {
}
impl Exporter for JsonExporter {
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
fn serialize(
&self,
results: &[BenchmarkResult],
metrics: &[MetricSelection],
) -> Result<Vec<u8>> {
let primary = metrics[0];
let mut output = to_vec_pretty(&HyperfineSummary {
schema_version: JSON_SCHEMA_VERSION,
metadata: &self.metadata,
+6 -1
View File
@@ -77,7 +77,12 @@ pub trait MarkupExporter {
}
impl<T: MarkupExporter> Exporter for T {
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
fn serialize(
&self,
results: &[BenchmarkResult],
metrics: &[MetricSelection],
) -> Result<Vec<u8>> {
let primary = metrics[0];
let stats: Vec<_> = results
.iter()
.map(|result| {
+10 -6
View File
@@ -44,8 +44,12 @@ pub enum ExportType {
/// Interface for different exporters.
trait Exporter {
/// Export the given entries in the serialized form.
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>>;
/// Export the given entries using a nonempty list of selected metrics, primary first.
fn serialize(
&self,
results: &[BenchmarkResult],
metrics: &[MetricSelection],
) -> Result<Vec<u8>>;
}
pub enum ExportTarget {
@@ -61,16 +65,16 @@ struct ExporterWithTarget {
/// Handles the management of multiple file exporters.
pub struct ExportManager {
exporters: Vec<ExporterWithTarget>,
primary: MetricSelection,
metrics: Vec<MetricSelection>,
}
impl ExportManager {
/// Build the ExportManager that will export the results specified
/// in the given ArgMatches
pub fn from_cli_arguments(matches: &ArgMatches, primary: MetricSelection) -> Result<Self> {
pub fn from_cli_arguments(matches: &ArgMatches, metrics: &[MetricSelection]) -> Result<Self> {
let mut export_manager = Self {
exporters: vec![],
primary,
metrics: metrics.to_vec(),
};
{
let mut add_exporter = |flag, exporttype| -> Result<()> {
@@ -120,7 +124,7 @@ impl ExportManager {
/// order not to clutter the output of hyperfine with intermediate results).
pub fn write_results(&self, results: &[BenchmarkResult], intermediate: bool) -> Result<()> {
for e in &self.exporters {
let content = || e.exporter.serialize(results, self.primary);
let content = || e.exporter.serialize(results, &self.metrics);
match e.target {
ExportTarget::File(ref filename) => {
+2 -2
View File
@@ -15,10 +15,10 @@ fn get_output<E: Exporter + Default>(results: &[BenchmarkResult], unit: Option<U
exporter
.serialize(
results,
MetricSelection {
&[MetricSelection {
unit,
..MetricSelection::default()
},
}],
)
.unwrap(),
)
+1 -1
View File
@@ -62,7 +62,7 @@ fn run() -> Result<()> {
}
}
let export_manager = ExportManager::from_cli_arguments(&cli_arguments, options.metrics[0])?;
let export_manager = ExportManager::from_cli_arguments(&cli_arguments, &options.metrics)?;
let mut scheduler = Scheduler::new(&commands, &options, &export_manager);
scheduler.run_benchmarks()?;