metrale_bench/hardware/report.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: [`HardwareStateReport`]: the two captures, the delta and both
4//! verdicts, as one value that travels with the run into its gate record
5//! (`GateRecord::hardware_state`, `.benchmarks/<id>/<date>-<sha>.json`).
6//!
7//! Owner: bench hardware.
8//! Invariants: none beyond the types. [`HardwareStateReport::close`] sets `after`,
9//! `delta` and `postcheck` together.
10
11use serde::{Deserialize, Serialize};
12
13use super::policy::{self, Postcheck, Precheck, Sensitivity};
14use super::state::{HardwareState, HardwareStateDelta};
15
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
17pub struct HardwareStateReport {
18 /// 2026-09-26: Which class of number this was, so a reader knows why the
19 /// verdict is what it is without looking the benchmark up.
20 pub sensitivity: Sensitivity,
21 /// 2026-09-26: Captured by the executor before the coherence probe and
22 /// `load()`, so before any request.
23 pub before: HardwareState,
24 /// 2026-09-26: Captured when the run's terminal frame is ready, including a
25 /// failed setup or a cancelled run. `None` when the precheck refused the run,
26 /// when the stream ended without a terminal frame, or when the capture task
27 /// failed.
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub after: Option<HardwareState>,
30 /// 2026-09-26: `after` minus `before`, set with `after` by [`HardwareStateReport::close`].
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub delta: Option<HardwareStateDelta>,
33 pub precheck: Precheck,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub postcheck: Option<Postcheck>,
36 /// 2026-09-26: `before.machine.perf_class()`, e.g. `"gb10@dgx1"`, copied out so
37 /// a reader need not derive it.
38 pub perf_class: String,
39}
40
41impl HardwareStateReport {
42 /// 2026-09-26: Open a report with the pre-run capture and its verdict. The
43 /// switches come from the environment, the ceilings from the caller.
44 pub fn opened(
45 sensitivity: Sensitivity,
46 before: HardwareState,
47 ceilings: Option<policy::TempCeilings>,
48 ) -> Self {
49 let precheck = policy::precheck(
50 sensitivity,
51 &before,
52 policy::PolicyOptions {
53 ceilings,
54 ..policy::PolicyOptions::from_env()
55 },
56 );
57 Self {
58 sensitivity,
59 perf_class: before.machine.perf_class(),
60 before,
61 after: None,
62 delta: None,
63 precheck,
64 postcheck: None,
65 }
66 }
67
68 /// 2026-09-26: Close it with the post-run capture, the delta and the validity
69 /// verdict.
70 pub fn close(&mut self, after: HardwareState) {
71 let delta = HardwareStateDelta::between(&self.before, &after);
72 self.postcheck = Some(policy::postcheck(
73 self.sensitivity,
74 &delta,
75 policy::PolicyOptions::from_env(),
76 ));
77 self.delta = Some(delta);
78 self.after = Some(after);
79 }
80
81 /// 2026-09-26: True when the precheck refused the run.
82 pub fn refuses(&self) -> bool {
83 self.precheck.decision == policy::Decision::Refuse
84 }
85
86 /// 2026-09-26: True when the postcheck judged the run invalid.
87 ///
88 /// A report that was never closed is not invalid: it is unmeasured, and
89 /// saying otherwise would blame the box for a harness failure.
90 pub fn invalidated(&self) -> bool {
91 self.postcheck
92 .as_ref()
93 .is_some_and(|p| p.validity == policy::Validity::Invalid)
94 }
95
96 /// 2026-09-26: Every concern from both phases, precheck first, for the run log.
97 pub fn concerns(&self) -> Vec<&str> {
98 self.precheck
99 .concerns
100 .iter()
101 .chain(self.postcheck.iter().flat_map(|p| p.concerns.iter()))
102 .map(String::as_str)
103 .collect()
104 }
105}
106
107#[cfg(test)]
108#[path = "report_tests.rs"]
109mod tests;