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;