use std::ops::{Add, AddAssign, Div}; use serde::*; use serde_json::to_vec_pretty; use super::Exporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::benchmark::measurement::Measurements; use crate::options::SortOrder; use crate::quantity::statistics::{max, mean, median, min, standard_deviation, UnsafeRawValue}; use crate::quantity::{byte, ratio, second, QuantityInUnit, Ratio, TimeUnit, Zero}; use anyhow::Result; mod metadata; use metadata::Metadata; const JSON_SCHEMA_VERSION: u32 = 2; #[derive(Serialize)] struct HyperfineSummary<'a> { schema_version: u32, metadata: &'a Metadata, results: Vec>, } #[derive(Serialize)] struct JsonBenchmarkResult<'a> { #[serde(flatten)] result: &'a BenchmarkResult, #[serde(skip_serializing_if = "std::ops::Not::not")] is_reference: bool, summary: BenchmarkSummary, } #[derive(Serialize)] struct BenchmarkSummary { time_wall_clock: StatisticalSummary, time_user: StatisticalSummary, time_system: StatisticalSummary, #[serde(skip_serializing_if = "Option::is_none")] memory_peak_resident: Option, #[serde(skip_serializing_if = "Option::is_none")] cpu_cycles: Option, #[serde(skip_serializing_if = "Option::is_none")] instructions: Option, #[serde(skip_serializing_if = "Option::is_none")] cache_references: Option, #[serde(skip_serializing_if = "Option::is_none")] cache_misses: Option, #[serde(skip_serializing_if = "Option::is_none")] branch_misses: Option, } impl BenchmarkSummary { fn from_measurements(measurements: &Measurements) -> Self { let values = &measurements.measurements; Self { time_wall_clock: StatisticalSummary::from_values( values.iter().map(|m| Some(m.time_wall_clock)), Some(second), ) .expect("Benchmarks have at least one measurement"), time_user: StatisticalSummary::from_values( values.iter().map(|m| Some(m.time_user)), Some(second), ) .expect("Benchmarks have at least one measurement"), time_system: StatisticalSummary::from_values( values.iter().map(|m| Some(m.time_system)), Some(second), ) .expect("Benchmarks have at least one measurement"), memory_peak_resident: StatisticalSummary::from_values( values .iter() .filter_map(|m| m.memory_peak_resident) .map(Some), Some(byte), ), cpu_cycles: StatisticalSummary::from_values( values.iter().map(|m| { m.hardware_counters .cpu_cycles .map(|v| Ratio::new::(v as f64)) }), None::, ), instructions: StatisticalSummary::from_values( values.iter().map(|m| { m.hardware_counters .instructions .map(|v| Ratio::new::(v as f64)) }), None::, ), cache_references: StatisticalSummary::from_values( values.iter().map(|m| { m.hardware_counters .cache_references .map(|v| Ratio::new::(v as f64)) }), None::, ), cache_misses: StatisticalSummary::from_values( values.iter().map(|m| { m.hardware_counters .cache_misses .map(|v| Ratio::new::(v as f64)) }), None::, ), branch_misses: StatisticalSummary::from_values( values.iter().map(|m| { m.hardware_counters .branch_misses .map(|v| Ratio::new::(v as f64)) }), None::, ), } } } #[derive(Serialize)] struct StatisticalSummary { #[serde(skip_serializing_if = "Option::is_none")] unit: Option<&'static str>, count: usize, mean: f64, stddev: Option, median: f64, min: f64, max: f64, } impl StatisticalSummary { fn from_values( values: impl IntoIterator>, unit: Option, ) -> Option where Q: Copy + PartialOrd + Add + AddAssign + Zero + Div + UnsafeRawValue + QuantityInUnit, P: Into, U: uom::si::Unit, { // Only summarize a metric when it is available for every supplied measurement. let values: Vec<_> = values.into_iter().collect::>()?; if values.is_empty() { return None; } Some(Self { unit: unit.map(|_| U::singular()), count: values.len(), mean: mean(values.iter().copied()).value_in_unit(), stddev: (values.len() > 1) .then(|| standard_deviation(values.iter().copied()).value_in_unit()), median: median(values.iter().copied()).value_in_unit(), min: min(values.iter().copied()).value_in_unit(), max: max(values.iter().copied()).value_in_unit(), }) } } #[derive(Default)] pub struct JsonExporter { metadata: Metadata, } impl Exporter for JsonExporter { fn serialize( &self, results: &[BenchmarkResult], _time_unit: Option, _sort_order: SortOrder, reference_index: Option, ) -> Result> { let mut output = to_vec_pretty(&HyperfineSummary { schema_version: JSON_SCHEMA_VERSION, metadata: &self.metadata, results: results .iter() .enumerate() .map(|(index, result)| JsonBenchmarkResult { result, is_reference: reference_index == Some(index), summary: BenchmarkSummary::from_measurements(&result.measurements), }) .collect(), }); if let Ok(ref mut content) = output { content.push(b'\n'); } Ok(output?) } } #[test] fn test_statistical_summary_unit_conversion() { use crate::quantity::{kibibyte, millisecond, Information, Time}; let time = StatisticalSummary::from_values( [Time::new::(1.0), Time::new::(3.0)].map(Some), Some(millisecond), ) .unwrap(); let information = StatisticalSummary::from_values( [ Information::new::(1024.0), Information::new::(3072.0), ] .map(Some), Some(kibibyte), ) .unwrap(); for (summary, unit, scale) in [ (time, "millisecond", 1000.0), (information, "kibibyte", 1.0), ] { assert_eq!(summary.unit, Some(unit)); assert_eq!(summary.count, 2); approx::assert_relative_eq!(summary.mean, 2.0 * scale); approx::assert_relative_eq!(summary.median, 2.0 * scale); approx::assert_relative_eq!(summary.min, scale); approx::assert_relative_eq!(summary.max, 3.0 * scale); approx::assert_relative_eq!(summary.stddev.unwrap(), 2.0_f64.sqrt() * scale); } }