Skip to main content

cu_profiler_report/
table.rs

1//! Human-facing aligned table for local CLI use.
2//!
3//! Hand-rolled (no table dependency) so the crate stays light and the output is
4//! deterministic for snapshot tests.
5
6use crate::model::{Report, scenario_budget, scenario_delta_pct, thousands};
7
8const HEADERS: [&str; 5] = ["Scenario", "Actual CU", "Budget", "Delta", "Status"];
9
10/// Render `report` as an aligned text table with a summary footer.
11#[must_use]
12pub fn render(report: &Report) -> String {
13    let mut rows: Vec<[String; 5]> = Vec::with_capacity(report.scenarios.len());
14    for s in &report.scenarios {
15        rows.push([
16            s.name.clone(),
17            thousands(s.measurement.total_cu),
18            scenario_budget(s).map_or_else(|| "-".to_string(), thousands),
19            scenario_delta_pct(s).map_or_else(|| "-".to_string(), |d| format!("{d:+.1}%")),
20            s.status.label().to_string(),
21        ]);
22    }
23
24    // Column widths: max of header and all cells.
25    let mut widths = HEADERS.map(str::len);
26    for row in &rows {
27        for (i, cell) in row.iter().enumerate() {
28            widths[i] = widths[i].max(cell.len());
29        }
30    }
31
32    let mut out = String::new();
33    push_row(&mut out, &HEADERS.map(String::from), &widths);
34    for row in &rows {
35        push_row(&mut out, row, &widths);
36    }
37
38    out.push('\n');
39    let sum = &report.summary;
40    out.push_str(&format!(
41        "{} scenario(s): {} passed, {} warned, {} failed — {} total CU\n",
42        sum.total_scenarios,
43        sum.passed,
44        sum.warned,
45        sum.failed,
46        thousands(sum.total_cu),
47    ));
48    out
49}
50
51fn push_row(out: &mut String, row: &[String; 5], widths: &[usize; 5]) {
52    // Column 0 (name) and column 4 (status) left-aligned; numerics right-aligned.
53    out.push_str(&pad_left(&row[0], widths[0]));
54    for i in 1..4 {
55        out.push_str("  ");
56        out.push_str(&pad_right(&row[i], widths[i]));
57    }
58    out.push_str("  ");
59    out.push_str(&pad_left(&row[4], widths[4]));
60    // Trim trailing spaces from the final left-aligned column.
61    while out.ends_with(' ') {
62        out.pop();
63    }
64    out.push('\n');
65}
66
67fn pad_left(s: &str, width: usize) -> String {
68    format!("{s:<width$}")
69}
70
71fn pad_right(s: &str, width: usize) -> String {
72    format!("{s:>width$}")
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use cu_profiler_core::Profiler;
79    use cu_profiler_core::backend::RecordedLogsBackend;
80    use cu_profiler_core::budget::BudgetPolicy;
81    use cu_profiler_core::metadata::RunMetadata;
82    use cu_profiler_core::scenario::Scenario;
83
84    fn sample_report() -> Report {
85        let mut backend = RecordedLogsBackend::new();
86        backend.insert_blob(
87            "swap_exact_in",
88            "Program User111 invoke [1]\n\
89             Program User111 consumed 96812 of 200000 compute units\n\
90             Program User111 success",
91            true,
92        );
93        let mut scenario = Scenario::new("swap_exact_in");
94        scenario.budget = BudgetPolicy {
95            absolute_max_cu: Some(100_000),
96            warn_at_budget_pct: Some(90.0),
97            ..Default::default()
98        };
99        Profiler::new().run(&backend, &[scenario], None, RunMetadata::recorded("0.1.0"))
100    }
101
102    #[test]
103    fn renders_headers_and_values() {
104        let table = render(&sample_report());
105        assert!(table.contains("Scenario"));
106        assert!(table.contains("swap_exact_in"));
107        assert!(table.contains("96,812"));
108        assert!(table.contains("100,000"));
109        assert!(table.contains("WARN"));
110        assert!(table.contains("1 scenario(s): 0 passed, 1 warned, 0 failed"));
111    }
112}