use crate::cli::TreemapArgs;
use crate::model::{crate_bin_totals, Node};
use anyhow::{anyhow, Context, Result};
use cargo_metadata::camino::Utf8Path;
use cargo_metadata::{DependencyKind, Metadata, Package, PackageId, TargetKind};
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::process::Stdio;
#[derive(Serialize)]
pub struct CrateRow {
pub name: String,
pub text: u64,
pub data: u64,
pub gzip: u64,
pub mono: u32,
pub loc: u64,
pub src_bytes: u64,
pub unsafe_n: u32,
pub compile_ms: f64,
pub dep_kind: String,
}
pub fn crate_rows(root: &Node, metrics: &HashMap<String, CrateMetrics>) -> Vec<CrateRow> {
let bin = crate_bin_totals(root);
let mut names: HashSet<&String> = bin.keys().collect();
names.extend(metrics.keys());
let mut rows: Vec<CrateRow> = names
.into_iter()
.filter(|n| !n.starts_with('[')) .map(|name| {
let (text, data, gzip, mono) = bin.get(name).copied().unwrap_or_default();
let m = metrics.get(name).cloned().unwrap_or_default();
CrateRow {
name: name.clone(),
text,
data,
gzip,
mono,
loc: m.loc,
src_bytes: m.src_bytes,
unsafe_n: m.unsafe_n,
compile_ms: m.compile_ms,
dep_kind: if m.dep_kind.is_empty() {
"normal".to_string()
} else {
m.dep_kind
},
}
})
.collect();
rows.sort_by(|a, b| b.text.cmp(&a.text).then(b.src_bytes.cmp(&a.src_bytes)));
rows
}
#[derive(Serialize, Clone, Default)]
pub struct CrateMetrics {
pub loc: u64,
pub src_bytes: u64,
pub unsafe_n: u32,
pub dep_kind: String,
pub compile_ms: f64,
}
pub fn compute(
meta: &Metadata,
wanted: &HashSet<String>,
linked: &HashSet<String>,
) -> HashMap<String, CrateMetrics> {
let kinds = classify_kinds(meta);
let mut out = HashMap::new();
for pkg in &meta.packages {
let name = lib_name(pkg);
let base = kinds.get(&pkg.id).map(String::as_str).unwrap_or("normal");
let is_linked = linked.contains(&name);
let interesting = wanted.contains(&name) || (!is_linked && base != "dev");
if !interesting {
continue;
}
let dep_kind = if !is_linked {
"compile-only"
} else if base == "workspace" {
"workspace"
} else {
"normal"
};
let (loc, src_bytes, unsafe_n) = scan_source(pkg);
out.insert(
name,
CrateMetrics {
loc,
src_bytes,
unsafe_n,
dep_kind: dep_kind.to_string(),
compile_ms: 0.0,
},
);
}
out
}
pub fn linked_crate_names(meta: &Metadata) -> HashSet<String> {
let libs: HashMap<PackageId, String> =
meta.packages.iter().map(|p| (p.id.clone(), lib_name(p))).collect();
let Some(resolve) = &meta.resolve else {
return libs.into_values().collect();
};
let proc: HashSet<PackageId> = meta
.packages
.iter()
.filter(|p| p.targets.iter().any(|t| t.kind.contains(&TargetKind::ProcMacro)))
.map(|p| p.id.clone())
.collect();
let deps: HashMap<PackageId, &Vec<cargo_metadata::NodeDep>> =
resolve.nodes.iter().map(|n| (n.id.clone(), &n.deps)).collect();
let mut seen: HashSet<PackageId> = HashSet::new();
let mut stack: Vec<PackageId> = Vec::new();
for m in &meta.workspace_members {
if !proc.contains(m) && seen.insert(m.clone()) {
stack.push(m.clone());
}
}
while let Some(id) = stack.pop() {
if let Some(ds) = deps.get(&id) {
for d in *ds {
if d.dep_kinds.iter().any(|k| k.kind == DependencyKind::Normal)
&& !proc.contains(&d.pkg)
&& seen.insert(d.pkg.clone())
{
stack.push(d.pkg.clone());
}
}
}
}
seen.iter().filter_map(|id| libs.get(id).cloned()).collect()
}
pub fn lib_target_name(pkg: &Package) -> Option<String> {
pkg.targets
.iter()
.find(|t| {
t.kind.iter().any(|k| {
matches!(
k,
TargetKind::Lib
| TargetKind::RLib
| TargetKind::ProcMacro
| TargetKind::CDyLib
| TargetKind::DyLib
)
})
})
.map(|t| t.name.replace('-', "_"))
}
fn lib_name(pkg: &Package) -> String {
lib_target_name(pkg).unwrap_or_else(|| pkg.name.to_string().replace('-', "_"))
}
fn scan_source(pkg: &Package) -> (u64, u64, u32) {
let Some(root) = pkg.manifest_path.parent() else {
return (0, 0, 0);
};
let src = root.join("src");
let dir = if src.exists() { src } else { root.to_path_buf() };
let (mut loc, mut bytes, mut unsafe_n) = (0u64, 0u64, 0u32);
let mut stack = vec![dir.into_std_path_buf()];
while let Some(d) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&d) else {
continue;
};
for ent in rd.flatten() {
let p = ent.path();
if p.is_dir() {
if p.file_name().and_then(|s| s.to_str()) != Some("target") {
stack.push(p);
}
} else if p.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Ok(text) = std::fs::read_to_string(&p) {
bytes += text.len() as u64;
loc += text.lines().filter(|l| !l.trim().is_empty()).count() as u64;
unsafe_n += count_word(&text, "unsafe");
}
}
}
}
(loc, bytes, unsafe_n)
}
fn count_word(src: &str, word: &str) -> u32 {
let mut n = 0;
let mut from = 0;
while let Some(rel) = src[from..].find(word) {
let i = from + rel;
let before = src[..i].chars().next_back();
let after = src[i + word.len()..].chars().next();
let ok = before.is_none_or(|c| !(c.is_alphanumeric() || c == '_'))
&& after.is_none_or(|c| !(c.is_alphanumeric() || c == '_'));
if ok {
n += 1;
}
from = i + word.len();
}
n
}
pub fn compile_times(args: &TreemapArgs, target_dir: &Utf8Path) -> Result<HashMap<String, f64>> {
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let timings_root = target_dir.join("treemap-timings");
let _ = std::fs::remove_dir_all(timings_root.as_std_path());
let mut cmd = std::process::Command::new(&cargo);
cmd.arg("build").arg("--timings");
cmd.env("CARGO_TARGET_DIR", timings_root.as_str());
if let Some(p) = &args.profile {
cmd.arg("--profile").arg(p);
} else if args.release {
cmd.arg("--release");
}
if args.features.all_features {
cmd.arg("--all-features");
}
if args.features.no_default_features {
cmd.arg("--no-default-features");
}
if !args.features.features.is_empty() {
cmd.arg("--features").arg(args.features.features.join(","));
}
crate::builder::push_build_flags(&mut cmd, args);
if let Some(mp) = &args.manifest.manifest_path {
cmd.arg("--manifest-path").arg(mp);
}
cmd.stdout(Stdio::null()).stderr(Stdio::inherit());
let status = cmd.status().context("spawning timed build")?;
if !status.success() {
anyhow::bail!("timed build failed");
}
let html = newest_timing_html(timings_root.join("cargo-timings").as_std_path())?;
let text = std::fs::read_to_string(&html).context("reading timings html")?;
parse_unit_data(&text)
}
fn newest_timing_html(dir: &Path) -> Result<PathBuf> {
let mut best: Option<(String, PathBuf)> = None;
for e in std::fs::read_dir(dir).context("reading cargo-timings dir")?.flatten() {
let p = e.path();
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("").to_string();
if name.starts_with("cargo-timing-")
&& name.ends_with(".html")
&& best.as_ref().is_none_or(|(b, _)| name > *b)
{
best = Some((name, p));
}
}
best.map(|(_, p)| p).ok_or_else(|| anyhow!("no timing report in {}", dir.display()))
}
fn parse_unit_data(html: &str) -> Result<HashMap<String, f64>> {
let key = "const UNIT_DATA = ";
let start = html.find(key).ok_or_else(|| anyhow!("no UNIT_DATA in timings html"))? + key.len();
let rest = &html[start..];
let end = rest.find("];").ok_or_else(|| anyhow!("malformed UNIT_DATA"))? + 1;
let units: serde_json::Value = serde_json::from_str(&rest[..end])?;
let mut out: HashMap<String, f64> = HashMap::new();
if let Some(arr) = units.as_array() {
for u in arr {
let name = u.get("name").and_then(|v| v.as_str()).unwrap_or("").replace('-', "_");
let dur = u.get("duration").and_then(|v| v.as_f64()).unwrap_or(0.0);
if !name.is_empty() {
*out.entry(name).or_insert(0.0) += dur * 1000.0;
}
}
}
Ok(out)
}
fn classify_kinds(meta: &Metadata) -> HashMap<PackageId, String> {
let mut out = HashMap::new();
let Some(resolve) = &meta.resolve else {
return out;
};
let nodes: HashMap<&PackageId, &cargo_metadata::Node> =
resolve.nodes.iter().map(|n| (&n.id, n)).collect();
let members: Vec<&PackageId> = meta.workspace_members.iter().collect();
let proc: HashSet<&PackageId> = meta
.packages
.iter()
.filter(|p| p.targets.iter().any(|t| t.kind.contains(&TargetKind::ProcMacro)))
.map(|p| &p.id)
.collect();
let normal = reach(&nodes, &members, DependencyKind::Normal);
let build = reach(&nodes, &members, DependencyKind::Build);
let dev = reach(&nodes, &members, DependencyKind::Development);
for p in &meta.packages {
let kind = if members.contains(&&p.id) {
"workspace"
} else if proc.contains(&p.id) {
"proc-macro"
} else if normal.contains(&p.id) {
"normal"
} else if build.contains(&p.id) {
"build"
} else if dev.contains(&p.id) {
"dev"
} else {
"normal"
};
out.insert(p.id.clone(), kind.to_string());
}
out
}
fn reach(
nodes: &HashMap<&PackageId, &cargo_metadata::Node>,
starts: &[&PackageId],
first_kind: DependencyKind,
) -> HashSet<PackageId> {
let mut seen = HashSet::new();
let mut stack = Vec::new();
for s in starts {
if let Some(n) = nodes.get(s) {
for d in &n.deps {
if d.dep_kinds.iter().any(|k| k.kind == first_kind) && seen.insert(d.pkg.clone()) {
stack.push(d.pkg.clone());
}
}
}
}
while let Some(id) = stack.pop() {
if let Some(n) = nodes.get(&id) {
for d in &n.deps {
if d.dep_kinds.iter().any(|k| k.kind == DependencyKind::Normal)
&& seen.insert(d.pkg.clone())
{
stack.push(d.pkg.clone());
}
}
}
}
seen
}