use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Metric {
pub kind: MetricKind,
pub value: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub std_dev: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval_low: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval_high: Option<f64>,
}
impl Metric {
#[must_use]
pub fn new(kind: MetricKind, value: f64) -> Self {
Self {
kind,
value,
std_dev: None,
interval_low: None,
interval_high: None,
}
}
#[must_use]
pub fn with_dispersion(
mut self,
std_dev: Option<f64>,
interval_low: Option<f64>,
interval_high: Option<f64>,
) -> Self {
self.std_dev = std_dev;
self.interval_low = interval_low;
self.interval_high = interval_high;
self
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetricKind {
WallTime,
ProcessorTime,
InstructionCount,
ConditionalBranches,
IndirectBranches,
AllocatedBytes,
AllocationCount,
}
impl MetricKind {
pub const ALL: [Self; 7] = [
Self::WallTime,
Self::ProcessorTime,
Self::InstructionCount,
Self::ConditionalBranches,
Self::IndirectBranches,
Self::AllocatedBytes,
Self::AllocationCount,
];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::WallTime => "wall_time",
Self::ProcessorTime => "processor_time",
Self::InstructionCount => "instruction_count",
Self::ConditionalBranches => "conditional_branches",
Self::IndirectBranches => "indirect_branches",
Self::AllocatedBytes => "allocated_bytes",
Self::AllocationCount => "allocation_count",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|kind| kind.as_str() == name)
}
#[must_use]
pub fn as_unit(self) -> &'static str {
match self {
Self::WallTime | Self::ProcessorTime => "ns",
Self::AllocatedBytes => "bytes",
Self::InstructionCount
| Self::ConditionalBranches
| Self::IndirectBranches
| Self::AllocationCount => "count",
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn metric_kind_serializes_snake_case() {
let json = serde_json::to_string(&MetricKind::InstructionCount).unwrap();
assert_eq!(json, "\"instruction_count\"");
}
#[test]
fn metric_kind_wire_name_matches_as_str() {
for kind in MetricKind::ALL {
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(json, format!("\"{}\"", kind.as_str()));
let parsed: MetricKind = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, kind);
}
}
#[test]
fn from_name_round_trips_every_kind_and_rejects_unknown() {
for kind in MetricKind::ALL {
assert_eq!(MetricKind::from_name(kind.as_str()), Some(kind));
}
assert_eq!(MetricKind::from_name("not_a_metric"), None);
assert_eq!(MetricKind::from_name(""), None);
assert_eq!(MetricKind::from_name("Instruction_Count"), None);
assert_eq!(MetricKind::from_name("instruction"), None);
}
#[test]
fn metric_kind_units_are_fixed_per_kind() {
assert_eq!(MetricKind::WallTime.as_unit(), "ns");
assert_eq!(MetricKind::ProcessorTime.as_unit(), "ns");
assert_eq!(MetricKind::AllocatedBytes.as_unit(), "bytes");
assert_eq!(MetricKind::InstructionCount.as_unit(), "count");
assert_eq!(MetricKind::ConditionalBranches.as_unit(), "count");
}
#[test]
fn metric_dispersion_is_omitted_when_absent() {
let metric = Metric::new(MetricKind::InstructionCount, 1.0);
let json = serde_json::to_string(&metric).unwrap();
assert!(!json.contains("std_dev"), "{json}");
assert!(!json.contains("interval_low"), "{json}");
}
#[test]
fn metric_dispersion_roundtrips_when_present() {
let metric = Metric::new(MetricKind::WallTime, 26.9).with_dispersion(
Some(0.47),
Some(26.6),
Some(27.2),
);
let json = serde_json::to_string(&metric).unwrap();
let parsed: Metric = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, metric);
assert_eq!(parsed.std_dev, Some(0.47));
assert_eq!(parsed.interval_low, Some(26.6));
assert_eq!(parsed.interval_high, Some(27.2));
}
}