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);
}
}