Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

src/mimir_lens/bloat.rs

//! **Lens A — the monomorphisation "generic bloat tax".**
//!
//! The code room's binary stream: every linked monomorphised instance
//! (`mono_instance` node, `INSTANCE_OF` → its generic item) with the size of
//! its symbol, per artifact.
//!
//! The model is dwarves' (`dwarves/crates/dwarves/src/mimir/bloat.rs`): a
//! generic instantiated N times costs `total − total / N` bytes more than one
//! shared copy would — the TAX — and only a NAMED generic function is
//! actionable (drop glue and trait-impl methods are reported, not actionable).

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use super::sources::{Feed, evidence_note};

/// Symbol-name fragments that mark compiler-generated drop glue.
const GLUE_MARKERS: &[&str] = &["drop_in_place", "drop_glue"];

/// What kind of generic a group is — because only one of the three is actionable.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GenericClass {
    /// Compiler drop glue. Not directly fixable; a proxy for distinct-type count.
    Glue,
    /// A generic trait impl whose owner was erased with the generics.
    TraitMethod,
    /// A named generic function. **The actionable list.**
    Named,
}

impl GenericClass {
    pub fn label(self) -> &'static str {
        match self {
            GenericClass::Glue => "glue",
            GenericClass::TraitMethod => "trait-method",
            GenericClass::Named => "named",
        }
    }

    pub fn actionable(self) -> bool {
        matches!(self, GenericClass::Named)
    }
}

/// Classify a generic by its path (the generic item's readable path, or a
/// generics-erased symbol name).
pub fn classify(no_generics: &str) -> GenericClass {
    if GLUE_MARKERS.iter().any(|m| no_generics.contains(m)) {
        return GenericClass::Glue;
    }
    let tail = no_generics.rsplit("::").next().unwrap_or("");
    if no_generics.starts_with("::") || no_generics.contains("_::::") || tail.is_empty() {
        return GenericClass::TraitMethod;
    }
    GenericClass::Named
}

/// One generic, and what its instantiations cost in one artifact.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenericGroup {
    pub symbol: String,
    pub crate_name: String,
    pub class: GenericClass,
    pub instantiations: u32,
    pub total_bytes: u64,
    pub tax_bytes: u64,
    pub example: String,
}

/// One analysed artifact's generic cost.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ArtifactBloat {
    pub repo: String,
    /// The artifact (`name@target-triple:profile`).
    pub artifact: String,
    /// Bytes of ALL its linked symbols (not the file size).
    pub total_bytes: u64,
    pub mono_bytes: u64,
    pub tax_bytes: u64,
    pub tax_pct: f64,
    pub tax_by_class: BTreeMap<String, u64>,
    pub worst: Vec<GenericGroup>,
    pub worst_named: Vec<GenericGroup>,
}

impl ArtifactBloat {
    pub fn actionable_tax(&self) -> u64 {
        self.tax_by_class
            .get(GenericClass::Named.label())
            .copied()
            .unwrap_or(0)
    }
}

/// The whole reading.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct BloatReport {
    pub artifacts: Vec<ArtifactBloat>,
    pub feeds: Vec<Feed>,
    pub evidence_note: String,
}

impl BloatReport {
    pub fn total_tax(&self) -> u64 {
        self.artifacts.iter().map(|a| a.tax_bytes).sum()
    }
    pub fn total_actionable_tax(&self) -> u64 {
        self.artifacts
            .iter()
            .map(ArtifactBloat::actionable_tax)
            .sum()
    }

    pub fn state_json(&self) -> serde_json::Value {
        serde_json::json!({
            "kind": "bloat",
            "artifacts": self.artifacts.len(),
            "total_tax_bytes": self.total_tax(),
            "actionable_tax_bytes": self.total_actionable_tax(),
            "evidence_note": self.evidence_note,
            "feeds": self.feeds,
            "worst_artifacts": self.artifacts.iter().map(|a| serde_json::json!({
                "repo": a.repo, "artifact": a.artifact,
                "total_bytes": a.total_bytes, "tax_bytes": a.tax_bytes,
                "tax_pct": a.tax_pct, "actionable_tax_bytes": a.actionable_tax(),
                "top_named": a.worst_named.iter().take(5).map(|g| serde_json::json!({
                    "symbol": g.symbol, "crate": g.crate_name,
                    "instantiations": g.instantiations, "tax_bytes": g.tax_bytes,
                })).collect::<Vec<_>>(),
            })).collect::<Vec<_>>(),
        })
    }
}

/// One linked monomorphised instance: the feed of this lens.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Instance {
    /// The artifact that links it.
    pub artifact: String,
    /// The crate that DEFINES the generic.
    pub crate_name: String,
    /// The generic item's path (`znippy::parse_stream`).
    pub generic: String,
    /// The generic is a method of a trait impl (its owner is a type, not a name).
    pub trait_impl: bool,
    /// The instance as rustc names it (`znippy::parse_stream::<u8>`).
    pub instance: String,
    pub bytes: u64,
}

/// The instances of ONE filed artifact, from its analysed symbols (a Build
/// Thing artifact's `symbol_sizes`): every sized code symbol that is a
/// monomorphisation — its demangled name carries generic arguments (v0
/// mangling), or, legacy-mangled (no arguments in the name, a hash per
/// instance), two or more distinct symbols share its name (cargo-bloat's
/// reading). The generic is the name without arguments; `trait_impl` = a
/// `<T as Trait>::m` method. Returns the instances and the bytes of ALL the
/// artifact's sized symbols.
pub fn instances_of(artifact: &str, symbols: &[nornir_introspect::size::SymbolSize]) -> (Vec<Instance>, u64) {
    let base = |s: &nornir_introspect::size::SymbolSize| {
        let n = if s.name_demangled_no_generics.is_empty() { s.name_demangled.as_str() } else { s.name_demangled_no_generics.as_str() };
        n.trim_end().trim_end_matches("::").to_string()
    };
    let code = |s: &&nornir_introspect::size::SymbolSize| s.size_bytes > 0 && s.section.starts_with(".text");
    let mut names: BTreeMap<String, std::collections::BTreeSet<&str>> = BTreeMap::new();
    for s in symbols.iter().filter(code) {
        names.entry(base(s)).or_default().insert(s.name_mangled.as_str());
    }
    let total: u64 = symbols.iter().map(|s| s.size_bytes).sum();
    let out = symbols
        .iter()
        .filter(code)
        .filter_map(|s| {
            let generic = base(s);
            let shared = names.get(&generic).is_some_and(|m| m.len() > 1);
            (s.is_monomorphization || shared).then(|| Instance {
                artifact: artifact.to_string(),
                crate_name: if s.crate_name.is_empty() { "?".to_string() } else { s.crate_name.clone() },
                trait_impl: generic.contains(" as "),
                instance: if s.name_demangled.is_empty() { s.name_mangled.clone() } else { s.name_demangled.clone() },
                generic,
                bytes: s.size_bytes,
            })
        })
        .collect();
    (out, total)
}

/// **Compute the generic bloat tax.** `artifacts` maps an artifact to
/// `(repo, bytes of all its linked symbols)`.
pub fn analyze(instances: &[Instance], artifacts: &BTreeMap<String, (String, u64)>, feeds: Vec<Feed>, min_instantiations: u32, top_n: usize) -> BloatReport {
    let mut agg: BTreeMap<(String, String, String), (u32, u64, String, bool)> = BTreeMap::new();
    for i in instances {
        let e = agg.entry((i.artifact.clone(), i.crate_name.clone(), i.generic.clone())).or_insert((0, 0, String::new(), i.trait_impl));
        e.0 += 1;
        e.1 += i.bytes;
        if e.2.is_empty() {
            e.2 = i.instance.clone();
        }
    }

    let mut per_artifact: BTreeMap<String, Vec<GenericGroup>> = BTreeMap::new();
    let mut mono_bytes: BTreeMap<String, u64> = BTreeMap::new();
    for ((artifact, krate, generic), (n, bytes, example, trait_impl)) in agg {
        *mono_bytes.entry(artifact.clone()).or_default() += bytes;
        if n < min_instantiations.max(2) {
            continue;
        }
        let tax = bytes.saturating_sub(bytes / n as u64);
        let class = match classify(&generic) {
            GenericClass::Named if trait_impl => GenericClass::TraitMethod,
            c => c,
        };
        per_artifact.entry(artifact).or_default().push(GenericGroup { class, symbol: generic, crate_name: krate, instantiations: n, total_bytes: bytes, tax_bytes: tax, example });
    }

    let mut out: Vec<ArtifactBloat> = per_artifact
        .into_iter()
        .map(|(artifact, mut groups)| {
            groups.sort_by(|a, b| b.tax_bytes.cmp(&a.tax_bytes).then_with(|| a.symbol.cmp(&b.symbol)));
            let tax_bytes: u64 = groups.iter().map(|g| g.tax_bytes).sum();
            let mut tax_by_class: BTreeMap<String, u64> = BTreeMap::new();
            for g in &groups {
                *tax_by_class.entry(g.class.label().to_string()).or_default() += g.tax_bytes;
            }
            let (repo, total_bytes) = artifacts.get(&artifact).cloned().unwrap_or_default();
            let worst_named: Vec<GenericGroup> = groups.iter().filter(|g| g.class.actionable()).take(top_n).cloned().collect();
            let mut worst = groups;
            worst.truncate(top_n);
            ArtifactBloat {
                repo,
                mono_bytes: mono_bytes.get(&artifact).copied().unwrap_or(0),
                artifact,
                total_bytes,
                tax_bytes,
                tax_pct: if total_bytes == 0 { 0.0 } else { 100.0 * tax_bytes as f64 / total_bytes as f64 },
                tax_by_class,
                worst,
                worst_named,
            }
        })
        .collect();
    out.sort_by_key(|a| std::cmp::Reverse(a.tax_bytes));

    let evidence_note = evidence_note(&feeds, "generic bloat tax");
    BloatReport { artifacts: out, feeds, evidence_note }
}

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

    fn inst(generic: &str, arg: &str, bytes: u64) -> Instance {
        Instance { artifact: "znippy@x86_64:release".into(), crate_name: "znippy".into(), generic: generic.into(), trait_impl: false, instance: format!("{generic}::<{arg}>"), bytes }
    }

    fn artifacts() -> BTreeMap<String, (String, u64)> {
        [("znippy@x86_64:release".to_string(), ("znippy".to_string(), 1000))].into()
    }

    fn feeds(rows: usize) -> Vec<Feed> {
        vec![Feed { table: "code_symbols".into(), rows, root: "code room".into(), missing: rows == 0 }]
    }

    #[test]
    fn a_generic_instantiated_four_times_reports_its_count_and_its_tax() {
        let i: Vec<Instance> = ["u8", "u16", "u32", "u64"].iter().map(|a| inst("znippy::parse_stream", a, 100)).collect();
        let r = analyze(&i, &artifacts(), feeds(4), 2, 10);
        assert_eq!(r.artifacts.len(), 1);
        let a = &r.artifacts[0];
        assert_eq!(a.repo, "znippy");
        let g = &a.worst[0];
        assert_eq!(g.instantiations, 4);
        assert_eq!(g.tax_bytes, 300);
        assert_eq!(g.class, GenericClass::Named);
        assert_eq!(g.example, "znippy::parse_stream::<u8>");
        assert_eq!(a.tax_pct, 30.0);
        assert_eq!(a.actionable_tax(), 300);
        // a generic linked once has no tax; the floor is the request's
        assert!(analyze(&i[..1], &artifacts(), feeds(1), 2, 10).artifacts.is_empty());
        assert!(analyze(&i, &artifacts(), feeds(4), 5, 10).artifacts.is_empty());
    }

    #[test]
    fn glue_and_trait_methods_are_classified_apart_from_named_generics() {
        assert_eq!(classify("core::ptr::drop_in_place"), GenericClass::Glue);
        assert_eq!(classify("::fmt"), GenericClass::TraitMethod);
        assert_eq!(classify("znippy::parse_stream"), GenericClass::Named);
        // a method of a trait impl is not actionable, whatever its path reads
        let mut i: Vec<Instance> = ["u8", "u16"].iter().map(|a| inst("znippy::Codec::encode", a, 50)).collect();
        i.iter_mut().for_each(|x| x.trait_impl = true);
        let r = analyze(&i, &artifacts(), feeds(2), 2, 10);
        assert_eq!(r.artifacts[0].worst[0].class, GenericClass::TraitMethod);
        assert_eq!(r.artifacts[0].actionable_tax(), 0);
    }

    #[test]
    fn a_room_without_linked_instances_reports_the_missing_feed() {
        let r = analyze(&[], &BTreeMap::new(), feeds(0), 2, 10);
        assert!(r.artifacts.is_empty());
        assert!(r.evidence_note.contains("MISSING FEEDS"), "{}", r.evidence_note);
        assert!(r.evidence_note.contains("code_symbols"), "{}", r.evidence_note);
    }
}