cargo-treemap 0.1.0

Interactive treemap analyzer for Rust binary size and dependency API usage — like webpack-bundle-analyzer, for cargo.
//! cargo-bloat-style ranked terminal table over the top-level groups (crates).

use crate::apiusage::DepValue;
use crate::cli::Metric;
use crate::model::{Meta, Node};
use crate::parity::CrateRow;
use crate::util::human_bytes;
use std::cmp::Reverse;

fn metric_of(n: &Node, m: Metric) -> u64 {
    match m {
        Metric::Text => n.text,
        Metric::Data => n.data,
        Metric::Gzip => n.gzip,
        Metric::Mono => n.count as u64,
        Metric::Stat => n.stat.unwrap_or(0),
    }
}

pub fn render(root: &Node, meta: &Meta, deps: &[DepValue], crates: &[CrateRow], metric: Metric) {
    // Sum over the rows: crate-level metrics (stat/mono) aren't rolled up onto the root node.
    let total = root
        .groups
        .iter()
        .map(|n| metric_of(n, metric))
        .sum::<u64>()
        .max(1);

    let mut rows: Vec<&Node> = root.groups.iter().collect();
    rows.sort_by_key(|n| Reverse(metric_of(n, metric)));

    println!(
        "\n  {:>9}  {:>9}  {:>9}  {:>6}  Crate",
        "Text", "Data", "Gzip", "%"
    );
    println!("  {}", "-".repeat(58));
    for n in &rows {
        let v = metric_of(n, metric);
        if v == 0 {
            continue;
        }
        let pct = 100.0 * v as f64 / total as f64;
        println!(
            "  {:>9}  {:>9}  {:>9}  {:>5.1}%  {}",
            human_bytes(n.text),
            human_bytes(n.data),
            human_bytes(n.gzip),
            pct,
            n.label
        );
    }
    println!("  {}", "-".repeat(58));
    println!(
        "  {:>9}  {:>9}  {:>9}  {:>5.1}%  {} total",
        human_bytes(root.text),
        human_bytes(root.data),
        human_bytes(root.gzip),
        100.0,
        root.label
    );
    println!(
        "\n  {} · {} · profile {} · sorted by {}\n",
        meta.artifacts.join(", "),
        meta.format,
        meta.profile,
        metric.field(),
    );

    if !crates.is_empty() {
        render_crates(crates);
    }
    if !deps.is_empty() {
        render_deps(deps, meta.api_accurate);
    }
}

fn render_crates(crates: &[CrateRow]) {
    let has_cc = crates.iter().any(|c| c.compile_ms > 0.0);
    let cc = if has_cc { "compile, " } else { "" };
    println!("  Per-crate metrics (binary size, source, {cc}unsafe, monomorphizations)\n");
    if has_cc {
        println!(
            "  {:>9}  {:>7}  {:>6}  {:>7}  {:>8}  {:>10}  crate",
            ".text", "LOC", "unsafe", "mono", "build", "kind"
        );
    } else {
        println!(
            "  {:>9}  {:>7}  {:>6}  {:>7}  {:>10}  crate",
            ".text", "LOC", "unsafe", "mono", "kind"
        );
    }
    println!("  {}", "-".repeat(if has_cc { 74 } else { 64 }));
    for c in crates.iter().take(40) {
        if has_cc {
            println!(
                "  {:>9}  {:>7}  {:>6}  {:>7}  {:>8}  {:>10}  {}",
                human_bytes(c.text),
                c.loc,
                c.unsafe_n,
                c.mono,
                fmt_ms(c.compile_ms),
                short_kind(&c.dep_kind),
                c.name,
            );
        } else {
            println!(
                "  {:>9}  {:>7}  {:>6}  {:>7}  {:>10}  {}",
                human_bytes(c.text),
                c.loc,
                c.unsafe_n,
                c.mono,
                short_kind(&c.dep_kind),
                c.name,
            );
        }
    }
    println!();
}

fn fmt_ms(ms: f64) -> String {
    if ms <= 0.0 {
        "-".to_string()
    } else if ms >= 1000.0 {
        format!("{:.2}s", ms / 1000.0)
    } else {
        format!("{:.0}ms", ms)
    }
}

fn short_kind(k: &str) -> &str {
    match k {
        "proc-macro" => "proc",
        "workspace" => "ws",
        other => other,
    }
}

fn render_deps(deps: &[DepValue], accurate: bool) {
    println!("  Dependency API usage (how much of each dep's public API you use)\n");
    println!(
        "  {:>7}  {:>5}  {:>6}  {:>9}  dependency",
        "surface", "used", "ratio", "size"
    );
    println!("  {}", "-".repeat(64));
    for d in deps {
        let ratio = match d.ratio {
            Some(r) => format!("{:.1}%", 100.0 * r),
            None => "-".to_string(),
        };
        let verdict = if d.is_proc_macro {
            "  proc-macro (unmeasurable)"
        } else if d.flag_unused {
            "  ⚠ unused?"
        } else if d.flag_low_value {
            "  ⚠ low-value / high-size"
        } else if !d.rustdoc_ok {
            "  (rustdoc unavailable)"
        } else if d.ratio.is_none() {
            "  (no measurable fns)"
        } else {
            ""
        };
        println!(
            "  {:>7}  {:>5}  {:>6}  {:>9}  {}{}",
            d.surface_fns,
            d.used_fns,
            ratio,
            human_bytes(d.size_text + d.size_data),
            d.name,
            verdict,
        );
    }
    if accurate {
        println!("\n  usage from an opt-0 build (cross-crate calls not inlined), ratios are trustworthy.");
    } else {
        println!("\n  note: 'used' is a LOWER BOUND, release inlining hides usage. Add --accurate for true ratios.");
    }
    println!();
}