cargo-treemap 0.1.0

Interactive treemap analyzer for Rust binary size and dependency API usage — like webpack-bundle-analyzer, for cargo.
//! The unified hierarchical model: workspace/artifact -> crate -> module -> symbol.
//! Every node carries all metrics (rolled up over its subtree) so the treemap can switch
//! the active metric entirely client-side.

use crate::artifact::{ParsedArtifact, SymClass};
use crate::attribute::Attributor;
use crate::dwarf::DwarfResolver;
use crate::util::{leading_ident, split_path, strip_generics};
use serde::{Deserialize, Serialize};
use std::cmp::Reverse;
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
pub struct Node {
    pub label: String,
    pub path: String,
    pub text: u64,
    pub data: u64,
    pub gzip: u64,
    pub count: u32,
    #[serde(default, skip_serializing_if = "str::is_empty")]
    pub conf: String,
    // Per-crate parity metrics, set only on crate-level nodes (the treemap falls back to text
    // for nodes without them).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stat: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub loc: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub unsafe_n: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dep_kind: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub groups: Vec<Node>,
}

#[derive(Serialize)]
pub struct Meta {
    pub tool: String,
    pub artifacts: Vec<String>,
    pub format: String,
    pub target: String,
    pub profile: String,
    pub total_text: u64,
    pub total_data: u64,
    pub total_gzip: u64,
    pub default_metric: String,
    /// True when the API-usage used-set came from an opt-0 `--accurate` build (trustworthy
    /// ratios); false means it was read from the possibly-inlined analyzed binary (lower bound).
    pub api_accurate: bool,
    // Provenance: exactly what produced this report.
    pub invocation: String,
    pub cwd: String,
    pub generated_unix: u64,
}

struct Tmp {
    label: String,
    path: String,
    text: u64,
    data: u64,
    gzip: u64,
    count: u32,
    conf: &'static str,
    children: HashMap<String, Tmp>,
}

impl Tmp {
    fn new(label: &str, path: String) -> Self {
        Tmp {
            label: label.to_string(),
            path,
            text: 0,
            data: 0,
            gzip: 0,
            count: 0,
            conf: "",
            children: HashMap::new(),
        }
    }
}

/// `crate :: module... :: leaf`, with monomorphizations collapsed at the leaf.
fn path_segments(crate_name: &str, demangled: &str) -> Vec<String> {
    let dsegs = split_path(demangled);
    if dsegs.is_empty() {
        return vec![crate_name.to_string(), demangled.to_string()];
    }
    let leaf = strip_generics(dsegs.last().unwrap());
    let mut out = Vec::with_capacity(dsegs.len() + 1);
    out.push(crate_name.to_string());
    // Drop a leading path segment that just repeats the (already-attributed) crate.
    let start = if leading_ident(&dsegs[0]) == crate_name { 1 } else { 0 };
    let module_end = dsegs.len() - 1;
    for seg in dsegs.iter().take(module_end).skip(start) {
        out.push(strip_generics(seg));
    }
    out.push(leaf);
    out
}

fn insert(root: &mut Tmp, segs: &[String], text: u64, data: u64, gzip: u64, conf: &'static str) {
    let mut cur = root;
    let mut acc = String::new();
    for seg in segs {
        if !acc.is_empty() {
            acc.push_str("::");
        }
        acc.push_str(seg);
        let path = acc.clone();
        cur = cur
            .children
            .entry(seg.clone())
            .or_insert_with(|| Tmp::new(seg, path));
    }
    cur.text += text;
    cur.data += data;
    cur.gzip += gzip;
    cur.count += 1;
    if cur.conf.is_empty() {
        cur.conf = conf;
    }
}

fn to_node(t: Tmp) -> Node {
    let mut groups: Vec<Node> = t.children.into_values().map(to_node).collect();
    let mut text = t.text;
    let mut data = t.data;
    let mut gzip = t.gzip;
    let mut count = t.count;
    for c in &groups {
        text += c.text;
        data += c.data;
        gzip += c.gzip;
        count += c.count;
    }
    groups.sort_by(|a, b| b.text.cmp(&a.text).then_with(|| a.label.cmp(&b.label)));
    Node {
        label: t.label,
        path: t.path,
        text,
        data,
        gzip,
        count,
        conf: t.conf.to_string(),
        stat: None,
        loc: None,
        unsafe_n: None,
        dep_kind: None,
        groups,
    }
}

/// Build the crate->module->symbol tree for one artifact.
pub fn build_tree(art: &ParsedArtifact, attr: &Attributor, dwarf: Option<&DwarfResolver>) -> Node {
    // Path = the artifact's own name so that, in a multi-artifact view, each artifact node has
    // a distinct top-level key (own color + independent legend toggle).
    let mut root = Tmp::new(&art.name, art.name.clone());
    for sym in &art.symbols {
        let a = attr.attribute(&sym.name, sym.address, dwarf);
        let segs = path_segments(&a.crate_name, &a.demangled);
        let (t, d) = match sym.class {
            SymClass::Text => (sym.size, 0),
            SymClass::Data => (0, sym.size),
        };
        insert(&mut root, &segs, t, d, sym.gzip_size, a.confidence.as_str());
    }
    let slack_text = art.totals.text.saturating_sub(art.totals.text_attributed);
    let slack_data = art.totals.data.saturating_sub(art.totals.data_attributed);
    if slack_text + slack_data > 0 {
        insert(
            &mut root,
            &["[unattributed]".to_string()],
            slack_text,
            slack_data,
            0,
            "unknown",
        );
    }
    to_node(root)
}

/// One artifact -> its node; several -> a synthetic root grouping them.
pub fn combine(mut nodes: Vec<Node>) -> Node {
    if nodes.len() == 1 {
        return nodes.pop().unwrap();
    }
    let (mut text, mut data, mut gzip, mut count) = (0u64, 0u64, 0u64, 0u32);
    for n in &nodes {
        text += n.text;
        data += n.data;
        gzip += n.gzip;
        count += n.count;
    }
    nodes.sort_by_key(|n| Reverse(n.text));
    Node {
        label: "(artifacts)".to_string(),
        path: String::new(),
        text,
        data,
        gzip,
        count,
        conf: String::new(),
        stat: None,
        loc: None,
        unsafe_n: None,
        dep_kind: None,
        groups: nodes,
    }
}

/// The artifact-level nodes (the single root, or each child of a synthetic multi-artifact root).
pub fn artifact_nodes(root: &Node) -> Vec<&Node> {
    if root.path.is_empty() && !root.groups.is_empty() {
        root.groups.iter().collect()
    } else {
        vec![root]
    }
}

/// Per top-level crate, its rolled-up (text, data, gzip, count) summed across artifacts.
pub fn crate_bin_totals(root: &Node) -> HashMap<String, (u64, u64, u64, u32)> {
    let mut out: HashMap<String, (u64, u64, u64, u32)> = HashMap::new();
    for art in artifact_nodes(root) {
        for c in &art.groups {
            let e = out.entry(c.label.clone()).or_default();
            e.0 += c.text;
            e.1 += c.data;
            e.2 += c.gzip;
            e.3 += c.count;
        }
    }
    out
}

/// Attach per-crate parity metrics to the crate-level nodes.
pub fn attach_metrics(root: &mut Node, metrics: &HashMap<String, crate::parity::CrateMetrics>) {
    let crate_nodes: Vec<&mut Node> = if root.path.is_empty() && !root.groups.is_empty() {
        root.groups.iter_mut().flat_map(|a| a.groups.iter_mut()).collect()
    } else {
        root.groups.iter_mut().collect()
    };
    for c in crate_nodes {
        if let Some(m) = metrics.get(&c.label) {
            c.stat = Some(m.src_bytes);
            c.loc = Some(m.loc);
            c.unsafe_n = Some(m.unsafe_n);
            if !m.dep_kind.is_empty() {
                c.dep_kind = Some(m.dep_kind.clone());
            }
        }
    }
}