metrale_circuit/hardware/
render_routes.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: The runtime routes of a hardware plan, rendered. A report lists each route that
4//! applies to a report run, with the route's estimate and launches beside the primary arm's and
5//! the rules it runs instead. A plan (`--format plan`) is followed by each route's plan.
6//!
7//! Owner: metrale-circuit (hardware).
8//! Invariants:
9//! - A report or plan with no applicable route renders exactly as it did before routes existed.
10
11use std::collections::BTreeSet;
12use std::fmt::Write as _;
13
14use super::gaps::{GapTable, gap_table};
15use super::plan::Resolved;
16use super::{HwError, HwReport, OnePlan};
17use crate::fuser::{FusionPlan, Policy};
18use crate::ir::Circuit;
19use crate::runtime::RuntimeRoute;
20
21/// 2026-09-30: One route at one report run, estimated.
22#[derive(Debug, Clone)]
23pub struct RouteRow {
24    /// 2026-09-30: The route.
25    pub route: RuntimeRoute,
26    /// 2026-09-30: The primary arm's table at this run.
27    pub primary_us: f64,
28    /// 2026-09-30: The primary arm's launches.
29    pub primary_launches: u64,
30    /// 2026-09-30: The route's table at this run.
31    pub table: GapTable,
32}
33
34/// 2026-09-30: Every route of every table in `tables`, estimated under the route's policy.
35pub fn route_rows(
36    r: &Resolved,
37    c: &Circuit,
38    policy: &Policy,
39    label: &str,
40    tables: &[GapTable],
41) -> Result<Vec<RouteRow>, HwError> {
42    let mut out = Vec::new();
43    for t in tables {
44        for rp in &t.planned.routes {
45            let settings = rp.route.policy(policy).settings;
46            out.push(RouteRow {
47                route: rp.route.clone(),
48                primary_us: t.total_us,
49                primary_launches: t.planned.plan.launches(),
50                table: gap_table(r, c, &settings, label, rp.planned.clone())?,
51            });
52        }
53    }
54    Ok(out)
55}
56
57fn rules_of(p: &FusionPlan) -> BTreeSet<&str> {
58    p.groups.iter().map(|g| g.rule.as_str()).collect()
59}
60
61/// 2026-09-30: The rules `route` runs that `primary` does not, and those it replaces.
62fn swapped(primary: &FusionPlan, route: &FusionPlan) -> (Vec<String>, Vec<String>) {
63    let (a, b) = (rules_of(primary), rules_of(route));
64    (
65        b.difference(&a).map(|s| format!("`{s}`")).collect(),
66        a.difference(&b).map(|s| format!("`{s}`")).collect(),
67    )
68}
69
70/// 2026-09-30: The "Runtime routes" section; nothing when no route applies.
71pub(super) fn routes_section(s: &mut String, r: &HwReport) {
72    if r.routes.is_empty() {
73        return;
74    }
75    let _ = writeln!(
76        s,
77        "## Runtime routes\n\nConditions the engine checks at run time, under which a step runs another arm than the plan's (FUSIONS.toml `[[runtime]]`). The estimates above are the primary arm's; each route is planned and estimated beside it{}.\n",
78        super::render::projection(r)
79    );
80    let mut described = BTreeSet::new();
81    for row in &r.routes {
82        let x = &row.route;
83        if described.insert(x.id.as_str()) {
84            let _ = writeln!(
85                s,
86                "- `{}`: when {}; planned as `{}` ({}).",
87                x.id,
88                x.why,
89                x.plans_as_text(),
90                x.cite
91            );
92        }
93    }
94    let _ = writeln!(
95        s,
96        "\n| case | route | time (ms) | route time (ms) | launches | route launches | route runs | instead of |\n|---|---|---:|---:|---:|---:|---|---|"
97    );
98    for row in &r.routes {
99        let p = &row.table.planned.plan;
100        let primary = r
101            .tables
102            .iter()
103            .find(|t| t.planned.run == row.table.planned.run)
104            .map(|t| &t.planned.plan);
105        let (runs, instead) = primary.map(|q| swapped(q, p)).unwrap_or_default();
106        let _ = writeln!(
107            s,
108            "| decode C={} ({} n={}) | `{}` | {:.3} | {:.3} | {} | {} | {} | {} |",
109            p.rows,
110            p.mode.name(),
111            p.rows,
112            row.route.id,
113            row.primary_us / 1e3,
114            row.table.total_us / 1e3,
115            row.primary_launches,
116            p.launches(),
117            runs.join(", "),
118            instead.join(", ")
119        );
120    }
121    let _ = writeln!(s);
122}
123
124/// 2026-09-30: `one`'s plan text, followed by each applicable route's plan under a heading that
125/// names the route, its condition and the settings it is planned as.
126/// 2026-10-02: Each group line names the compute unit its kernels run on (`compute=`).
127pub fn plan_text(circuit: &Circuit, one: &OnePlan) -> String {
128    let families = &one.resolved.families;
129    let group = |g: &crate::fuser::Group| super::tc_policy::unit_tag(families, &g.kernels);
130    let node = |n: crate::ir::NodeIdx| one.planned.pipelines.line(n);
131    let notes = crate::render::Notes {
132        group: &group,
133        node: &node,
134    };
135    let mut s = crate::render::render_noted(circuit, &one.planned.plan, &one.header, &notes);
136    for rp in &one.planned.routes {
137        let header = crate::render::with_settings(&one.header, &rp.route.policy(&one.policy));
138        let node = |n: crate::ir::NodeIdx| rp.planned.pipelines.line(n);
139        let notes = crate::render::Notes {
140            group: &group,
141            node: &node,
142        };
143        s.push_str(&crate::render::route_section(
144            circuit,
145            &rp.route,
146            &rp.planned.plan,
147            &header,
148            &notes,
149        ));
150    }
151    s
152}