Support hardware counters (instructions, cache misses, ...) (#983)

* Add hardware counters to JSON exports

* Clarify Linux hardware counter setup
This commit is contained in:
David Peter authored and GitHub committed 2026-10-06 08:59:40 +02:00
1 parent ab86cb99cc
commit bc3a3f0524
13 files changed
+432 -34

No files matched your search

+6
View File
@@ -89,6 +89,7 @@ fn test_csv() {
time_user: Time::new::<second>(7.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -96,6 +97,7 @@ fn test_csv() {
time_user: Time::new::<second>(8.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -103,6 +105,7 @@ fn test_csv() {
time_user: Time::new::<second>(12.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -133,6 +136,7 @@ fn test_csv() {
time_user: Time::new::<second>(17.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -140,6 +144,7 @@ fn test_csv() {
time_user: Time::new::<second>(18.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -147,6 +152,7 @@ fn test_csv() {
time_user: Time::new::<second>(19.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
+94 -32
View File
@@ -8,7 +8,7 @@ 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, second, QuantityInUnit, Ratio, TimeUnit, Zero};
use crate::quantity::{byte, ratio, second, QuantityInUnit, Ratio, TimeUnit, Zero};
use anyhow::Result;
@@ -40,36 +40,92 @@ struct BenchmarkSummary {
time_system: StatisticalSummary,
#[serde(skip_serializing_if = "Option::is_none")]
memory_peak_resident: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
cpu_cycles: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_references: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_misses: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
branch_misses: Option<StatisticalSummary>,
}
impl BenchmarkSummary {
fn from_measurements(measurements: &Measurements) -> Self {
let values = &measurements.measurements;
let mut memory_values = values
.iter()
.filter_map(|m| m.memory_peak_resident)
.peekable();
Self {
time_wall_clock: StatisticalSummary::from_values(
values.iter().map(|m| m.time_wall_clock),
second,
),
time_user: StatisticalSummary::from_values(values.iter().map(|m| m.time_user), second),
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| m.time_system),
second,
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::<ratio>(v as f64))
}),
None::<ratio>,
),
instructions: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.instructions
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
cache_references: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.cache_references
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
cache_misses: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.cache_misses
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
branch_misses: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.branch_misses
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
memory_peak_resident: memory_values
.peek()
.is_some()
.then(|| StatisticalSummary::from_values(memory_values, byte)),
}
}
}
#[derive(Serialize)]
struct StatisticalSummary {
unit: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
unit: Option<&'static str>,
count: usize,
mean: f64,
stddev: Option<f64>,
@@ -79,7 +135,10 @@ struct StatisticalSummary {
}
impl StatisticalSummary {
fn from_values<Q, P, U>(values: impl IntoIterator<Item = Q>, _unit: U) -> Self
fn from_values<Q, P, U>(
values: impl IntoIterator<Item = Option<Q>>,
unit: Option<U>,
) -> Option<Self>
where
Q: Copy
+ PartialOrd
@@ -92,13 +151,13 @@ impl StatisticalSummary {
P: Into<Q>,
U: uom::si::Unit,
{
let values: Vec<_> = values.into_iter().collect();
assert!(
!values.is_empty(),
"Statistical summaries require at least one measurement"
);
Self {
unit: U::singular(),
// Only summarize a metric when it is available for every supplied measurement.
let values: Vec<_> = values.into_iter().collect::<Option<_>>()?;
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)
@@ -106,7 +165,7 @@ impl StatisticalSummary {
median: median(values.iter().copied()).value_in_unit(),
min: min(values.iter().copied()).value_in_unit(),
max: max(values.iter().copied()).value_in_unit(),
}
})
}
}
@@ -149,22 +208,25 @@ fn test_statistical_summary_unit_conversion() {
use crate::quantity::{kibibyte, millisecond, Information, Time};
let time = StatisticalSummary::from_values(
[Time::new::<second>(1.0), Time::new::<second>(3.0)],
millisecond,
);
[Time::new::<second>(1.0), Time::new::<second>(3.0)].map(Some),
Some(millisecond),
)
.unwrap();
let information = StatisticalSummary::from_values(
[
Information::new::<byte>(1024.0),
Information::new::<byte>(3072.0),
],
kibibyte,
);
]
.map(Some),
Some(kibibyte),
)
.unwrap();
for (summary, unit, scale) in [
(time, "millisecond", 1000.0),
(information, "kibibyte", 1.0),
] {
assert_eq!(summary.unit, unit);
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);
+24
View File
@@ -36,6 +36,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.09),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -43,6 +44,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.10),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -50,6 +52,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.14),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -65,6 +68,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(2.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -72,6 +76,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(3.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -79,6 +84,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(4.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -139,6 +145,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -146,6 +153,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -153,6 +161,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -168,6 +177,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -175,6 +185,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -182,6 +193,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -242,6 +254,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -249,6 +262,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -256,6 +270,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -271,6 +286,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -278,6 +294,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -285,6 +302,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -344,6 +362,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.01),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -351,6 +370,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.02),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -358,6 +378,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.03),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -373,6 +394,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.11),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -380,6 +402,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.12),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -387,6 +410,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.13),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),