use super::*;
use crate::pass2::Graph;
use crate::pass2::{ATTRIBUTION_TOP_N, AttributionRaw};
const THRESHOLD_PCT: f64 = 10.0;
#[cfg(test)]
pub const DOMINATED_CAP: usize = 50;
const BIG_DROP_RATIO: f64 = 0.7;
const MAX_ACCUM_DEPTH: usize = 1000;
const PACKAGE_THRESHOLD_BP: u32 = 100;
pub(crate) const UNREACHABLE_HISTOGRAM_CAP: usize = 30;
const BIG_DROPS_CAP: usize = 25;
const IMMEDIATE_DOMINATORS_CAP: usize = 30;
const MERGED_PATH_MAX_NODES: usize = 60;
pub fn build_model(
g: &Graph,
dc_offsets: &[u32],
dc_targets: &[u32],
leak_children_cap: usize,
depth_counts: &[u64],
opts: &crate::AnalyzeOptions,
alloc_sites: Option<AllocSites>,
) -> Report {
let generated = now_iso8601();
crate::trace::probe("build_model: before system_overview aggregates");
let overview = build_system_overview(g, depth_counts, opts.top_consumers);
crate::trace::probe("build_model: after system_overview aggregates");
let leaks = build_leak_suspects(
g,
dc_offsets,
dc_targets,
leak_children_cap,
opts.root_path_max_depth,
opts.dominator_tree_max_nodes,
opts.dominator_tree_max_depth,
);
crate::trace::probe("build_model: after leak_suspects aggregates");
let top = build_top_consumers(g, opts.top_consumers);
crate::trace::probe("build_model: after top_consumers aggregates");
let threads = build_thread_overview(g);
crate::trace::probe("build_model: after thread_overview aggregates");
let top_components = build_top_components(&overview);
crate::trace::probe("build_model: after top_components aggregates");
let dominator_analysis = build_dominator_analysis(g, dc_offsets, dc_targets);
crate::trace::probe("build_model: after dominator_analysis aggregates");
let references = build_references(g);
crate::trace::probe("build_model: after references only-weakly-retained rollup");
let collection_attribution = build_collection_attribution(g, &overview);
let fields_by_size = build_fields_by_size(g, &overview);
let biggest_collections = build_biggest_collections(g);
let collection_contents = build_collection_contents(g);
Report {
schema_version: SCHEMA_VERSION,
generated,
overview,
leaks,
top,
threads,
top_components,
alloc_sites,
arrays_by_size: g.arrays_by_size.clone(),
dominator_analysis,
collections: g.collections.clone(),
references,
collection_attribution,
fields_by_size,
biggest_collections,
collection_contents,
leak_indicators: build_leak_indicators(g),
}
}
fn is_anonymous_class(name: &str) -> bool {
if let Some(pos) = name.rfind('$') {
let after = &name[pos + 1..];
if !after.is_empty() && after.chars().all(|c| c.is_ascii_digit()) {
return true;
}
}
name.contains("$$Lambda$") || name.contains("$$Anon") || name.contains("$Proxy")
}
fn build_leak_indicators(g: &Graph) -> LeakIndicators {
let anonymous_class_count = g
.class_names
.iter()
.filter(|n| is_anonymous_class(n))
.count() as u64;
let thread_local_null_key_count = g.thread_local_null_key_count;
let direct_byte_buffer_capacity_sum = g.direct_byte_buffer_capacity_sum;
LeakIndicators {
anonymous_class_count,
thread_local_null_key_count,
direct_byte_buffer_capacity_sum,
}
}
fn class_display(g: &Graph, i: usize) -> String {
let Some(&raw_ci) = g.class_idx.get(i) else {
return String::new();
};
let ci = raw_ci as usize;
if ci < g.class_names.len() {
pretty_class_name(&g.class_names[ci])
} else {
String::new()
}
}
fn build_references(g: &Graph) -> ReferencesAnalysis {
use std::collections::HashMap;
let undef = u32::MAX;
let mut references = g.references.clone();
let mut per_kind: [&mut Option<ReferenceStats>; 3] = [
&mut references.soft,
&mut references.weak,
&mut references.phantom,
];
for (kind, stats) in per_kind.iter_mut().enumerate() {
let Some(stats) = stats.as_mut() else {
continue;
};
let mut by_class: HashMap<String, (u64, u64)> = HashMap::new();
for &ri in &g.reference_referent_idx[kind] {
let i = ri as usize;
if g.idom[i] != undef {
continue; }
let e = by_class.entry(class_display(g, i)).or_insert((0, 0));
e.0 += 1;
e.1 += g.shallow[i] as u64;
}
let mut rows: Vec<RefStatClassRow> = by_class
.into_iter()
.map(|(pretty_class, (objects, shallow))| RefStatClassRow {
pretty_class,
objects,
shallow,
})
.collect();
rows.sort_unstable_by(|a, b| {
b.objects
.cmp(&a.objects)
.then_with(|| a.pretty_class.cmp(&b.pretty_class))
});
stats.only_weakly_retained = rows;
}
references
}
fn kind_label(k: u8) -> &'static str {
match k {
0 => "list",
1 => "map",
2 => "set",
3 => "deque",
4 => "queue",
5 => "tree",
6 => "object array",
7 => "primitive array",
_ => "mixed",
}
}
fn build_collection_attribution(
g: &Graph,
overview: &SystemOverview,
) -> Option<CollectionAttribution> {
use std::collections::HashMap;
let raw = g.collection_attribution_raw.as_ref()?;
let mut holder_counts: HashMap<String, u64> = HashMap::new();
for row in &overview.histogram {
*holder_counts.entry(row.pretty_class.clone()).or_insert(0) += row.instances;
}
Some(aggregate_collection_attribution(
raw,
&g.retained,
g.collection_attribution_truncated,
&holder_counts,
))
}
fn build_fields_by_size(g: &Graph, overview: &SystemOverview) -> Option<FieldsBySize> {
use std::collections::HashMap;
let raw = g.fields_by_size_raw.as_ref()?;
let elems_by_idx: std::collections::HashMap<u32, u64> = g
.coll_values_raw
.as_ref()
.map(|cv| {
cv.iter()
.map(|c| (c.container_idx, c.value_indices.len() as u64))
.collect()
})
.unwrap_or_default();
let mut holder_counts: HashMap<String, u64> = HashMap::new();
for row in &overview.histogram {
*holder_counts.entry(row.pretty_class.clone()).or_insert(0) += row.instances;
}
let mut rows: Vec<FieldBySizeRow> = raw
.iter()
.map(|grp| {
let mut total_retained: u64 = 0;
let mut type_retained: HashMap<String, u64> = HashMap::new();
for &idx in &grp.pointee_indices {
let r = g.retained.get(idx as usize).copied().unwrap_or(0);
total_retained += r;
*type_retained
.entry(class_display(g, idx as usize))
.or_insert(0) += r;
}
let pointee_type = dominant_pointee_type(&type_retained, total_retained);
let holder_instances = holder_counts
.get(&pretty_class_name(&grp.holder_class))
.copied()
.unwrap_or(0);
let elements: u64 = grp
.pointee_indices
.iter()
.filter_map(|idx| elems_by_idx.get(idx).copied())
.sum();
let category = classify_pointee(&pointee_type);
FieldBySizeRow {
holder_class: pretty_class_name(&grp.holder_class),
field: grp.field.clone(),
pointee_type,
total_retained,
pointees: grp.pointee_indices.len() as u64,
holder_instances,
elements,
category,
}
})
.collect();
rows.sort_by(|a, b| {
b.total_retained
.cmp(&a.total_retained)
.then(b.pointees.cmp(&a.pointees))
.then_with(|| a.holder_class.cmp(&b.holder_class))
.then_with(|| a.field.cmp(&b.field))
});
let truncated = rows.len() > ATTRIBUTION_TOP_N;
rows.truncate(ATTRIBUTION_TOP_N);
Some(FieldsBySize { rows, truncated })
}
fn dominant_pointee_type(
type_retained: &std::collections::HashMap<String, u64>,
total_retained: u64,
) -> String {
if type_retained.len() == 1 {
return type_retained.keys().next().cloned().unwrap_or_default();
}
let (best, best_r) = type_retained
.iter()
.max_by_key(|(_, r)| **r)
.map(|(t, r)| (t.clone(), *r))
.unwrap_or_default();
if total_retained > 0 && best_r * 2 > total_retained {
best
} else {
"varies".to_string()
}
}
fn dominant_value_type(counts: &std::collections::HashMap<String, u64>) -> String {
if counts.len() == 1 {
return counts.keys().next().cloned().unwrap_or_default();
}
let total: u64 = counts.values().sum();
let (best, best_c) = counts
.iter()
.max_by_key(|(_, c)| **c)
.map(|(t, c)| (t.clone(), *c))
.unwrap_or_default();
if total > 0 && best_c * 2 > total {
best
} else {
"varies".to_string()
}
}
fn top_value_shares(
counts: &std::collections::HashMap<String, u64>,
k: usize,
) -> Vec<ValueTypeShare> {
let mut v: Vec<ValueTypeShare> = counts
.iter()
.map(|(t, c)| ValueTypeShare {
type_name: t.clone(),
count: *c,
})
.collect();
v.sort_by(|a, b| {
b.count
.cmp(&a.count)
.then_with(|| a.type_name.cmp(&b.type_name))
});
v.truncate(k);
v
}
fn classify_pointee(pointee_type: &str) -> String {
if pointee_type.ends_with("[]") {
"array".to_string()
} else if pointee_type.starts_with("java.util.")
|| pointee_type.contains("Map")
|| pointee_type.contains("List")
|| pointee_type.contains("Set")
|| pointee_type.contains("Collection")
|| pointee_type.contains("scala.collection")
{
"collection".to_string()
} else {
"object".to_string()
}
}
fn build_biggest_collections(g: &Graph) -> Option<BiggestCollections> {
let raw = g.coll_values_raw.as_ref()?;
const TOP_N: usize = ATTRIBUTION_TOP_N;
const TOP_K_TYPES: usize = 4;
let mut rows: Vec<BiggestCollectionRow> = raw
.iter()
.map(|c| {
let mut counts: std::collections::HashMap<String, u64> =
std::collections::HashMap::new();
for &vi in &c.value_indices {
*counts.entry(class_display(g, vi as usize)).or_insert(0) += 1;
}
let retained = g
.retained
.get(c.container_idx as usize)
.copied()
.unwrap_or(0);
BiggestCollectionRow {
kind: kind_label(c.kind).to_string(),
container_class: c.container_class.clone(),
elements: c.value_indices.len() as u64,
retained: Some(retained),
owner: c.owner.clone(),
dominant_value_type: Some(dominant_value_type(&counts)),
value_type_breakdown: top_value_shares(&counts, TOP_K_TYPES),
}
})
.collect();
rows.sort_by(|a, b| {
b.retained
.unwrap_or(0)
.cmp(&a.retained.unwrap_or(0))
.then(b.elements.cmp(&a.elements))
.then_with(|| a.container_class.cmp(&b.container_class))
});
let truncated = rows.len() > TOP_N;
let combined: Vec<BiggestCollectionRow> = rows.iter().take(TOP_N).cloned().collect();
const KINDS: [&str; 6] = ["list", "map", "set", "deque", "queue", "tree"];
let mut by_kind: Vec<CollectionKindTable> = Vec::new();
for kind in KINDS {
let mut krows: Vec<BiggestCollectionRow> =
rows.iter().filter(|r| r.kind == kind).cloned().collect();
if krows.is_empty() {
continue;
}
krows.truncate(TOP_N);
by_kind.push(CollectionKindTable {
kind: kind.to_string(),
rows: krows,
});
}
Some(BiggestCollections {
combined,
by_kind,
truncated,
})
}
fn build_collection_contents(g: &Graph) -> Option<CollectionContents> {
use std::collections::HashMap;
let raw = g.coll_values_raw.as_ref()?;
const TOP_N: usize = ATTRIBUTION_TOP_N;
const TOP_K_TYPES: usize = 5;
struct Acc {
instances: u64,
total_values: u64,
type_counts: HashMap<String, u64>,
}
let mut by_class: HashMap<String, Acc> = HashMap::new();
for c in raw {
let acc = by_class.entry(c.container_class.clone()).or_insert(Acc {
instances: 0,
total_values: 0,
type_counts: HashMap::new(),
});
acc.instances += 1;
acc.total_values += c.value_indices.len() as u64;
for &vi in &c.value_indices {
*acc.type_counts
.entry(class_display(g, vi as usize))
.or_insert(0) += 1;
}
}
let mut rows: Vec<CollectionContentsRow> = by_class
.into_iter()
.map(|(collection_class, acc)| CollectionContentsRow {
collection_class,
instances: acc.instances,
total_values: acc.total_values,
top_value_types: top_value_shares(&acc.type_counts, TOP_K_TYPES),
})
.collect();
rows.sort_by(|a, b| {
b.total_values
.cmp(&a.total_values)
.then_with(|| a.collection_class.cmp(&b.collection_class))
});
let truncated = rows.len() > TOP_N;
rows.truncate(TOP_N);
Some(CollectionContents { rows, truncated })
}
fn aggregate_collection_attribution(
raw: &[AttributionRaw],
retained: &[u64],
truncated: bool,
holder_counts: &std::collections::HashMap<String, u64>,
) -> CollectionAttribution {
use std::collections::HashMap;
struct OverallAcc {
total_elements: u64,
total_retained: u64,
seen: std::collections::HashSet<u32>,
first_kind: u8,
mixed: bool,
}
struct BiggestAcc {
elements: u64,
retained: u64,
container_class: String,
capacity: u64,
}
let mut overall: HashMap<(String, String), OverallAcc> = HashMap::new();
let mut biggest: HashMap<(String, String), BiggestAcc> = HashMap::new();
for rec in raw {
let retained_bytes = retained
.get(rec.container_idx as usize)
.copied()
.unwrap_or(0);
let key = (rec.holder_class.clone(), rec.field.clone());
let acc = overall.entry(key.clone()).or_insert_with(|| OverallAcc {
total_elements: 0,
total_retained: 0,
seen: std::collections::HashSet::new(),
first_kind: rec.container_kind,
mixed: false,
});
if acc.seen.insert(rec.container_idx) {
acc.total_elements += rec.elements;
acc.total_retained += retained_bytes;
if rec.container_kind != acc.first_kind {
acc.mixed = true;
}
}
let b = biggest.entry(key).or_insert_with(|| BiggestAcc {
elements: 0,
retained: 0,
container_class: String::new(),
capacity: 0,
});
if rec.elements > b.elements || (rec.elements == b.elements && retained_bytes > b.retained)
{
b.elements = rec.elements;
b.retained = retained_bytes;
b.container_class = crate::report::pretty_class_name(&rec.container_class);
b.capacity = rec.capacity;
}
}
let mut most_overall: Vec<FieldAttributionRow> = overall
.into_iter()
.map(|((holder_class, field), acc)| FieldAttributionRow {
container_kind: if acc.mixed {
"mixed".to_string()
} else {
kind_label(acc.first_kind).to_string()
},
total_elements: acc.total_elements,
total_retained: acc.total_retained,
container_count: acc.seen.len() as u64,
holder_instances: holder_counts
.get(&crate::report::pretty_class_name(&holder_class))
.copied()
.unwrap_or(0),
holder_class,
field,
})
.collect();
most_overall.sort_by(|a, b| {
b.total_elements
.cmp(&a.total_elements)
.then(b.total_retained.cmp(&a.total_retained))
.then_with(|| a.holder_class.cmp(&b.holder_class))
.then_with(|| a.field.cmp(&b.field))
});
most_overall.truncate(ATTRIBUTION_TOP_N);
let mut biggest_single: Vec<FieldAttributionBiggestRow> = biggest
.into_iter()
.map(|((holder_class, field), b)| FieldAttributionBiggestRow {
holder_class,
field,
container_class: b.container_class,
elements: b.elements,
retained: b.retained,
capacity: b.capacity,
})
.collect();
biggest_single.sort_by(|a, b| {
b.elements
.cmp(&a.elements)
.then(b.retained.cmp(&a.retained))
.then_with(|| a.holder_class.cmp(&b.holder_class))
.then_with(|| a.field.cmp(&b.field))
});
biggest_single.truncate(ATTRIBUTION_TOP_N);
CollectionAttribution {
most_overall,
biggest_single,
truncated,
}
}
const TOP_COMPONENTS: usize = 10;
const COMPONENT_TOP_CLASSES: usize = 5;
fn build_top_components(overview: &SystemOverview) -> TopComponents {
use std::collections::HashMap;
let total_retained: u64 = overview.histogram.iter().map(|r| r.retained).sum();
struct Acc {
label: String,
retained: u64,
classes: Vec<ComponentClass>,
}
let mut by_loader: HashMap<u64, Acc> = HashMap::new();
for row in &overview.histogram {
let label = row
.loader_label
.clone()
.unwrap_or_else(|| format!("loader @ {:#x}", row.loader_id));
let acc = by_loader.entry(row.loader_id).or_insert_with(|| Acc {
label,
retained: 0,
classes: Vec::new(),
});
acc.retained += row.retained;
acc.classes.push(ComponentClass {
pretty_class: row.pretty_class.clone(),
retained: row.retained,
});
}
let mut components: Vec<Component> = by_loader
.into_values()
.map(|mut acc| {
acc.classes.sort_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(a.pretty_class.cmp(&b.pretty_class))
});
acc.classes.truncate(COMPONENT_TOP_CLASSES);
let pct = if total_retained > 0 {
acc.retained as f64 / total_retained as f64 * 100.0
} else {
0.0
};
Component {
loader_label: acc.label,
retained: acc.retained,
pct,
top_classes: acc.classes,
}
})
.collect();
components.sort_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(a.loader_label.cmp(&b.loader_label))
.then_with(|| {
let ak = a.top_classes.first().map(|c| c.pretty_class.as_str());
let bk = b.top_classes.first().map(|c| c.pretty_class.as_str());
ak.cmp(&bk)
})
});
components.truncate(TOP_COMPONENTS);
TopComponents { components }
}
fn build_dominator_analysis(
g: &Graph,
dc_offsets: &[u32],
dc_targets: &[u32],
) -> DominatorAnalysis {
let n = g.n;
let undef = u32::MAX;
let class_count = g.class_names.len();
let dom_children = |node: usize| -> &[u32] {
&dc_targets[dc_offsets[node] as usize..dc_offsets[node + 1] as usize]
};
let display_of = |i: usize| -> String {
let ci = g.class_idx[i] as usize;
if ci < class_count {
pretty_class_name(&g.class_names[ci])
} else {
String::new()
}
};
let total_shallow: u64 = (0..n)
.filter(|&i| g.idom[i] != undef)
.map(|i| g.shallow[i] as u64)
.sum();
const DROP_THRESHOLD_PCT: f64 = 1.0;
let threshold = (total_shallow as f64 * DROP_THRESHOLD_PCT / 100.0) as u64;
let mut drops: Vec<BigDropRow> = Vec::new();
for i in 0..n {
if g.idom[i] == undef {
continue;
}
if g.retained[i] < threshold {
continue;
}
let kids = dom_children(i);
let child_count = kids.len() as u64;
let (largest_child_retained, largest_child_idx) = kids
.iter()
.map(|&c| (g.retained[c as usize], c))
.max_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)))
.unwrap_or((0, u32::MAX));
let drop_bytes = g.retained[i].saturating_sub(largest_child_retained);
if drop_bytes == 0 {
continue;
}
drops.push(BigDropRow {
obj_index_1based: (i as u64) + 1,
display_class: display_of(i),
retained: g.retained[i],
child_count,
largest_child_retained,
largest_child_class: if largest_child_idx != u32::MAX {
display_of(largest_child_idx as usize)
} else {
String::new()
},
drop_bytes,
});
}
drops.sort_unstable_by(|a, b| {
b.drop_bytes
.cmp(&a.drop_bytes)
.then(a.obj_index_1based.cmp(&b.obj_index_1based))
});
drops.truncate(BIG_DROPS_CAP);
let big_drops = BigDrops {
threshold,
rows: drops,
};
let remap = class_row_remap(g);
let mut dom_count = vec![0u64; class_count]; let mut domd_count = vec![0u64; class_count]; let mut dom_shallow = vec![0u64; class_count];
let mut domd_shallow = vec![0u64; class_count];
for p in 0..n {
if g.idom[p] == undef {
continue;
}
let kids = dom_children(p);
if kids.is_empty() {
continue;
}
let pci = g.class_idx[p] as usize;
if pci >= class_count {
continue;
}
let pci = remap[pci] as usize;
dom_count[pci] += 1;
dom_shallow[pci] += g.shallow[p] as u64;
for &c in kids {
domd_count[pci] += 1;
domd_shallow[pci] += g.shallow[c as usize] as u64;
}
}
let mut order: Vec<usize> = (0..class_count)
.filter(|&ci| remap[ci] as usize == ci && dom_count[ci] > 0)
.collect();
order.sort_unstable_by(|&a, &b| {
domd_shallow[b]
.cmp(&domd_shallow[a])
.then(domd_count[b].cmp(&domd_count[a]))
.then(a.cmp(&b))
});
order.truncate(IMMEDIATE_DOMINATORS_CAP);
let rows: Vec<ImmediateDominatorRow> = order
.into_iter()
.map(|ci| ImmediateDominatorRow {
dominator_class: pretty_class_name(&g.class_names[ci]),
dominator_count: dom_count[ci],
dominated_count: domd_count[ci],
dominator_shallow: dom_shallow[ci],
dominated_shallow: domd_shallow[ci],
})
.collect();
let immediate_dominators = ImmediateDominators { rows };
DominatorAnalysis {
big_drops,
immediate_dominators,
}
}
fn thread_state_label(status: i32) -> String {
const ALIVE: i32 = 0x0001;
const TERMINATED: i32 = 0x0002;
const RUNNABLE: i32 = 0x0004;
const BLOCKED_ON_MONITOR: i32 = 0x0400;
const WAITING: i32 = 0x0080;
const WAITING_INDEFINITELY: i32 = 0x0010;
const WAITING_WITH_TIMEOUT: i32 = 0x0020;
const SLEEPING: i32 = 0x0040;
const IN_OBJECT_WAIT: i32 = 0x0100;
const PARKED: i32 = 0x0200;
let mut parts: Vec<&str> = Vec::new();
if status & ALIVE != 0 {
parts.push("alive");
}
if status & TERMINATED != 0 {
parts.push("terminated");
}
if status & RUNNABLE != 0 {
parts.push("runnable");
}
if status & BLOCKED_ON_MONITOR != 0 {
parts.push("blocked on monitor");
}
if status & WAITING != 0 {
parts.push("waiting");
}
if status & WAITING_INDEFINITELY != 0 {
parts.push("waiting indefinitely");
}
if status & WAITING_WITH_TIMEOUT != 0 {
parts.push("waiting with timeout");
}
if status & SLEEPING != 0 {
parts.push("sleeping");
}
if status & IN_OBJECT_WAIT != 0 {
parts.push("in Object.wait");
}
if status & PARKED != 0 {
parts.push("parked");
}
if parts.is_empty() {
String::new()
} else {
format!("[{}]", parts.join(", "))
}
}
pub(crate) fn build_alloc_sites(g: &Graph, top_n: usize) -> AllocSites {
build_alloc_sites_from(g, top_n, g.alloc_stack_serial.iter().copied())
}
pub(crate) struct AllocAgg<'g> {
g: &'g Graph,
top_n: usize,
idx: usize,
agg: std::collections::HashMap<u32, (u64, u64, u64)>,
}
impl<'g> AllocAgg<'g> {
pub(crate) fn new(g: &'g Graph, top_n: usize) -> Self {
Self {
g,
top_n,
idx: 0,
agg: std::collections::HashMap::new(),
}
}
pub(crate) fn push(&mut self, serial: u32) {
let i = self.idx;
self.idx += 1;
if serial == 0 {
return;
}
if i >= self.g.shallow.len() {
return;
}
let e = self.agg.entry(serial).or_insert((0, 0, 0));
e.0 += 1;
e.1 += self.g.shallow[i] as u64;
e.2 += self.g.retained[i];
}
pub(crate) fn finish(self) -> AllocSites {
if self.agg.is_empty() {
return AllocSites {
traces_present: false,
sites: vec![],
};
}
let empty_frames: Vec<String> = Vec::new();
let mut sites: Vec<AllocSite> = self
.agg
.into_iter()
.map(
|(stack_serial, (object_count, shallow_total, retained_total))| {
let frames = self
.g
.alloc_frames_by_serial
.as_ref()
.and_then(|m| m.get(&stack_serial))
.cloned()
.unwrap_or_else(|| empty_frames.clone());
AllocSite {
stack_serial,
frames,
object_count,
shallow_total,
retained_total,
}
},
)
.collect();
sites.sort_by(|a, b| {
b.object_count
.cmp(&a.object_count)
.then_with(|| b.retained_total.cmp(&a.retained_total))
.then_with(|| a.stack_serial.cmp(&b.stack_serial))
});
sites.truncate(self.top_n);
AllocSites {
traces_present: true,
sites,
}
}
}
pub(crate) fn build_alloc_sites_from<I: Iterator<Item = u32>>(
g: &Graph,
top_n: usize,
serials: I,
) -> AllocSites {
let mut agg = AllocAgg::new(g, top_n);
for serial in serials {
agg.push(serial);
}
agg.finish()
}
pub(crate) fn build_thread_overview(g: &Graph) -> ThreadOverview {
let threads = g
.thread_stacks
.iter()
.map(|t| {
let class_name = if t.thread_obj_idx == u32::MAX {
None
} else {
g.class_idx
.get(t.thread_obj_idx as usize)
.and_then(|&ci| g.class_names.get(ci as usize))
.cloned()
};
let local_objects = Some(
g.thread_local_samples
.get(&t.thread_serial)
.map(|idxs| {
let mut objs: Vec<ThreadLocalObj> = idxs
.iter()
.map(|&li| {
let display_class = g
.class_idx
.get(li as usize)
.and_then(|&ci| g.class_names.get(ci as usize))
.cloned()
.unwrap_or_else(|| "<unknown>".to_string());
ThreadLocalObj {
obj_index_1based: li as usize + 1,
display_class,
shallow: g.shallow[li as usize] as u64,
retained: g.retained[li as usize],
}
})
.collect();
objs.sort_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(a.obj_index_1based.cmp(&b.obj_index_1based))
});
objs
})
.unwrap_or_default(),
);
let (shallow, retained) = if t.thread_obj_idx == u32::MAX {
(0, 0)
} else {
let idx = t.thread_obj_idx as usize;
(
g.shallow.get(idx).copied().unwrap_or(0) as u64,
g.retained.get(idx).copied().unwrap_or(0),
)
};
let props = g.thread_props.get(&t.thread_serial);
let is_daemon = props.map(|p| p.is_daemon).unwrap_or(false);
let priority = props.map(|p| p.priority).unwrap_or(0);
let thread_state = props
.map(|p| thread_state_label(p.thread_status))
.unwrap_or_default();
let context_class_loader = props
.map(|p| p.context_loader_addr)
.filter(|&a| a != 0)
.map(|addr| loader_label_for_addr(g, addr));
let (significant_frames, max_local_retained) = build_significant_frames(g, t, retained);
ThreadInfo {
thread_serial: t.thread_serial,
name: g
.thread_props
.get(&t.thread_serial)
.map(|p| p.name.clone())
.filter(|s| !s.is_empty()),
class_name,
frames: t.frames.clone(),
local_root_count: g
.thread_local_counts
.get(&t.thread_serial)
.copied()
.unwrap_or(0),
local_objects,
shallow,
retained,
max_local_retained,
context_class_loader,
is_daemon,
priority,
thread_state,
significant_frames,
}
})
.collect();
ThreadOverview { threads }
}
fn loader_label_for_addr(g: &Graph, addr: u64) -> String {
if let Some(label) = g.loader_labels.get(&addr) {
return format!("{label} @ {addr:#x}");
}
format!("@ {addr:#x}")
}
fn build_significant_frames(
g: &Graph,
t: &crate::pass2::ThreadStack,
thread_retained: u64,
) -> (Vec<SignificantFrame>, u64) {
use std::collections::BTreeMap;
let Some(pairs) = g.thread_local_frame_samples.get(&t.thread_serial) else {
return (Vec::new(), 0);
};
if pairs.is_empty() {
return (Vec::new(), 0);
}
let mut by_frame: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
for &(frame_number, local_idx) in pairs {
by_frame.entry(frame_number).or_default().push(local_idx);
}
let mut max_local_retained: u64 = 0;
let mut frames_out: Vec<SignificantFrame> = Vec::new();
for (&frame_number, locals) in &by_frame {
let frame = if frame_number == u32::MAX {
"<no frame> (JNI local / native stack)".to_string()
} else {
t.frames
.get(frame_number as usize)
.cloned()
.unwrap_or_else(|| format!("<frame #{frame_number}>"))
};
let mut locals_out: Vec<SignificantLocal> = locals
.iter()
.map(|&li| {
let display_class = g
.class_idx
.get(li as usize)
.and_then(|&ci| g.class_names.get(ci as usize))
.cloned()
.unwrap_or_else(|| "<unknown>".to_string());
let retained = g.retained.get(li as usize).copied().unwrap_or(0);
max_local_retained = max_local_retained.max(retained);
let pct = if thread_retained > 0 {
retained as f64 / thread_retained as f64 * 100.0
} else {
0.0
};
SignificantLocal {
display_class: pretty_class_name(&display_class),
retained,
pct,
}
})
.collect();
locals_out.sort_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(a.display_class.cmp(&b.display_class))
});
frames_out.push(SignificantFrame {
frame,
locals: locals_out,
});
}
(frames_out, max_local_retained)
}
fn compute_fragmentation_ratio(total_shallow: u64, unreachable_shallow: u64) -> f64 {
let denom = total_shallow + unreachable_shallow;
if denom == 0 {
0.0
} else {
unreachable_shallow as f64 / denom as f64
}
}
fn compute_top_class_concentration_bp(
histogram: &[crate::report::HistRow],
total_retained: u64,
) -> u32 {
if total_retained == 0 {
return 0;
}
histogram
.first()
.map(|r| ((r.retained.saturating_mul(10_000)) / total_retained).min(10_000) as u32)
.unwrap_or(0)
}
fn build_system_overview(g: &Graph, depth_counts: &[u64], top_n: usize) -> SystemOverview {
let n = g.n;
let class_count = g.class_names.len();
let remap = class_row_remap(g);
let mut total_objects: u64 = 0;
let mut total_shallow: u64 = 0;
let mut unreachable_count: u64 = 0;
let mut unreachable_shallow: u64 = 0;
let mut unreach_count: Vec<u64> = vec![0; class_count];
let mut unreach_shallow: Vec<u64> = vec![0; class_count];
const KIND_ORDER: [&str; 4] = [
"Instances",
"Object arrays",
"Primitive arrays",
"Class objects",
];
let kind_idx = |k: &str| KIND_ORDER.iter().position(|&x| x == k).unwrap();
let mut comp_objs = [0u64; 4];
let mut comp_sh = [0u64; 4];
let vroot_u32 = n as u32;
let undef_u32 = u32::MAX;
let mut tops: Vec<u64> = Vec::new();
let mut classes_loaded: u64 = 0;
let mut loader_set: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut inst_count: Vec<u64> = vec![0; class_count];
let mut shallow_total: Vec<u64> = vec![0; class_count];
let mut class_retained: Vec<u64> = vec![0; class_count];
let mut max_shallow: Vec<u64> = vec![0; class_count];
for i in 0..n {
let id = g.idom[i];
let sh = g.shallow[i] as u64;
let ci_raw = g.class_idx[i] as usize;
if id != undef_u32 {
total_objects += 1;
total_shallow += sh;
let b = kind_idx(object_kind(g, i));
comp_objs[b] += 1;
comp_sh[b] += sh;
if id == vroot_u32 {
tops.push(g.retained[i]);
}
if ci_raw < class_count {
let ci = remap[ci_raw] as usize;
inst_count[ci] += 1;
shallow_total[ci] += sh;
if sh > max_shallow[ci] {
max_shallow[ci] = sh;
}
if !g.has_same_class_ancestor.get(i) {
class_retained[ci] += g.retained[i];
}
}
let repr = class_obj_repr(g, i);
if repr != undef_u32 {
if (repr as usize) < class_count {
let ci = remap[repr as usize] as usize;
class_retained[ci] += g.retained[i];
}
classes_loaded += 1;
let lid = g
.class_obj_class_idx
.get(&(i as u32))
.and_then(|&row| g.class_loader_id.get(row as usize).copied())
.unwrap_or(0);
loader_set.insert(lid);
}
} else {
unreachable_count += 1;
unreachable_shallow += sh;
if ci_raw < class_count {
let ci = remap[ci_raw] as usize;
unreach_count[ci] += 1;
unreach_shallow[ci] += sh;
}
}
}
let classloaders_loaded = loader_set.len() as u64;
if let Some(sz) = g.system_classloader_shallow {
total_objects += 1;
total_shallow += sz as u64;
}
let gc_roots = (g
.gc_root_indices
.len()
.saturating_sub(g.synthetic_root_count)) as u64;
let gc_roots_by_type = {
let mut counts: std::collections::HashMap<&'static str, u64> =
std::collections::HashMap::new();
for &ty in &g.gc_root_types {
*counts.entry(gc_root_type_label(ty)).or_insert(0) += 1;
}
if g.synthetic_root_count > 0 {
let sys = gc_root_type_label(crate::types::heap::ROOT_SYSTEM_CLASS);
if let Some(c) = counts.get_mut(sys) {
*c = c.saturating_sub(g.synthetic_root_count as u64);
if *c == 0 {
counts.remove(sys);
}
}
}
let mut rows: Vec<GcRootTypeRow> = counts
.into_iter()
.map(|(root_type, count)| GcRootTypeRow {
root_type: root_type.to_string(),
count,
})
.collect();
rows.sort_by(|a, b| {
b.count
.cmp(&a.count)
.then_with(|| a.root_type.cmp(&b.root_type))
});
rows
};
let heap_composition = {
let mut objs = comp_objs;
let mut sh = comp_sh;
if let Some(sz) = g.system_classloader_shallow {
let b = kind_idx("Instances");
objs[b] += 1;
sh[b] += sz as u64;
}
let by_kind = KIND_ORDER
.iter()
.enumerate()
.filter(|&(b, _)| objs[b] > 0)
.map(|(b, &k)| KindStat {
kind: k.to_string(),
objects: objs[b],
shallow_heap: sh[b],
})
.collect();
HeapComposition { by_kind }
};
let dominator_depth_histogram: Vec<DepthBucket> = depth_counts
.iter()
.enumerate()
.filter(|&(_, &objects)| objects > 0)
.map(|(i, &objects)| DepthBucket {
depth: (i + 1) as u32,
objects,
})
.collect();
let retention_concentration = {
tops.sort_unstable_by(|a, b| b.cmp(a)); let denom = total_shallow.max(1);
let bp = |sum: u64| -> u32 { ((sum as u128 * 10_000) / denom as u128) as u32 };
let prefix = |k: usize| -> u64 { tops.iter().take(k).sum() };
let total_retained: u64 = tops.iter().sum();
let one_pct = denom / 100;
let num_objects_ge_1pct = tops.iter().filter(|&&r| r >= one_pct).count() as u64;
RetentionSummary {
total_retained,
top1_bp: bp(prefix(1)),
top10_bp: bp(prefix(10)),
top100_bp: bp(prefix(100)),
num_objects_ge_1pct,
}
};
if let Some(sz) = g.system_classloader_shallow {
for ci in 0..class_count {
if remap[ci] as usize == ci
&& pretty_class_name(&g.class_names[ci]) == "java.lang.ClassLoader"
{
inst_count[ci] += 1;
shallow_total[ci] += sz as u64;
break;
}
}
}
let mut order: Vec<usize> = (0..class_count)
.filter(|&ci| remap[ci] as usize == ci)
.collect();
order.sort_unstable_by(|&a, &b| class_retained[b].cmp(&class_retained[a]).then(a.cmp(&b)));
let histogram: Vec<HistRow> = order
.into_iter()
.map(|ci| HistRow {
pretty_class: pretty_class_name(&g.class_names[ci]),
instances: inst_count[ci],
shallow: shallow_total[ci],
retained: class_retained[ci],
max_instance_shallow: max_shallow[ci],
loader_id: g.class_loader_id.get(ci).copied().unwrap_or(0),
loader_label: {
let lid = g.class_loader_id.get(ci).copied().unwrap_or(0);
if lid == 0 {
Some("<boot>".to_string())
} else {
g.loader_labels.get(&lid).cloned()
}
},
})
.collect();
let unreachable_histogram: Vec<UnreachableClassRow> = {
let mut order: Vec<usize> = (0..class_count)
.filter(|&ci| remap[ci] as usize == ci && unreach_count[ci] > 0)
.collect();
order.sort_unstable_by(|&a, &b| {
unreach_shallow[b]
.cmp(&unreach_shallow[a])
.then(unreach_count[b].cmp(&unreach_count[a]))
.then(a.cmp(&b))
});
order.truncate(UNREACHABLE_HISTOGRAM_CAP);
order
.into_iter()
.map(|ci| UnreachableClassRow {
pretty_class: pretty_class_name(&g.class_names[ci]),
objects: unreach_count[ci],
shallow: unreach_shallow[ci],
})
.collect()
};
let (loader_rollup, duplicate_classes) = {
use std::collections::HashMap;
const LOADER_CAP: usize = 8;
let mut roll: HashMap<u64, LoaderRollup> = HashMap::new();
struct DupAcc {
loader_ids: std::collections::HashSet<u64>,
loaders: Vec<String>,
total_instances: u64,
total_retained: u64,
per_loader: HashMap<u64, (String, u64, u64, u64)>,
}
let mut dup: HashMap<String, DupAcc> = HashMap::new();
for row in &histogram {
let e = roll.entry(row.loader_id).or_insert_with(|| LoaderRollup {
loader_label: row.loader_label.clone(),
loader_id: row.loader_id,
class_count: 0,
instances: 0,
shallow: 0,
retained: 0,
});
e.class_count += 1;
e.instances += row.instances;
e.shallow += row.shallow;
e.retained += row.retained;
let d = dup
.entry(row.pretty_class.clone())
.or_insert_with(|| DupAcc {
loader_ids: std::collections::HashSet::new(),
loaders: Vec::new(),
total_instances: 0,
total_retained: 0,
per_loader: HashMap::new(),
});
let label = row
.loader_label
.clone()
.unwrap_or_else(|| format!("loader@{:#x}", row.loader_id));
if d.loader_ids.insert(row.loader_id) && d.loaders.len() < LOADER_CAP {
d.loaders.push(label.clone());
}
d.total_instances += row.instances;
d.total_retained += row.retained;
if d.per_loader.contains_key(&row.loader_id) || d.per_loader.len() < LOADER_CAP {
let e = d
.per_loader
.entry(row.loader_id)
.or_insert((label, 0, 0, 0));
e.1 += row.instances;
e.2 += row.shallow;
e.3 += row.retained;
}
}
let mut rollup: Vec<LoaderRollup> = roll.into_values().collect();
rollup.sort_unstable_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(a.loader_id.cmp(&b.loader_id))
});
rollup.truncate(top_n);
let mut dups: Vec<DuplicateClass> = dup
.into_iter()
.filter(|(_, d)| d.loader_ids.len() > 1)
.map(|(pretty_class, d)| {
let DupAcc {
loader_ids,
loaders,
total_instances,
total_retained,
per_loader,
} = d;
let mut per_loader: Vec<DuplicateClassLoaderRow> = per_loader
.into_iter()
.map(
|(loader_id, (loader_label, instances, shallow, retained))| {
DuplicateClassLoaderRow {
loader_label,
loader_id,
instances,
shallow,
retained,
}
},
)
.collect();
per_loader.sort_unstable_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(b.instances.cmp(&a.instances))
.then(a.loader_id.cmp(&b.loader_id))
});
DuplicateClass {
pretty_class,
loader_count: loader_ids.len() as u64,
loaders,
total_instances,
total_retained,
per_loader,
}
})
.collect();
dups.sort_unstable_by(|a, b| {
b.total_retained
.cmp(&a.total_retained)
.then_with(|| a.pretty_class.cmp(&b.pretty_class))
});
dups.truncate(top_n);
(rollup, dups)
};
let heap_fragmentation_ratio = compute_fragmentation_ratio(total_shallow, unreachable_shallow);
let top_class_concentration_bp =
compute_top_class_concentration_bp(&histogram, retention_concentration.total_retained);
let gc_roots_retained_by_type: Vec<crate::report::GcRootRetainedRow> = {
use std::collections::HashMap;
let mut by_type: HashMap<String, (u64, u64)> = HashMap::new();
for (&idx, &ty) in g.gc_root_indices.iter().zip(g.gc_root_types.iter()) {
if let Some(label) = gc_root_type_label_opt(ty) {
let retained = g.retained.get(idx as usize).copied().unwrap_or(0);
let e = by_type.entry(label.to_string()).or_insert((0, 0));
e.0 += 1;
e.1 = e.1.saturating_add(retained);
}
}
let mut rows: Vec<crate::report::GcRootRetainedRow> = by_type
.into_iter()
.map(
|(root_type, (count, retained))| crate::report::GcRootRetainedRow {
root_type,
count,
retained,
},
)
.collect();
rows.sort_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(a.root_type.cmp(&b.root_type))
});
rows
};
let compressed_oops = Some(g.ref_size < g.id_size);
let dump_creation = if g.header_timestamp_ms != 0 {
Some(g.header_timestamp_ms as i64)
} else {
None
};
SystemOverview {
source_name: g.source_name.clone(),
file_path: g.file_path.clone(),
format: g.format.clone(),
file_size: g.file_size,
identifier_size_bits: g.id_size as u32 * 8,
compressed_oops,
dump_creation,
total_objects,
total_shallow,
gc_roots,
gc_roots_by_type,
heap_composition,
dominator_depth_histogram,
retention_concentration,
classes_loaded,
classloaders_loaded,
unreachable_count,
unreachable_shallow,
unreachable_histogram,
histogram,
histogram_truncated_to: None,
system_properties: g
.system_properties
.iter()
.map(|(k, v)| PropEntry {
key: k.clone(),
value: v.clone(),
})
.collect(),
jvm_version: g.jvm_version.clone(),
loader_rollup,
duplicate_classes,
record_census: g.record_census.clone(),
duplicate_strings: g.dup_strings.clone(),
heap_fragmentation_ratio,
top_class_concentration_bp,
gc_roots_retained_by_type,
}
}
fn build_dom_subtree(
root: usize,
dc_offsets: &[u32],
dc_targets: &[u32],
display_of: &dyn Fn(usize) -> String,
g: &Graph,
max_nodes: usize,
max_depth: usize,
) -> DomTreeNode {
struct Frame {
depth: usize,
node: DomTreeNode,
pending: Vec<u32>,
child_pos: usize,
}
let sorted_children = |idx: usize| -> Vec<u32> {
let mut kids: Vec<u32> =
dc_targets[dc_offsets[idx] as usize..dc_offsets[idx + 1] as usize].to_vec();
kids.sort_unstable_by(|&a, &b| {
g.retained[b as usize]
.cmp(&g.retained[a as usize])
.then(a.cmp(&b))
});
kids
};
let make_node = |idx: usize| DomTreeNode {
obj_index_1based: idx + 1,
display_class: display_of(idx),
shallow: g.shallow[idx] as u64,
retained: g.retained[idx],
children: Vec::new(),
};
let mut emitted: usize = 1;
let root_pending = if max_depth == 0 {
Vec::new()
} else {
sorted_children(root)
};
let mut stack: Vec<Frame> = vec![Frame {
depth: 0,
node: make_node(root),
pending: root_pending,
child_pos: 0,
}];
loop {
let top = stack.last_mut().expect("stack never empties before break");
let can_descend = top.depth < max_depth;
if can_descend && top.child_pos < top.pending.len() && emitted < max_nodes {
let child = top.pending[top.child_pos] as usize;
top.child_pos += 1;
emitted += 1;
let depth = top.depth + 1;
let pending = if depth < max_depth {
sorted_children(child)
} else {
Vec::new()
};
stack.push(Frame {
depth,
node: make_node(child),
pending,
child_pos: 0,
});
} else {
let done = stack.pop().expect("frame present").node;
match stack.last_mut() {
Some(parent) => parent.node.children.push(done),
None => return done,
}
}
}
}
pub(crate) fn build_leak_suspects(
g: &Graph,
dc_offsets: &[u32],
dc_targets: &[u32],
cap: usize,
root_path_max_depth: usize,
dom_max_nodes: usize,
dom_max_depth: usize,
) -> LeakSuspects {
let n = g.n;
let undef = u32::MAX;
let mut total_shallow: u64 = (0..n)
.filter(|&i| g.idom[i] != undef)
.map(|i| g.shallow[i] as u64)
.sum();
if let Some(sz) = g.system_classloader_shallow {
total_shallow += sz as u64;
}
let threshold = (total_shallow as f64 * THRESHOLD_PCT / 100.0) as u64;
let dom_children = |node: usize| -> &[u32] {
&dc_targets[dc_offsets[node] as usize..dc_offsets[node + 1] as usize]
};
struct RawSuspect {
is_single: bool,
obj_idx: u32, class_idx: usize,
instance_count: u64,
retained: u64,
shallow: u64,
}
let mut suspects: Vec<RawSuspect> = Vec::new();
let mut single_class_set: std::collections::HashSet<usize> = std::collections::HashSet::new();
for &i in dom_children(n) {
let idx = i as usize;
if g.retained[idx] >= threshold {
let ci = g.class_idx[idx] as usize;
single_class_set.insert(ci);
suspects.push(RawSuspect {
is_single: true,
obj_idx: i,
class_idx: ci,
instance_count: 1,
retained: g.retained[idx],
shallow: g.shallow[idx] as u64,
});
}
}
let class_count = g.class_names.len();
let mut group_retained: Vec<u64> = vec![0; class_count];
let mut group_count: Vec<u64> = vec![0; class_count];
let mut group_shallow: Vec<u64> = vec![0; class_count];
for &i in dom_children(n) {
let idx = i as usize;
let ci = g.class_idx[idx] as usize;
if ci < class_count {
group_retained[ci] += g.retained[idx];
group_count[ci] += 1;
group_shallow[ci] += g.shallow[idx] as u64;
}
}
for ci in 0..class_count {
if group_retained[ci] >= threshold && !single_class_set.contains(&ci) {
suspects.push(RawSuspect {
is_single: false,
obj_idx: u32::MAX,
class_idx: ci,
instance_count: group_count[ci],
retained: group_retained[ci],
shallow: group_shallow[ci],
});
}
}
suspects.sort_unstable_by(|a, b| {
b.retained
.cmp(&a.retained)
.then(a.class_idx.cmp(&b.class_idx))
.then(a.obj_idx.cmp(&b.obj_idx))
});
let display_of = |idx: usize| -> String {
let ci = g.class_idx[idx] as usize;
if class_obj_repr(g, idx) != u32::MAX {
let repr = class_obj_repr(g, idx) as usize;
if repr < g.class_names.len() {
return pretty_class_name(&g.class_names[repr]);
}
}
if ci < g.class_names.len() {
pretty_class_name(&g.class_names[ci])
} else {
String::from("?")
}
};
let mut root_type_of: std::collections::HashMap<u32, u8> = std::collections::HashMap::new();
for (idx, &ty) in g.gc_root_indices.iter().zip(g.gc_root_types.iter()) {
root_type_of
.entry(*idx)
.and_modify(|e| *e = (*e).min(ty))
.or_insert(ty);
}
let vroot_u32 = n as u32;
let build_merged_paths = |members: &[u32], group_label: &str| -> Option<MergedPathNode> {
if members.is_empty() {
return None;
}
struct MNode {
display_class: String,
object_count: u64,
retained: u64,
root_type_label: Option<String>,
children: Vec<usize>,
}
let mut arena: Vec<MNode> = Vec::new();
arena.push(MNode {
display_class: group_label.to_string(),
object_count: 0,
retained: 0,
root_type_label: None,
children: Vec::new(),
});
for &m in members {
let mut chain: Vec<usize> = Vec::new();
let mut cur = m as usize;
let mut depth = 0usize;
loop {
chain.push(cur);
let idom = g.idom[cur];
if idom == vroot_u32 || idom == undef {
break;
}
if depth >= root_path_max_depth {
break;
}
cur = idom as usize;
depth += 1;
}
let last = chain.len().saturating_sub(1);
let mut node = 0usize; arena[node].object_count += 1;
arena[node].retained += g.retained[m as usize];
for (hop_i, &obj) in chain.iter().enumerate() {
let label = display_of(obj);
let existing = arena[node]
.children
.iter()
.copied()
.find(|&c| arena[c].display_class == label);
let child = match existing {
Some(c) => c,
None => {
if arena.len() >= MERGED_PATH_MAX_NODES {
break;
}
let idx = arena.len();
arena.push(MNode {
display_class: label,
object_count: 0,
retained: 0,
root_type_label: None,
children: Vec::new(),
});
arena[node].children.push(idx);
idx
}
};
arena[child].object_count += 1;
arena[child].retained += g.retained[obj];
if hop_i == last && arena[child].root_type_label.is_none() {
if let Some(&ty) = root_type_of.get(&(obj as u32)) {
if let Some(lbl) = gc_root_type_label_opt(ty) {
arena[child].root_type_label = Some(lbl.to_string());
}
}
}
node = child;
}
}
for i in 0..arena.len() {
let mut kids = std::mem::take(&mut arena[i].children);
kids.sort_by(|&a, &b| {
arena[b]
.retained
.cmp(&arena[a].retained)
.then(arena[b].object_count.cmp(&arena[a].object_count))
.then(arena[a].display_class.cmp(&arena[b].display_class))
});
arena[i].children = kids;
}
fn to_model(arena: &[MNode], idx: usize) -> MergedPathNode {
let node = &arena[idx];
MergedPathNode {
display_class: node.display_class.clone(),
object_count: node.object_count,
retained: node.retained,
root_type_label: node.root_type_label.clone(),
children: node.children.iter().map(|&c| to_model(arena, c)).collect(),
}
}
Some(to_model(&arena, 0))
};
let mut out: Vec<Suspect> = suspects
.iter()
.map(|s| {
let mut path: Vec<PathStep> = Vec::new();
let mut accumulation: Option<usize> = None;
let mut root_type_label = String::new();
if s.is_single {
if let Some(&ty) = root_type_of.get(&s.obj_idx) {
if let Some(label) = gc_root_type_label_opt(ty) {
root_type_label = label.to_string();
}
}
let mut cur = s.obj_idx as usize;
let mut cur_ret = g.retained[cur];
path.push(PathStep {
depth: 0,
obj_index_1based: cur + 1,
display_class: display_of(cur),
retained: cur_ret,
});
let mut depth = 0usize;
loop {
let best_child = dom_children(cur).iter().max_by(|&&a, &&b| {
g.retained[a as usize]
.cmp(&g.retained[b as usize])
.then(b.cmp(&a))
});
let Some(&c) = best_child else {
accumulation = Some(cur);
break;
};
let child = c as usize;
let child_ret = g.retained[child];
let drops = (child_ret as f64) < (cur_ret as f64) * BIG_DROP_RATIO;
if drops {
accumulation = Some(cur);
break;
}
depth += 1;
if depth >= MAX_ACCUM_DEPTH {
break;
}
path.push(PathStep {
depth,
obj_index_1based: child + 1,
display_class: display_of(child),
retained: child_ret,
});
cur = child;
cur_ret = child_ret;
}
}
let mut dominated: Vec<DominatedRow> = Vec::new();
let mut dominated_by_class: Vec<HistRow> = Vec::new();
let mut dominated_total_count: u64 = 0;
if let Some(ap) = accumulation {
let mut kids: Vec<u32> = dom_children(ap).to_vec();
dominated_total_count = kids.len() as u64;
kids.sort_unstable_by(|&a, &b| {
g.retained[b as usize]
.cmp(&g.retained[a as usize])
.then(a.cmp(&b))
});
for &k in kids.iter().take(cap) {
let ki = k as usize;
dominated.push(DominatedRow {
obj_index_1based: ki + 1,
display_class: display_of(ki),
shallow: g.shallow[ki] as u64,
retained: g.retained[ki],
});
}
let class_count = g.class_names.len();
let mut cls_count: std::collections::HashMap<usize, (u64, u64, u64)> =
std::collections::HashMap::new();
for &k in &kids {
let ki = k as usize;
let ci = g.class_idx[ki] as usize;
if ci < class_count {
let e = cls_count.entry(ci).or_insert((0, 0, 0));
e.0 += 1;
e.1 += g.shallow[ki] as u64;
e.2 += g.retained[ki];
}
}
let mut rows: Vec<(usize, u64, u64, u64)> = cls_count
.into_iter()
.map(|(ci, (c, sh, ret))| (ci, c, sh, ret))
.collect();
rows.sort_unstable_by(|a, b| b.3.cmp(&a.3).then(a.0.cmp(&b.0)));
for (ci, c, sh, ret) in rows.into_iter().take(cap) {
dominated_by_class.push(HistRow {
pretty_class: pretty_class_name(&g.class_names[ci]),
instances: c,
shallow: sh,
retained: ret,
max_instance_shallow: 0,
loader_id: g.class_loader_id.get(ci).copied().unwrap_or(0),
loader_label: {
let lid = g.class_loader_id.get(ci).copied().unwrap_or(0);
if lid == 0 {
Some("<boot>".to_string())
} else {
g.loader_labels.get(&lid).cloned()
}
},
});
}
}
let pretty_class = if s.obj_idx != u32::MAX {
display_of(s.obj_idx as usize)
} else {
pretty_class_name(&g.class_names[s.class_idx])
};
let mut keywords: Vec<String> = vec![pretty_class.clone()];
let (accumulation_class, accumulation_retained, accumulation_obj_1based) =
match accumulation {
Some(ap) => {
let ac = display_of(ap);
if !keywords.contains(&ac) {
keywords.push(ac.clone());
}
(Some(ac), Some(g.retained[ap]), Some(ap + 1))
}
None => (None, None, None),
};
let dominated_len_captured = dominated.len() as u64;
let dominator_tree_node: Option<DomTreeNode> = accumulation.map(|ap| {
build_dom_subtree(
ap,
dc_offsets,
dc_targets,
&display_of,
g,
dom_max_nodes,
dom_max_depth,
)
});
Suspect {
is_single: s.is_single,
pretty_class,
instance_count: s.instance_count,
retained: s.retained,
shallow: s.shallow,
path,
accumulation_obj_1based,
accumulation_class,
accumulation_retained,
dominated,
dominated_total_count,
dominated_shown: dominated_len_captured,
dominated_by_class,
keywords,
root_type_label,
root_path: None,
dominator_tree: dominator_tree_node,
merged_paths: None,
}
})
.collect();
{
let vroot = n as u32;
for (k, s) in suspects.iter().enumerate() {
if !s.is_single {
continue;
}
let mut chain: Vec<RootPathStep> = Vec::new();
let mut cur = s.obj_idx as usize;
let mut depth = 0usize;
loop {
let idom = g.idom[cur];
let is_root = idom == vroot;
let root_type_label = root_type_of
.get(&(cur as u32))
.and_then(|&ty| gc_root_type_label_opt(ty).map(|l| l.to_string()));
chain.push(RootPathStep {
obj_index_1based: cur + 1,
display_class: display_of(cur),
retained: g.retained[cur],
root_type_label,
});
if is_root || idom == undef {
break;
}
if depth >= root_path_max_depth {
break;
}
cur = idom as usize;
depth += 1;
}
out[k].root_path = Some(chain);
}
}
{
for (k, s) in suspects.iter().enumerate() {
if s.is_single {
continue;
}
let mut members: Vec<u32> = dom_children(n)
.iter()
.copied()
.filter(|&i| g.class_idx[i as usize] as usize == s.class_idx)
.collect();
members.sort_unstable();
let group_label = out[k].pretty_class.clone();
out[k].merged_paths = build_merged_paths(&members, &group_label);
}
}
LeakSuspects {
total_shallow,
suspects: out,
}
}
pub(crate) fn build_size_distribution(retained_desc: &[u64]) -> TopSizeDistribution {
if retained_desc.is_empty() {
return TopSizeDistribution::default();
}
let count = retained_desc.len() as u64;
let max = retained_desc[0];
let min = *retained_desc.last().unwrap();
let total: u64 = retained_desc.iter().sum();
let median = retained_desc[retained_desc.len() / 2];
let mut map: std::collections::BTreeMap<u64, u64> = std::collections::BTreeMap::new();
for &r in retained_desc {
let upper = if r <= 1 {
1
} else {
r.checked_next_power_of_two().unwrap_or(u64::MAX)
};
*map.entry(upper).or_insert(0) += 1;
}
let buckets = map
.into_iter()
.map(|(upper_bytes, count)| SizeBucket { upper_bytes, count })
.collect();
TopSizeDistribution {
buckets,
count,
min,
max,
median,
total,
}
}
fn build_top_consumers(g: &Graph, top_n: usize) -> TopConsumers {
let n = g.n;
let vroot = n as u32;
let undef = u32::MAX;
let class_count = g.class_names.len();
let mut top_level: Vec<u32> = Vec::new();
for i in 0..n {
if g.idom[i] == vroot {
top_level.push(i as u32);
}
}
let total_shallow: u64 = (0..n)
.filter(|&i| g.idom[i] != undef)
.map(|i| g.shallow[i] as u64)
.sum();
let mut sorted_top: Vec<u32> = top_level.clone();
sorted_top.sort_unstable_by(|&a, &b| {
g.retained[b as usize]
.cmp(&g.retained[a as usize])
.then(a.cmp(&b))
});
let sorted_retained: Vec<u64> = sorted_top.iter().map(|&i| g.retained[i as usize]).collect();
let size_distribution = build_size_distribution(&sorted_retained);
let biggest_objects: Vec<ObjRow> = sorted_top
.iter()
.take(top_n)
.map(|&i| {
let idx = i as usize;
let ci = g.class_idx[idx] as usize;
let display_class = if class_obj_repr(g, idx) != undef {
let repr = class_obj_repr(g, idx) as usize;
if repr < g.class_names.len() {
pretty_class_name(&g.class_names[repr])
} else if ci < g.class_names.len() {
pretty_class_name(&g.class_names[ci])
} else {
String::from("?")
}
} else if ci < g.class_names.len() {
pretty_class_name(&g.class_names[ci])
} else {
String::from("?")
};
let pct = if total_shallow > 0 {
g.retained[idx] as f64 / total_shallow as f64 * 100.0
} else {
0.0
};
let pct_bp = if total_shallow > 0 {
(g.retained[idx] as f64 / total_shallow as f64 * 10000.0).round() as u64
} else {
0
};
ObjRow {
obj_index_1based: idx + 1,
display_class,
shallow: g.shallow[idx] as u64,
retained: g.retained[idx],
pct_bp,
pct,
}
})
.collect();
let mut class_retained: Vec<u64> = vec![0; class_count];
let mut class_count_map: Vec<u64> = vec![0; class_count];
let remap = class_row_remap(g);
for &i in &top_level {
let idx = i as usize;
let ci = g.class_idx[idx] as usize;
if ci < class_count {
let ci = remap[ci] as usize;
class_retained[ci] += g.retained[idx];
class_count_map[ci] += 1;
}
}
let mut class_order: Vec<usize> = (0..class_count)
.filter(|&ci| class_retained[ci] > 0)
.collect();
class_order
.sort_unstable_by(|&a, &b| class_retained[b].cmp(&class_retained[a]).then(a.cmp(&b)));
let biggest_classes: Vec<ClassRow> = class_order
.iter()
.take(top_n)
.map(|&ci| ClassRow {
pretty_class: pretty_class_name(&g.class_names[ci]),
instances: class_count_map[ci],
retained: class_retained[ci],
})
.collect();
struct Builder {
top_dominator_count: u64,
shallow_heap: u64,
retained_heap: u64,
children: std::collections::BTreeMap<String, Builder>,
}
impl Builder {
fn new() -> Builder {
Builder {
top_dominator_count: 0,
shallow_heap: 0,
retained_heap: 0,
children: std::collections::BTreeMap::new(),
}
}
}
let mut root = Builder::new();
for &i in &top_level {
let idx = i as usize;
let raw_name = if class_obj_repr(g, idx) != undef {
let repr = class_obj_repr(g, idx) as usize;
if repr < g.class_names.len() {
&g.class_names[repr]
} else {
let ci = g.class_idx[idx] as usize;
if ci < g.class_names.len() {
&g.class_names[ci]
} else {
continue;
}
}
} else {
let ci = g.class_idx[idx] as usize;
if ci < g.class_names.len() {
&g.class_names[ci]
} else {
continue;
}
};
let retained = g.retained[idx];
let shallow = g.shallow[idx] as u64;
let path = package_path(raw_name);
root.top_dominator_count += 1;
root.shallow_heap += shallow;
root.retained_heap += retained;
let mut node = &mut root;
for seg in path.split('.') {
node = node
.children
.entry(seg.to_string())
.or_insert_with(Builder::new);
node.top_dominator_count += 1;
node.shallow_heap += shallow;
node.retained_heap += retained;
}
}
let total = root.retained_heap;
let threshold_bp = PACKAGE_THRESHOLD_BP;
fn convert(name: String, b: Builder, total: u64, threshold_bp: u32) -> PackageNode {
let mut children: Vec<PackageNode> = b
.children
.into_iter()
.filter(|(_, cb)| {
cb.retained_heap as u128 * 10_000 >= total as u128 * threshold_bp as u128
})
.map(|(seg, cb)| convert(seg, cb, total, threshold_bp))
.collect();
children.sort_by(|a, b| {
b.retained_heap
.cmp(&a.retained_heap)
.then_with(|| a.name.cmp(&b.name))
});
PackageNode {
name,
top_dominator_count: b.top_dominator_count,
shallow_heap: b.shallow_heap,
retained_heap: b.retained_heap,
children,
}
}
let biggest_packages = convert(String::new(), root, total, threshold_bp);
TopConsumers {
biggest_objects,
biggest_classes,
threshold_bp,
biggest_packages,
size_distribution,
}
}
#[cfg(test)]
mod fragmentation_tests {
use super::*;
#[test]
fn fragmentation_ratio_zero_when_no_unreachable() {
assert_eq!(compute_fragmentation_ratio(1000, 0), 0.0_f64);
}
#[test]
fn fragmentation_ratio_half() {
assert!((compute_fragmentation_ratio(500, 500) - 0.5).abs() < 1e-9);
}
#[test]
fn fragmentation_ratio_zero_empty_heap() {
assert_eq!(compute_fragmentation_ratio(0, 0), 0.0_f64);
}
}
#[cfg(test)]
mod attribution_tests {
use super::*;
fn rec(
container_idx: u32,
holder: &str,
field: &str,
kind: u8,
container_class: &str,
elements: u64,
) -> AttributionRaw {
AttributionRaw {
container_idx,
holder_class: holder.to_string(),
field: field.to_string(),
container_kind: kind,
container_class: container_class.to_string(),
elements,
capacity: elements,
}
}
fn no_holders() -> std::collections::HashMap<String, u64> {
std::collections::HashMap::new()
}
#[test]
fn test_ordering_desc_by_elements() {
let raw = vec![
rec(0, "com/foo/Big", "items", 0, "java/util/ArrayList", 100),
rec(1, "com/foo/Small", "items", 0, "java/util/ArrayList", 10),
];
let retained = vec![5000u64, 500u64];
let mut holders = std::collections::HashMap::new();
holders.insert("com.foo.Big".to_string(), 3u64);
let ca = aggregate_collection_attribution(&raw, &retained, false, &holders);
assert_eq!(ca.most_overall.len(), 2);
assert_eq!(ca.most_overall[0].holder_class, "com/foo/Big");
assert_eq!(ca.most_overall[0].total_elements, 100);
assert_eq!(ca.most_overall[0].total_retained, 5000);
assert_eq!(
ca.most_overall[0].holder_instances, 3,
"holder_instances populated from the map"
);
assert_eq!(ca.most_overall[1].holder_instances, 0, "absent holder ⇒ 0");
assert_eq!(ca.most_overall[1].holder_class, "com/foo/Small");
assert_eq!(ca.biggest_single[0].holder_class, "com/foo/Big");
assert_eq!(ca.biggest_single[0].elements, 100);
assert_eq!(
ca.biggest_single[0].capacity, 100,
"rec() defaults capacity to elements"
);
assert_eq!(ca.biggest_single[0].container_class, "java.util.ArrayList");
assert!(!ca.truncated);
}
#[test]
fn test_distinct_container_dedup() {
let raw = vec![
rec(0, "com/foo/Cache", "map", 0, "java/util/HashMap", 42),
rec(0, "com/foo/Cache", "map", 0, "java/util/HashMap", 42),
];
let retained = vec![9000u64];
let ca = aggregate_collection_attribution(&raw, &retained, false, &no_holders());
assert_eq!(ca.most_overall.len(), 1);
let row = &ca.most_overall[0];
assert_eq!(row.container_count, 1, "shared container counted once");
assert_eq!(row.total_elements, 42, "elements not double-counted");
assert_eq!(row.total_retained, 9000, "retained not double-counted");
}
#[test]
fn test_mixed_kind() {
let raw = vec![
rec(0, "com/foo/Holder", "data", 0, "java/util/ArrayList", 5),
rec(1, "com/foo/Holder", "data", 6, "[Ljava/lang/Object;", 7),
];
let retained = vec![100u64, 200u64];
let ca = aggregate_collection_attribution(&raw, &retained, false, &no_holders());
assert_eq!(ca.most_overall.len(), 1);
assert_eq!(ca.most_overall[0].container_kind, "mixed");
assert_eq!(ca.most_overall[0].container_count, 2);
assert_eq!(ca.most_overall[0].total_elements, 12);
assert_eq!(ca.most_overall[0].total_retained, 300);
}
#[test]
fn test_single_kind_label() {
let raw = vec![rec(0, "com/foo/H", "arr", 7, "[I", 3)];
let retained = vec![64u64];
let ca = aggregate_collection_attribution(&raw, &retained, true, &no_holders());
assert_eq!(ca.most_overall[0].container_kind, "primitive array");
assert!(ca.truncated);
}
#[test]
fn test_out_of_range_retained_is_zero() {
let raw = vec![rec(99, "com/foo/H", "f", 0, "java/util/ArrayList", 1)];
let retained = vec![10u64]; let ca = aggregate_collection_attribution(&raw, &retained, false, &no_holders());
assert_eq!(ca.most_overall[0].total_retained, 0);
assert_eq!(ca.biggest_single[0].retained, 0);
}
}
#[cfg(test)]
mod leak_indicator_tests {
use super::*;
#[test]
fn anonymous_class_patterns() {
assert!(is_anonymous_class("com/example/Foo$1")); assert!(is_anonymous_class("com/example/Foo$$Lambda$42/0x1234")); assert!(is_anonymous_class("com/example/Foo$Proxy1")); assert!(is_anonymous_class("com/example/$$Anon")); assert!(!is_anonymous_class("com/example/Foo$Bar")); assert!(!is_anonymous_class("java/lang/String")); }
}