cargo-treemap 0.1.0

Interactive treemap analyzer for Rust binary size and dependency API usage — like webpack-bundle-analyzer, for cargo.
//! Render a diff: terminal table, JSON, or an HTML treemap of changes.

use crate::cli::{Format, Metric, TreemapArgs};
use crate::diff::DiffNode;
use crate::model::Meta;
use crate::util::{human_bytes, signed_bytes};
use anyhow::{Context, Result};
use std::cmp::Reverse;
use std::path::{Path, PathBuf};

fn delta_of(n: &DiffNode, m: Metric) -> i64 {
    match m {
        Metric::Data => n.delta_data(),
        Metric::Gzip => n.delta_gzip(),
        _ => n.delta_text(),
    }
}
fn base_of(n: &DiffNode, m: Metric) -> u64 {
    match m {
        Metric::Data => n.base_data,
        Metric::Gzip => n.base_gzip,
        _ => n.base_text,
    }
}
fn head_of(n: &DiffNode, m: Metric) -> u64 {
    match m {
        Metric::Data => n.head_data,
        Metric::Gzip => n.head_gzip,
        _ => n.head_text,
    }
}

pub fn emit(diff: &DiffNode, meta: &Meta, base_path: &Path, args: &TreemapArgs) -> Result<()> {
    match args.format {
        Format::Table => {
            table(diff, base_path, args.metric);
            Ok(())
        }
        Format::Json => {
            let text = serde_json::to_string_pretty(diff)?;
            match &args.output {
                Some(p) => {
                    std::fs::write(p, text).with_context(|| format!("writing {}", p.display()))?;
                    eprintln!("cargo-treemap: wrote {}", p.display());
                }
                None => println!("{text}"),
            }
            Ok(())
        }
        Format::Html => {
            let html = super::diffhtml::render(diff, meta, base_path, args.metric);
            let path = args
                .output
                .clone()
                .unwrap_or_else(|| PathBuf::from("cargo-treemap-diff.html"));
            std::fs::write(&path, html).with_context(|| format!("writing {}", path.display()))?;
            let abs = std::fs::canonicalize(&path).unwrap_or(path);
            eprintln!("cargo-treemap: wrote {}", abs.display());
            if args.open {
                super::open_in_browser(&abs);
            }
            Ok(())
        }
    }
}

fn table(diff: &DiffNode, base_path: &Path, metric: Metric) {
    let mut rows: Vec<&DiffNode> = diff.groups.iter().collect();
    rows.sort_by_key(|n| Reverse(delta_of(n, metric).abs()));

    println!(
        "\n  Size comparison ({}): {} (base) vs current build\n",
        metric.field(),
        base_path.display()
    );
    println!("  {:>11}  {:>9}  {:>9}  crate", "change", "base", "head");
    println!("  {}", "-".repeat(52));
    let mut unchanged = 0;
    for n in &rows {
        let d = delta_of(n, metric);
        if d == 0 {
            unchanged += 1;
            continue;
        }
        println!(
            "  {:>11}  {:>9}  {:>9}  {}{}",
            signed_bytes(d),
            human_bytes(base_of(n, metric)),
            human_bytes(head_of(n, metric)),
            n.label,
            tag(n, metric),
        );
    }
    println!("  {}", "-".repeat(52));
    println!(
        "  {:>11}  {:>9}  {:>9}  (total)",
        signed_bytes(delta_of(diff, metric)),
        human_bytes(base_of(diff, metric)),
        human_bytes(head_of(diff, metric)),
    );
    if unchanged > 0 {
        println!("\n  {unchanged} crates unchanged");
    }
    println!();
}

fn tag(n: &DiffNode, m: Metric) -> &'static str {
    if base_of(n, m) == 0 {
        " (new)"
    } else if head_of(n, m) == 0 {
        " (removed)"
    } else {
        ""
    }
}