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, ¶ms)
}));
}
/// 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}"
);
}
}
}