Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

dwarves-gui/src/binview.rs

//! **The Binary tab** — the binary half of the SELECTED workspace's code room, as
//! Mímir v2 holds it.
//!
//! The pane is the shared, presentational [`facett_binview::BinaryPane`] (a bloat
//! treemap · a size sparkline · the hardening row). The host seam ([`RoomBinaryData`])
//! asks the room through the forge (`CodeRoom.Answer`, [`crate::room`]) and maps the
//! answer onto the pane's shapes — nothing is computed here:
//!
//! | what the pane draws | asked of | answer |
//! |---|---|---|
//! | the artifact, its bytes by crate (the treemap) and its largest symbols | `Viz.Binary` | mimir-binary's join of the linked artifact into the room (`code_symbols`) |
//! | its size per binary release (the sparkline) | `Viz.Binary` (`history`) | the binary stream's ledger (`bin_ingest_runs`: file size per release) |
//! | hardening (RELRO · PIE · NX · canary) | — | NOT in the binary layer: the pane draws no badge and says so |
//!
//! `Viz.Binary` is a whole-room pane: the server computes it on its jera `rooms` lane
//! and the client asks again while it is `queued` / `running` ([`crate::room`]).
//!
//! ## What this replaced (2026-10-15)
//! The tab read the four pre-v2 tables `nornir bin analyze|diff` wrote (`binary_facts`
//! / `section_sizes` / `symbol_sizes` / `artifact_diff`) with `Warehouse.Scan`. Those
//! have no writer in Mímir v2: the binary layer is in the room. The before→after diff
//! waterfall went with them (no producer of section deltas) — the pane skips the layer.
//!
//! **Never a sample:** a room with no linked artifact (`not_consumed` / `no_artifact`),
//! an unreachable forge, an answer still on the queue — each is drawn as its state with
//! the reader's evidence, never a treemap.

use std::sync::Arc;

use facett_binview::{BinaryDataSeam, BinaryPane, Checksec, CrateSize, Relro, SymbolSize};
use facett_core::{Facet, Panel};

use crate::room::{self, ForgeRoom, Pending, RoomAnswer, RoomSource};

/// The surface the tab asks.
pub const S_BINARY: &str = "Viz.Binary";

/// The feed, named the way an operator would look it up.
pub const BINARY_FEED: &str =
    "the code room's binary half (CodeRoom.Answer `Viz.Binary`: code_symbols · bin_ingest_runs)";

/// The pane's data seam over ONE measured `Viz.Binary` answer — the reader's rows,
/// field for field.
pub struct RoomBinaryData {
    answer: RoomAnswer,
}

impl RoomBinaryData {
    pub fn new(answer: RoomAnswer) -> Self {
        Self { answer }
    }
}

fn u(v: &serde_json::Value) -> u64 {
    v.as_u64().unwrap_or(0)
}

impl BinaryDataSeam for RoomBinaryData {
    fn source(&self) -> &str {
        if self.answer.body["source"] == "build" {
            "a Build Thing artifact filed with the server (Viz.Binary)"
        } else {
            "the Mímir v2 code room (Viz.Binary)"
        }
    }
    fn artifact_name(&self) -> String {
        self.answer.body["artifact"]
            .as_str()
            .unwrap_or("")
            .to_string()
    }
    /// A build artifact's hardening is the server's analysis of its bytes; the room's
    /// binary layer joins symbols and grades nothing.
    fn checksec(&self) -> Checksec {
        let c = &self.answer.body["checksec"];
        let b = |k: &str| c[k].as_bool().unwrap_or(false);
        Checksec {
            relro: match c["relro"].as_str().unwrap_or("") {
                "full" | "Full" => Relro::Full,
                "partial" | "Partial" => Relro::Partial,
                _ => Relro::None,
            },
            pie: b("pie"),
            nx: b("nx"),
            stack_canary: b("stack_canary"),
            stripped: b("stripped"),
            fortify: b("fortify"),
        }
    }
    fn checksec_measured(&self) -> bool {
        self.answer.body["checksec"].is_object()
    }
    fn crate_sizes(&self) -> Vec<CrateSize> {
        self.answer
            .rows("crates")
            .iter()
            .map(|c| CrateSize {
                crate_name: c["crate"].as_str().unwrap_or("?").to_string(),
                bytes: u(&c["bytes"]),
            })
            .collect()
    }
    fn symbol_sizes(&self) -> Vec<SymbolSize> {
        self.answer
            .rows("symbols")
            .iter()
            .map(|s| SymbolSize {
                crate_name: s["crate"].as_str().unwrap_or("?").to_string(),
                symbol: s["symbol"].as_str().unwrap_or("?").to_string(),
                bytes: u(&s["bytes"]),
            })
            .collect()
    }
    /// The artifact's file size per binary release, oldest first — only releases whose
    /// ledger carries a size (an unmeasured release is left out, not drawn as 0).
    fn size_history(&self) -> Vec<u64> {
        self.answer
            .rows("history")
            .iter()
            .filter_map(|h| h["file_size"].as_u64())
            .collect()
    }
}

/// What a binary answer that is not a measurement means, in plain words — for the
/// two states that say the room has no binary yet. `None` for the others (their own
/// sentence says it).
pub fn plain_line(a: &RoomAnswer) -> Option<String> {
    match a.state.as_str() {
        "no_artifact" => Some(
            "No linked artifact in the room yet: the server reads binaries where a build puts them for each consumed commit, and nothing has built them there. No Build Thing run has filed one with the server either."
                .to_string(),
        ),
        "not_consumed" => Some(
            "No binary in the room yet: the binary stream of these repos was not read, and no Build Thing run has filed an artifact with the server.".to_string(),
        ),
        _ => None,
    }
}

/// The Binary tab: the reused [`BinaryPane`] over the selected workspace's `Viz.Binary`.
pub struct BinaryTab {
    /// The pane over the last answer that was a measurement; `None` until one lands
    /// (the tab then draws the state, never an empty treemap).
    inner: Option<Box<dyn Panel>>,
    source: Arc<dyn RoomSource>,
    /// The forge endpoint + bearer a `{"set_workspace":…}` re-binds with (`None` for an
    /// injected source).
    forge: Option<(String, String)>,
    workspace: String,
    /// The artifact asked for (`""` = the one linking the most bytes).
    artifact: String,
    answer: RoomAnswer,
    /// A read in flight. The frame polls it; `wait` blocks on it.
    pending: Option<Pending<RoomAnswer>>,
}

impl BinaryTab {
    /// The shipped tab: the binary half of `workspace`'s room, asked through the forge.
    pub fn new(
        endpoint: impl Into<String>,
        token: impl Into<String>,
        workspace: impl Into<String>,
    ) -> Self {
        let (endpoint, token, workspace) = (endpoint.into(), token.into(), workspace.into());
        let mut me = Self::with_source(ForgeRoom::shared(&endpoint, &token, &workspace), workspace);
        me.forge = Some((endpoint, token));
        me
    }

    /// The tab over an injected source (tests). Starts its first read.
    pub fn with_source(source: Arc<dyn RoomSource>, workspace: impl Into<String>) -> Self {
        let answer = RoomAnswer::pending(S_BINARY, &source.describe());
        let mut me = Self {
            inner: None,
            source,
            forge: None,
            workspace: workspace.into(),
            artifact: String::new(),
            answer,
            pending: None,
        };
        me.bind();
        me
    }

    /// **(Re-)read** off the egui thread. A re-bind while one is in flight is not a
    /// second round trip.
    pub fn bind(&mut self) {
        if self.pending.is_some() {
            return;
        }
        let source = Arc::clone(&self.source);
        let params = if self.artifact.is_empty() {
            serde_json::json!({})
        } else {
            serde_json::json!({ "artifact": self.artifact })
        };
        self.pending = Some(Pending::spawn("binview-read", move || {
            source.ask(S_BINARY, &params)
        }));
    }

    /// Non-blocking: land the read if the worker finished. Called once per frame.
    pub fn poll(&mut self) {
        let Some(p) = &self.pending else { return };
        if let Some(res) = p.try_take() {
            self.pending = None;
            self.land(res);
        }
    }

    /// Blocking: wait for the read in flight (a scripted caller; the window never does).
    pub fn wait(&mut self) {
        if let Some(p) = self.pending.take() {
            let res = p.wait();
            self.land(res);
        }
    }

    pub fn is_pending(&self) -> bool {
        self.pending.is_some()
    }

    fn land(&mut self, res: Result<RoomAnswer, ()>) {
        let a = res.unwrap_or_else(|()| room::worker_died(S_BINARY, &*self.source));
        // An answer that is not a measurement mounts NO pane: the tab draws the state.
        self.inner = a.is_measured().then(|| {
            Box::new(
                BinaryPane::new(Box::new(RoomBinaryData::new(a.clone())))
                    .with_title("Binary — Mímir v2"),
            ) as Box<dyn Panel>
        });
        self.answer = a;
    }

    /// The workspace the pane is bound to.
    pub fn workspace(&self) -> &str {
        &self.workspace
    }

    /// The artifacts the room links (for the picker): `(artifact, bytes)`.
    fn artifacts(&self) -> Vec<(String, u64)> {
        self.answer
            .rows("artifacts")
            .iter()
            .map(|a| {
                (
                    a["artifact"].as_str().unwrap_or("?").to_string(),
                    u(&a["bytes"]),
                )
            })
            .collect()
    }
}

impl Facet for BinaryTab {
    fn title(&self) -> &str {
        "Binary"
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        self.poll();
        if self.pending.is_some() {
            ui.ctx().request_repaint();
        }
        let mut pick: Option<String> = None;
        ui.horizontal_wrapped(|ui| {
            ui.label(format!(
                "Binary of: {}",
                if self.workspace.is_empty() {
                    "(no workspace selected)"
                } else {
                    &self.workspace
                }
            ));
            let arts = self.artifacts();
            if arts.len() > 1 {
                ui.separator();
                let shown = self.answer.body["artifact"]
                    .as_str()
                    .unwrap_or("")
                    .to_string();
                egui::ComboBox::from_id_salt("binview-artifact")
                    .selected_text(&shown)
                    .show_ui(ui, |ui| {
                        for (a, bytes) in &arts {
                            if ui
                                .selectable_label(*a == shown, format!("{a} · {bytes} B"))
                                .clicked()
                            {
                                pick = Some(a.clone());
                            }
                        }
                    });
            }
            if let Some(p) = &self.pending {
                ui.separator();
                ui.spinner();
                ui.label(egui::RichText::new(room::asking_line(p.started)).small());
            }
        });
        if let Some(a) = pick {
            self.artifact = a;
            self.bind();
        }
        ui.separator();
        match self.inner.as_mut() {
            Some(pane) => {
                ui.label(
                    egui::RichText::new(format!("room: {}", self.answer.room_line()))
                        .small()
                        .weak(),
                );
                let ev = self.answer.evidence_text();
                if !ev.is_empty() {
                    ui.label(egui::RichText::new(ev).small().weak());
                }
                pane.ui(ui);
            }
            // not a measurement: what it means in plain words, then the state and
            // the server's evidence — no treemap, no number
            None => {
                if let Some(line) = plain_line(&self.answer) {
                    ui.label(egui::RichText::new(line).strong());
                }
                room::callout(ui, "binary", &self.answer);
            }
        }
    }

    fn state_json(&self) -> serde_json::Value {
        // The pane's own state when the answer was measured; else NO pane keys at all —
        // a reader of this JSON must not find a `crate_count: 0` nobody counted.
        let mut v = match &self.inner {
            Some(pane) => pane.state_json(),
            None => serde_json::json!({ "kind": "binview" }),
        };
        if let Some(obj) = v.as_object_mut() {
            obj.insert(
                "selected_workspace".into(),
                serde_json::json!(self.workspace),
            );
            obj.insert("from_room".into(), serde_json::json!(self.inner.is_some()));
            obj.insert("feed".into(), serde_json::json!(BINARY_FEED));
            obj.insert("pending".into(), serde_json::json!(self.pending.is_some()));
            obj.insert(
                "artifacts".into(),
                serde_json::json!(self
                    .artifacts()
                    .into_iter()
                    .map(|(a, _)| a)
                    .collect::<Vec<_>>()),
            );
            obj.insert("room".into(), self.answer.state_json());
            obj.insert(
                "source".into(),
                self.answer
                    .body
                    .get("source")
                    .cloned()
                    .unwrap_or(serde_json::Value::Null),
            );
            obj.insert("plain".into(), serde_json::json!(plain_line(&self.answer)));
        }
        v
    }

    fn update_json(&mut self, msg_json: &str) {
        if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
            // The selector's broadcast: read the SELECTED workspace's room.
            if let Some(ws) = v.get("set_workspace").and_then(|t| t.as_str()) {
                if let Some((endpoint, token)) = &self.forge {
                    self.source = ForgeRoom::shared(endpoint, token, ws);
                    // an in-flight read of the previous workspace must not land as this one's
                    self.pending = None;
                }
                self.workspace = ws.to_string();
                self.artifact.clear();
                self.bind();
                return;
            }
            if let Some(a) = v.get("artifact").and_then(|t| t.as_str()) {
                self.artifact = a.to_string();
                self.pending = None;
                self.bind();
                return;
            }
            if v.get("wait").and_then(|t| t.as_bool()) == Some(true) {
                self.wait();
                return;
            }
        }
        if let Some(pane) = self.inner.as_mut() {
            pane.update_json(msg_json);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::room::testing::{refusal, room, CannedRoom};
    use serde_json::json;

    /// A `Viz.Binary` answer as the reader sends it: the `shop` artifact — charge 300 B,
    /// checkout 120 B, a `core` fn 40 B — with three releases in its ledger.
    fn binary_body() -> serde_json::Value {
        json!({
            "state": "complete",
            "artifacts": [{ "artifact": "shop@x86_64-unknown-linux-gnu:release", "binary": "shop", "symbols": 3, "bytes": 460, "imports": 1, "file_size": 9000, "text_size": 4000 }],
            "artifact": "shop@x86_64-unknown-linux-gnu:release",
            "crates": [{ "crate": "shop", "bytes": 420, "symbols": 2 }, { "crate": "core", "bytes": 40, "symbols": 1 }],
            "symbols": [
                { "symbol": "shop::pay::charge", "crate": "shop", "bytes": 300 },
                { "symbol": "shop::checkout", "crate": "shop", "bytes": 120 },
                { "symbol": "core::fmt::write", "crate": "core", "bytes": 40 },
            ],
            "total_symbols": 3,
            "history": [{ "rev": 1, "file_size": 8000 }, { "rev": 2, "file_size": null }, { "rev": 3, "file_size": 9000 }],
            "evidence": { "notes": ["hardening (RELRO · PIE · NX · stack canary · fortify) is not in the binary layer"] },
            "room": room("nordisk", "shop", 2, 120, 340),
        })
    }

    /// The tab asks the code room's binary half and draws it on the reused pane: the
    /// treemap by crate, the size history from the ledger (an unmeasured release left
    /// out), NO hardening badge — and it asks nothing else (no pre-v2 table).
    #[test]
    fn the_binary_tab_draws_the_rooms_binary_half() {
        let src = CannedRoom::new(&[(S_BINARY, binary_body())]);
        let mut tab = BinaryTab::with_source(src.clone(), "nordisk");
        tab.wait();
        let st = Facet::state_json(&tab);
        assert_eq!(
            (st["kind"].as_str(), st["from_room"].as_bool()),
            (Some("binview"), Some(true)),
            "{st}"
        );
        assert_eq!(st["artifact"], "shop@x86_64-unknown-linux-gnu:release");
        assert_eq!(st["crate_count"], 2);
        assert_eq!(
            (
                st["treemap"][0]["crate"].as_str(),
                st["treemap"][0]["bytes"].as_u64()
            ),
            (Some("shop"), Some(420))
        );
        assert_eq!(st["history"], json!([8000, 9000]));
        assert!(
            st["checksec"].is_null(),
            "no hardening badge from a layer that does not grade the ELF: {st}"
        );
        assert_eq!(st["room"]["state"], "complete");
        let asked: Vec<String> = src
            .asked
            .lock()
            .unwrap()
            .iter()
            .map(|(s, _)| s.clone())
            .collect();
        assert_eq!(asked, [S_BINARY]);
        // picking an artifact asks for it
        Facet::update_json(&mut tab, r#"{"artifact":"shop"}"#);
        tab.wait();
        assert_eq!(
            src.asked.lock().unwrap().last().unwrap().1,
            json!({ "artifact": "shop" })
        );
    }

    /// `no_artifact` — what dwarf's live room answers (nothing builds where the
    /// binary hook reads) — is a state the tab knows: the server's sentence as
    /// evidence and a plain line, never "a state this client knows not".
    #[test]
    fn no_artifact_is_said_plainly_with_the_servers_reason() {
        let why = "the room [nornir@11 (3108a5c062)] holds no linked symbol: nornir: no_artifact — no linked artifact of this commit — looked in /home/dwarves/.nornir/workspaces/nordisk/builds/mimir_v2/artifacts/nornir/3108a5c0; no Build Thing run has filed an artifact with this server either (`Coderoom.Artifacts` is empty)";
        let mut tab = BinaryTab::with_source(
            CannedRoom::new(&[(S_BINARY, refusal("no_artifact", why))]),
            "nordisk",
        );
        tab.wait();
        let st = Facet::state_json(&tab);
        assert_eq!(
            (st["room"]["state"].as_str(), st["from_room"].as_bool()),
            (Some("no_artifact"), Some(false)),
            "{st}"
        );
        assert_eq!(
            st["room"]["evidence"], why,
            "the server's reason, not `unstated`: {st}"
        );
        assert!(
            st["plain"]
                .as_str()
                .unwrap_or("")
                .starts_with("No linked artifact in the room yet"),
            "{st}"
        );
        assert_eq!(chip_of_state(&tab), "NO ARTIFACT");
    }

    /// A Build Thing artifact (`source: build`) is drawn with its MEASURED hardening.
    #[test]
    fn a_build_artifact_carries_its_measured_hardening() {
        let mut body = binary_body();
        body["source"] = json!("build");
        body["checksec"] = json!({ "relro": "full", "pie": true, "nx": true, "stack_canary": false, "fortify": true, "stripped": true, "total_bytes": 9000 });
        let mut tab = BinaryTab::with_source(CannedRoom::new(&[(S_BINARY, body)]), "nordisk");
        tab.wait();
        let st = Facet::state_json(&tab);
        assert_eq!(st["source"], "build");
        let badges = st["checksec"]
            .as_array()
            .unwrap_or_else(|| panic!("measured badges: {st}"));
        assert!(
            badges
                .iter()
                .any(|b| b["label"] == "PIE" && b["ok"] == true)
                && badges
                    .iter()
                    .any(|b| b["label"] == "CANARY" && b["ok"] == false),
            "{st}"
        );
    }

    fn chip_of_state(tab: &BinaryTab) -> String {
        room::chip_of(&tab.answer).0
    }

    /// A room with no linked artifact, or an answer still on the jera queue, is its
    /// state — no pane, no treemap, no number.
    #[test]
    fn no_binary_is_a_state_not_an_empty_treemap() {
        for body in [
            refusal(
                "not_consumed",
                "the room holds no linked symbol: the binary stream of its repos was not consumed",
            ),
            json!({ "state": "running", "job_id": "j-3", "note": "running on jera — job j-3 (running 2s); ask again for the answer" }),
        ] {
            let word = body["state"].as_str().unwrap().to_string();
            let mut tab = BinaryTab::with_source(CannedRoom::new(&[(S_BINARY, body)]), "nordisk");
            tab.wait();
            let st = Facet::state_json(&tab);
            assert_eq!(
                (st["from_room"].as_bool(), st["room"]["state"].as_str()),
                (Some(false), Some(word.as_str())),
                "{st}"
            );
            assert!(
                st.get("crate_count").is_none() && st.get("treemap").is_none(),
                "no pane keys: {st}"
            );
        }
    }
}