Skip to main content

launchbound_report/
lib.rs

1//! The run report (`report.v1`). The section that prints every
2//! configuration that was **faster and refused** is the product: never
3//! soften, hide, or downrank it.
4
5#![warn(missing_docs)]
6
7mod build;
8mod render;
9
10pub use build::{RunDir, build_report};
11pub use render::render_text;
12
13// Re-exported, not merely used: `Summary` appears in the public fields of
14// `CandidateReport`, `ChosenInfo` and `RejectedFaster`, so a caller that reads
15// a report has to be able to name it. Without this the type was reachable and
16// unnameable unless you also depended on `launchbound-bench` directly.
17pub use launchbound_bench::Summary;
18
19use serde::{Deserialize, Serialize};
20
21/// What can go wrong assembling a report from a run directory.
22#[derive(Debug, thiserror::Error)]
23pub enum ReportError {
24    /// The run directory is missing a document, or one of them does not
25    /// declare the schema this build reads.
26    #[error("run dir: {0}")]
27    RunDir(String),
28    /// The report could not be written.
29    #[error("report io: {0}")]
30    Io(String),
31}
32
33/// A `report.v1` document: the whole run, as a reader should see it.
34///
35/// This is the surface every other consumer reads — the TUI, the text
36/// renderer, and anyone parsing the JSON. Its schema is validated against
37/// `schemas/report.v1.json` in CI.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Report {
40    /// Schema tag; always `report.v1`.
41    pub schema: String,
42    /// The kernel this run tuned.
43    pub kernel: String,
44    /// The compute capability the gate ran at. A verdict does not transfer
45    /// to another one (`docs/LIMITATIONS.md`).
46    pub gate_cc: String,
47    /// `measured` or `estimated` — stamped on the report and every number
48    /// in it. Reporting an estimate as a measurement is release-blocking
49    /// (docs/LIMITATIONS.md).
50    pub measurement_kind: String,
51    /// `full` when every candidate passed through the reconverge gate;
52    /// `none` on the Metal path (there is no MSL analyzer — §3.4). The
53    /// renderer prints the no-gate notice unconditionally when this is
54    /// `none`; a test asserts it cannot be omitted.
55    pub convergence_gate: String,
56    /// The part measured on. Absent when nothing was measured.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub device: Option<DeviceInfo>,
59    /// The operator's written reason for measuring refused candidates.
60    /// Present exactly when `--allow-unsafe` was used, so a report
61    /// containing refused timings always says why they were taken.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub allow_unsafe_reason: Option<String>,
64    /// The recommended configuration. Absent when nothing was both
65    /// admitted and measured — a gate-only run, or one whose whole
66    /// admitted set failed to measure.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub chosen: Option<ChosenInfo>,
69    /// IDs whose intervals overlap the chosen one: reported as
70    /// indistinguishable, never ranked (docs/BENCHMARKING.md).
71    #[serde(default)]
72    pub indistinguishable_from_chosen: Vec<String>,
73    /// THE section: refused configurations that measurably beat the chosen
74    /// one (their CI is entirely below the chosen CI).
75    #[serde(default)]
76    pub rejected_faster: Vec<RejectedFaster>,
77    /// Every candidate, admitted or not, measured or not.
78    pub candidates: Vec<CandidateReport>,
79    /// Run-level tallies.
80    pub totals: Totals,
81}
82
83/// The part the measurements were taken on. Results are valid only for it.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct DeviceInfo {
86    /// Product name as the driver reports it, e.g. `NVIDIA A10G`.
87    pub name: String,
88    /// The device's actual compute capability, which need not equal
89    /// [`Report::gate_cc`] — the gate can be run for a different target
90    /// than the one measured on.
91    pub cc: String,
92    /// Driver version, part of what makes a timing reproducible.
93    pub driver_version: String,
94}
95
96/// The configuration this run recommends: fastest median among the
97/// candidates that were admitted by the gate *and* measured successfully.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ChosenInfo {
100    /// Its canonical `config.v1` ID.
101    pub id: String,
102    /// Its dimension assignments, e.g. `block_x=128 tile=256`.
103    pub config: String,
104    /// Its timing summary. Present by construction — being measured is
105    /// what made it eligible.
106    pub summary: Summary,
107}
108
109/// A refused configuration that measurably beat the chosen one.
110///
111/// This is the section the whole tool exists to produce: it is the cost of
112/// the safety decision, stated in numbers rather than asserted. Populated
113/// only when refused candidates were measured under `--allow-unsafe`, and
114/// only when the candidate's whole confidence interval sits below the
115/// chosen one's — "faster" here means measurably, not on a point estimate.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct RejectedFaster {
118    /// Its canonical `config.v1` ID.
119    pub id: String,
120    /// Its dimension assignments.
121    pub config: String,
122    /// Its timing summary.
123    pub summary: Summary,
124    /// chosen_median / this_median: how much faster the refused one was.
125    pub speedup_vs_chosen: f64,
126    /// The rules that refused it — what the speed cost buys.
127    pub rules: Vec<RuleRef>,
128}
129
130/// One analyzer rule as the report carries it.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct RuleRef {
133    /// Rule ID, e.g. `RC001`.
134    pub rule: String,
135    /// `file:line:col` of the offending site, when the analyzer gave one.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub span: Option<String>,
138    /// Why it applies at this configuration — the launch-shape reasoning,
139    /// not just the analyzer's generic message.
140    pub reason: String,
141}
142
143/// Every candidate the run considered, admitted or not, measured or not.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct CandidateReport {
146    /// Its canonical `config.v1` ID.
147    pub id: String,
148    /// Its dimension assignments.
149    pub config: String,
150    /// clean | admitted_with_caveats | disqualified | tool_error
151    pub verdict: String,
152    /// Rules that fired: refusals when disqualified, caveats when
153    /// admitted with them, empty when clean.
154    #[serde(default)]
155    pub rules: Vec<RuleRef>,
156    /// ok | error | timeout | unmeasured
157    pub measurement_status: String,
158    /// Timing summary, present exactly when `measurement_status` is `ok`.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub summary: Option<Summary>,
161    /// Why the measurement failed, when it did — including the timeout
162    /// message for a candidate that hung, which is the gate being right.
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub measurement_error: Option<String>,
165    /// Wall-clock GPU seconds this candidate consumed, spent or wasted.
166    pub gpu_seconds: f64,
167}
168
169/// Run-level tallies, so a reader can check the parts add up.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct Totals {
172    /// Configurations enumerated, after constraints pruned the product.
173    pub candidates: usize,
174    /// Passed the gate — clean or admitted with caveats.
175    pub admitted: usize,
176    /// Refused by the gate.
177    pub refused: usize,
178    /// Measured successfully. At most `admitted`, and fewer when a budget
179    /// ran out or a measurement failed.
180    pub measured_ok: usize,
181    /// Total GPU seconds the run consumed.
182    pub gpu_seconds: f64,
183}