use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::{BenchmarkId, Metric, MetricKind, RunContext};
pub const SCHEMA_VERSION: u32 = 3;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Run {
pub schema_version: u32,
pub context: RunContext,
pub results: Vec<BenchmarkResult>,
}
impl Run {
#[must_use]
pub fn new(context: RunContext, results: Vec<BenchmarkResult>) -> Self {
Self {
schema_version: SCHEMA_VERSION,
context,
results,
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
pub type MetricList = SmallVec<[Metric; 2]>;
fn deserialize_metrics<'de, D>(deserializer: D) -> Result<MetricList, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = SmallVec::<[RawMetric<'de>; 2]>::deserialize(deserializer)?;
Ok(raw.into_iter().filter_map(RawMetric::into_metric).collect())
}
#[derive(Deserialize)]
struct RawMetric<'a> {
kind: &'a str,
value: f64,
#[serde(default)]
std_dev: Option<f64>,
#[serde(default)]
interval_low: Option<f64>,
#[serde(default)]
interval_high: Option<f64>,
}
impl RawMetric<'_> {
fn into_metric(self) -> Option<Metric> {
Some(Metric {
kind: MetricKind::from_name(self.kind)?,
value: self.value,
std_dev: self.std_dev,
interval_low: self.interval_low,
interval_high: self.interval_high,
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct BenchmarkResult {
pub id: BenchmarkId,
#[serde(deserialize_with = "deserialize_metrics")]
pub metrics: MetricList,
}
impl BenchmarkResult {
#[must_use]
pub fn new(id: BenchmarkId, metrics: impl Into<MetricList>) -> Self {
Self {
id,
metrics: metrics.into(),
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use nonempty::nonempty;
use super::*;
use crate::{EnvironmentInfo, GitInfo, MetricKind, ToolchainInfo};
fn sample_context() -> RunContext {
let epoch = "2024-01-01T00:00:00Z".parse().unwrap();
RunContext::new(
epoch,
GitInfo::default(),
EnvironmentInfo::default(),
ToolchainInfo::default(),
"0.0.1".to_owned(),
)
}
#[test]
fn run_new_stamps_schema_version() {
let run = Run::new(sample_context(), Vec::new());
assert_eq!(run.schema_version, SCHEMA_VERSION);
}
#[test]
fn run_json_roundtrip() {
let result = BenchmarkResult::new(
BenchmarkId::new(nonempty![
"nm".to_owned(),
"nm::observe".to_owned(),
"pull".to_owned(),
]),
vec![Metric::new(MetricKind::InstructionCount, 1234.0)],
);
let run = Run::new(sample_context(), vec![result]);
let json = run.to_json().unwrap();
let parsed = Run::from_json(&json).unwrap();
assert_eq!(parsed, run);
}
#[test]
fn metrics_serialize_as_a_plain_json_array() {
let result = BenchmarkResult::new(
BenchmarkId::new(nonempty!["pkg".to_owned(), "case".to_owned()]),
vec![
Metric::new(MetricKind::InstructionCount, 10.0),
Metric::new(MetricKind::ConditionalBranches, 20.0),
],
);
let json = serde_json::to_string(&result).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
let metrics = value.get("metrics").expect("the metrics field is present");
assert!(
metrics.is_array(),
"metrics must serialize as a JSON array: {json}"
);
assert_eq!(metrics.as_array().unwrap().len(), 2);
let parsed: BenchmarkResult = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, result);
}
#[test]
fn result_roundtrips_across_the_inline_capacity_boundary() {
const KINDS: [MetricKind; 3] = [
MetricKind::InstructionCount,
MetricKind::ConditionalBranches,
MetricKind::IndirectBranches,
];
for count in [1_usize, 2, 3] {
let metrics: Vec<Metric> = KINDS
.iter()
.take(count)
.map(|&kind| Metric::new(kind, 1.0))
.collect();
let result = BenchmarkResult::new(
BenchmarkId::new(nonempty!["pkg".to_owned(), "case".to_owned()]),
metrics,
);
let json = serde_json::to_string(&result).unwrap();
let parsed: BenchmarkResult = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, result);
assert_eq!(parsed.metrics.len(), count);
}
}
#[test]
fn unknown_metric_kinds_are_dropped_on_read() {
let result = BenchmarkResult::new(
BenchmarkId::new(nonempty!["pkg".to_owned(), "case".to_owned()]),
vec![Metric::new(MetricKind::InstructionCount, 10.0)],
);
let mut value: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
value
.get_mut("metrics")
.and_then(serde_json::Value::as_array_mut)
.unwrap()
.push(serde_json::json!({ "kind": "estimated_cycles", "value": 907.0 }));
let json = serde_json::to_string(&value).unwrap();
let parsed: BenchmarkResult = serde_json::from_str(&json).unwrap();
let kinds: Vec<MetricKind> = parsed.metrics.iter().map(|metric| metric.kind).collect();
assert_eq!(kinds, vec![MetricKind::InstructionCount]);
}
}