metrale_bench/benchmarks/scheduler_equivalence/
report.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: How a scheduler-equivalence run is presented: the terminal frame
4//! built from `compare::score`.
5//!
6//! Owner: bench, scheduler-equivalence gate.
7//! Invariants:
8//! - No I/O: every function here maps the legs or the score to a value.
9
10use std::collections::BTreeMap;
11use std::time::Duration;
12
13use super::compare::{Cell, Leg, Score, metrics, score, verdict};
14use super::unmeasured;
15use crate::result::{
16    BenchmarkResult, Cell as TCell, CellStyle, Column, LogLine, ResultTable, Stat,
17};
18
19/// 2026-09-26: The run's terminal frame: summary, table, metrics, verdict, and
20/// one warning line per unmeasured request.
21pub(super) fn terminal(
22    legs: &[Leg],
23    diagnostics: &BTreeMap<String, f64>,
24    elapsed: Duration,
25) -> BenchmarkResult {
26    let s = score(legs);
27    BenchmarkResult::completed("done", elapsed)
28        .with_summary(summary(&s))
29        .with_table(table(&s))
30        .with_metrics(metrics(&s, diagnostics))
31        .with_log(log(&s))
32        .with_verdict(verdict(&s))
33}
34
35pub(super) fn summary(s: &Score) -> Vec<Stat> {
36    let sum = |f: &dyn Fn(&Cell) -> usize| s.cells.iter().map(f).sum::<usize>();
37    vec![
38        Stat::new("cells", s.cells.len().to_string(), ""),
39        Stat::new(
40            "samples",
41            s.cells.first().map(|c| c.samples).unwrap_or(0).to_string(),
42            "",
43        ),
44        Stat::new(
45            "DIVERGED",
46            sum(&|c| c.async_vs_sync.diverged).to_string(),
47            "",
48        ),
49        Stat::new(
50            "control diverged",
51            sum(&|c| c.control_vs_sync.as_ref().map(|k| k.diverged).unwrap_or(0)).to_string(),
52            "",
53        ),
54        Stat::new(
55            "unmeasured causes",
56            unmeasured::breakdown(&all_unmeasured(s)),
57            "",
58        ),
59    ]
60}
61
62fn all_unmeasured(s: &Score) -> Vec<unmeasured::Unmeasured> {
63    s.cells.iter().flat_map(|c| c.unmeasured.clone()).collect()
64}
65
66/// 2026-09-26: One warning per unmeasured request, in cell order.
67pub(super) fn log(s: &Score) -> Vec<LogLine> {
68    all_unmeasured(s)
69        .iter()
70        .map(|u| LogLine::warn(u.to_string()))
71        .collect()
72}
73
74/// 2026-09-26: One row per cell, agreeing cells included, so the table shows
75/// which concurrencies were compared.
76pub(super) fn table(s: &Score) -> ResultTable {
77    let mut t = ResultTable::new(
78        "SYNC VS ASYNC, PER LANE AND CONCURRENCY",
79        vec![
80            Column::left("Lane", 10),
81            Column::left("C", 4),
82            Column::left("Samples", 8),
83            Column::left("Async diverged", 15),
84            Column::left("Control diverged", 17),
85            Column::left("Unmeasured", 11),
86            Column::left("Causes", 24),
87            Column::left("Detail", 30),
88        ],
89    );
90    for c in &s.cells {
91        let bad = c.async_vs_sync.diverged > 0
92            || c.async_vs_sync.unmeasured > 0
93            || c.control_vs_sync
94                .as_ref()
95                .is_some_and(|k| k.diverged > 0 || k.unmeasured > 0);
96        let style = if bad { CellStyle::Bad } else { CellStyle::Good };
97        let detail = if c.async_vs_sync.diverged_ids.is_empty() {
98            "identical".to_string()
99        } else {
100            c.async_vs_sync.diverged_ids.join(", ")
101        };
102        t.push(vec![
103            TCell::new(c.lane.label().to_string()),
104            TCell::new(c.concurrency.to_string()),
105            TCell::new(c.samples.to_string()),
106            TCell::styled(c.async_vs_sync.diverged.to_string(), style),
107            TCell::new(
108                c.control_vs_sync
109                    .as_ref()
110                    .map(|k| k.diverged.to_string())
111                    .unwrap_or_else(|| "-".to_string()),
112            ),
113            TCell::new(
114                (c.async_vs_sync.unmeasured
115                    + c.control_vs_sync
116                        .as_ref()
117                        .map(|k| k.unmeasured)
118                        .unwrap_or(0))
119                .to_string(),
120            ),
121            TCell::new(unmeasured::breakdown(&c.unmeasured)),
122            TCell::new(detail),
123        ]);
124    }
125    t
126}