cargo-treemap 0.1.0

Interactive treemap analyzer for Rust binary size and dependency API usage — like webpack-bundle-analyzer, for cargo.
//! DWARF-based attribution: resolve a symbol's address to its source file, then map that path
//! to the owning crate. More accurate than symbol-name heuristics, since it credits code to
//! the crate whose source defines it (correctly placing monomorphized generics), at the cost
//! of needing debug info.
//!
//! `addr2line::Loader` reads DWARF from the binary, an adjacent `.dSYM` bundle, or the `.o`
//! files a Mach-O debug map points at, so it works on ELF and macOS alike.
//!
//! This only refines *code*. Anonymous read-only data (const-promoted literals, compiler
//! constants) has no symbol and no DIE, so it can't be attributed by address; a DIE walk over
//! `DW_TAG_variable` was tried and measured to reclaim ~nothing (named statics already resolve
//! by symbol name; the rest is section slack), so it isn't done.

use anyhow::{anyhow, Result};
use std::path::Path;

pub struct DwarfResolver {
    loader: addr2line::Loader,
}

impl DwarfResolver {
    pub fn load(path: &Path) -> Result<Self> {
        let loader = addr2line::Loader::new(path).map_err(|e| anyhow!("{e}"))?;
        Ok(DwarfResolver { loader })
    }

    /// The crate owning the code at `addr`, from its source file, or None if unmapped.
    pub fn crate_for_addr(&self, addr: u64) -> Option<String> {
        let loc = self.loader.find_location(addr).ok()??;
        crate_from_path(loc.file?)
    }
}

/// Map a source file path to a crate name (underscore-normalized, to match symbol crates).
///   registry dep: `…/registry/src/<index>/<name>-<version>/src/…`  -> `<name>`
///   std:          `…/rustc/<hash>/library/<core|alloc|std|…>/…`     -> `core`/`alloc`/`std`
/// Git deps and local/workspace paths return None (fall back to name-based attribution).
fn crate_from_path(path: &str) -> Option<String> {
    if let Some(rest) = after(path, "/registry/src/") {
        let comp = rest.split('/').nth(1)?;
        return Some(strip_version(comp).replace('-', "_"));
    }
    if let Some(rest) = after(path, "/library/") {
        if path.contains("/rustc/") || path.contains("/lib/rustlib/") {
            return Some(rest.split('/').next()?.replace('-', "_"));
        }
    }
    None
}

fn after<'a>(hay: &'a str, needle: &str) -> Option<&'a str> {
    hay.find(needle).map(|i| &hay[i + needle.len()..])
}

/// `regex-automata-0.4.3` -> `regex-automata`; strips a trailing `-<semver>`.
fn strip_version(dir: &str) -> String {
    let parts: Vec<&str> = dir.split('-').collect();
    let cut = parts
        .iter()
        .position(|p| p.chars().next().is_some_and(|c| c.is_ascii_digit()))
        .unwrap_or(parts.len());
    if cut == 0 {
        dir.to_string()
    } else {
        parts[..cut].join("-")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_path_to_crate() {
        assert_eq!(
            crate_from_path(
                "/Users/x/.cargo/registry/src/index.crates.io-abc/regex-automata-0.4.3/src/dfa.rs"
            ),
            Some("regex_automata".to_string())
        );
        assert_eq!(
            crate_from_path("/root/.cargo/registry/src/idx/serde_json-1.0.150/src/de.rs"),
            Some("serde_json".to_string())
        );
    }

    #[test]
    fn std_path_to_crate() {
        assert_eq!(
            crate_from_path("/rustc/abc123/library/core/src/ptr/mod.rs"),
            Some("core".to_string())
        );
        assert_eq!(
            crate_from_path("/rustc/abc/library/alloc/src/vec.rs"),
            Some("alloc".to_string())
        );
    }

    #[test]
    fn local_path_is_unmapped() {
        assert_eq!(crate_from_path("/home/me/project/src/main.rs"), None);
        assert_eq!(crate_from_path("src/lib.rs"), None);
    }

    #[test]
    fn version_stripping() {
        assert_eq!(strip_version("regex-automata-0.4.3"), "regex-automata");
        assert_eq!(strip_version("serde_json-1.0.150"), "serde_json");
        assert_eq!(strip_version("rustc-demangle-0.1.27"), "rustc-demangle");
        assert_eq!(strip_version("noversion"), "noversion");
    }
}