metrale_bench/benchmark.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The [`Benchmark`] trait, a [`Plugin`] driven one step at a time,
4//! and [`BenchmarkDescriptor`], its static identity.
5//!
6//! A benchmark owns its phase state and does one step of work per
7//! [`Benchmark::next`], returning a frame. It must not block the runtime or
8//! loop to completion inside one step: the driver checks for cancellation only
9//! between frames. [`Benchmark::run`] delegates to [`crate::executor::drive`],
10//! the same loop the executor uses.
11//!
12//! Owner: bench.
13//! Invariants: none beyond the types.
14
15use std::future::Future;
16
17use anyhow::Result;
18use futures::Stream;
19
20use crate::dynamic::DynBenchmark;
21use crate::hardware::Sensitivity;
22use crate::params::{ParamSpec, ParamValues};
23use crate::plugin::Plugin;
24use crate::result::BenchmarkResult;
25
26/// 2026-09-26: Static identity of a benchmark, and how to construct one. The
27/// registry (`registry::ALL`), the TUI list pane and the run directories
28/// (`ArtifactStore::runs_dir`) all read it.
29pub struct BenchmarkDescriptor {
30 /// 2026-09-26: Stable and filename-safe (`[a-z0-9-]`, checked by
31 /// `registry::tests::ids_are_unique_and_filename_safe`). Names the run
32 /// directory `<home>/runs/<id>/`.
33 pub id: &'static str,
34 pub name: &'static str,
35 /// 2026-09-26: One line for the suite list.
36 pub summary: &'static str,
37 /// 2026-09-26: A paragraph for the detail pane: what it measures and what
38 /// it costs.
39 pub detail: &'static str,
40 /// 2026-09-26: Rough wall time at default parameters, for display, e.g.
41 /// `"~15 min"`.
42 pub duration_hint: &'static str,
43 /// 2026-09-26: Expected wall time at default parameters, in seconds: the
44 /// certification planner's estimate when no completed run of this id has
45 /// been measured (`bench_certify::plan::units`). Zero only on descriptors
46 /// whose `duration_hint` starts with `unrunnable`
47 /// (`registry::tests::every_runnable_benchmark_declares_an_expected_duration`).
48 pub expected_secs: u64,
49 /// 2026-09-26: When this benchmark's measurement last changed (new
50 /// thresholds, prompt set or scoring rule), as `YYYY-MM-DD`; not when its
51 /// code was edited. It tells a reader whether two runs are comparable.
52 pub updated: &'static str,
53 /// 2026-09-26: True when starting has a side effect beyond load on the
54 /// endpoint. The TUI (`tui::bench_keys`) and `bench certify` (`--yes`)
55 /// require an explicit confirmation for these.
56 pub needs_confirmation: bool,
57 /// 2026-09-26: The checkpoint families this benchmark is defined on, if
58 /// any. The executor's endpoint check (`coherence::probe_for`) warns on a
59 /// mismatch and never refuses the run. `None` means the benchmark measures
60 /// whatever it is pointed at.
61 pub intended_for: Option<ModelExpectation>,
62 /// 2026-09-26: Parameters whose run-time value comes from the selected
63 /// model variant's committed bound, as `(param key, metric key)` pairs, for
64 /// benchmarks that compute their own verdict against a knob that is also a
65 /// `BENCH.toml` threshold (the agentic gate's `wall_budget_s` against
66 /// `sum_wall_s`). A gate run (`bench_resolve::apply_threshold_params`) and
67 /// the TUI derive the value from the variant's bound; an explicit `--param`
68 /// wins.
69 pub threshold_params: &'static [(&'static str, &'static str)],
70 /// 2026-09-26: Whether this benchmark's number is a speed number, and so
71 /// corruptible by the state of the box that produced it. A required field
72 /// of the descriptor, not a list in the hardware policy, so every new
73 /// benchmark has to answer it. See [`crate::hardware::policy`].
74 pub sensitivity: Sensitivity,
75 pub ctor: fn() -> Box<dyn DynBenchmark>,
76}
77
78/// 2026-09-26: Which checkpoints a benchmark's numbers mean something for.
79#[derive(Clone, Copy, Debug)]
80pub struct ModelExpectation {
81 /// 2026-09-26: Lower-case substrings identifying an acceptable checkpoint
82 /// family, not an exact id: [`ModelExpectation::accepts`] matches any model
83 /// id whose lower-cased form contains one of them, whatever its org prefix
84 /// or quantization suffix.
85 pub families: &'static [&'static str],
86 /// 2026-09-26: Appended to the mismatch warning: which model the benchmark
87 /// is defined on, and what running it elsewhere means.
88 pub note: &'static str,
89}
90
91impl ModelExpectation {
92 /// 2026-09-26: Does `model` belong to a family this benchmark is defined on?
93 pub fn accepts(&self, model: &str) -> bool {
94 let lowered = model.to_lowercase();
95 self.families.iter().any(|f| lowered.contains(f))
96 }
97}
98
99impl BenchmarkDescriptor {
100 pub fn build(&self) -> Box<dyn DynBenchmark> {
101 (self.ctor)()
102 }
103}
104
105pub trait Benchmark: Plugin {
106 fn descriptor(&self) -> &'static BenchmarkDescriptor;
107
108 /// 2026-09-26: The parameters shown before the run starts, so the user can
109 /// change them. Their defaults are in the returned specs.
110 fn parameters(&self) -> Vec<ParamSpec>;
111
112 /// 2026-09-26: Receive the edited values. Validate here and return an
113 /// error naming the offending field; the executor calls `next()` only after
114 /// this returns `Ok`.
115 fn configure(&mut self, values: &ParamValues) -> Result<()>;
116
117 /// 2026-09-26: Drive `next()` to completion, streaming every frame. The
118 /// stream ends after the first terminal [`crate::RunStatus`], or after an
119 /// error. It does not call `cleanup()`.
120 fn run(&mut self) -> impl Stream<Item = Result<BenchmarkResult>> + '_
121 where
122 Self: Sized + Send,
123 {
124 crate::executor::drive(self)
125 }
126
127 /// 2026-09-26: One step of work, returning the frame for that step.
128 fn next(&mut self) -> impl Future<Output = Result<BenchmarkResult>> + Send;
129
130 /// 2026-09-26: Release whatever the run acquired. The executor calls it
131 /// once on every return path of a run: completion, failure, cancellation,
132 /// a failed setup and a refused hardware precheck.
133 fn cleanup(&mut self) -> impl Future<Output = Result<()>> + Send {
134 async { Ok(()) }
135 }
136}
137
138#[cfg(test)]
139#[path = "benchmark_desc_tests.rs"]
140mod desc_tests;