#![allow(dead_code)]
use std::{
cmp::Reverse,
collections::HashMap,
io::{self, ErrorKind},
};
use crate::{reader::HprofReader, types::tags, vbyte};
type PairFieldTally = HashMap<(u32, u32), Vec<(String, u32)>>;
use super::Pass2;
pub const INB_BLOCK: usize = 16;
#[derive(Debug, Clone, Default)]
pub struct ThreadStack {
pub thread_serial: u32,
pub thread_obj_idx: u32,
pub frames: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ThreadProps {
pub name: String,
pub is_daemon: bool,
pub priority: i32,
pub thread_status: i32,
pub context_loader_addr: u64,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct RecordCensus {
pub utf8_records: u64,
pub load_class_records: u64,
pub unload_class_records: u64,
pub stack_frame_records: u64,
pub stack_trace_records: u64,
pub heap_dump_segments: u64,
pub instance_dumps: u64,
pub obj_array_dumps: u64,
pub prim_array_dumps: u64,
pub class_dumps: u64,
pub gc_root_tag_counts: Vec<(u8, u64)>,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct DupStrings {
pub distinct_values: u64,
pub duplicated_values: u64,
pub total_string_instances: u64,
pub approx_wasted_bytes: u64,
#[serde(default)]
pub top_duplicated: Vec<DupStringSample>,
#[serde(default)]
pub length_histogram: Vec<StrLenBucket>,
#[serde(default)]
pub length_stats: StrLenStats,
#[serde(default)]
pub top_string_holders: Vec<StringHolder>,
#[serde(default)]
pub top_by_length: Vec<DupStringSample>,
#[serde(default)]
pub char_array_waste: Option<CharArrayWaste>,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct DupStringSample {
pub text: String,
pub count: u64,
pub len: u32,
pub wasted_bytes: u64,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct CharArrayWasteRow {
pub array_obj_1based: usize,
pub length: u64,
pub used: u64,
pub wasted_bytes: u64,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct CharArrayWaste {
pub arrays_examined: u64,
pub wasteful_arrays: u64,
pub total_wasted_bytes: u64,
pub top: Vec<CharArrayWasteRow>,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct StrLenBucket {
pub upper_len: u32,
pub count: u64,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct StrLenStats {
pub min: u32,
pub max: u32,
pub median: u32,
pub total: u64,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct StringHolder {
pub class_name: String,
pub string_refs: u64,
}
#[derive(Clone)]
pub struct AttributionRaw {
pub container_idx: u32,
pub holder_class: String,
pub field: String,
pub container_kind: u8,
pub container_class: String,
pub elements: u64,
pub capacity: u64,
}
#[derive(Clone)]
pub struct FieldSizeRaw {
pub holder_class: String,
pub field: String,
pub pointee_indices: Vec<u32>,
}
#[derive(Clone)]
pub struct CollValuesRaw {
pub container_idx: u32,
pub kind: u8,
pub container_class: String,
pub owner: Option<String>,
pub value_indices: Vec<u32>,
}
pub struct Graph {
pub n: usize,
pub format: String,
pub file_size: u64,
pub source_name: String,
pub file_path: String,
pub id_size: u8,
pub ref_size: u8,
pub header_timestamp_ms: u64,
pub gc_root_indices: Vec<u32>,
#[allow(dead_code)]
pub gc_root_types: Vec<u8>,
pub shallow: Vec<u32>,
pub class_idx: Vec<u32>,
pub class_names: Vec<String>,
pub class_loader_id: Vec<u64>,
pub loader_labels: std::collections::HashMap<u64, String>,
pub thread_stacks: Vec<ThreadStack>,
pub thread_props: std::collections::HashMap<u32, ThreadProps>,
pub thread_local_counts: std::collections::HashMap<u32, u64>,
pub thread_local_samples: std::collections::HashMap<u32, Vec<u32>>,
pub thread_local_frame_samples: std::collections::HashMap<u32, Vec<(u32, u32)>>,
pub system_properties: Vec<(String, String)>,
pub jvm_version: Option<String>,
pub class_obj_class_idx: HashMap<u32, u32>, pub fwd_offsets: Vec<u32>,
pub fwd_targets: crate::chunkvec::ChunkU32,
pub synthetic_root_count: usize,
pub system_classloader_shallow: Option<u32>,
pub idom: Vec<u32>,
pub retained: Vec<u64>,
pub has_same_class_ancestor: crate::bitset::Bitset,
pub alloc_stack_serial: Vec<u32>,
pub alloc_frames_by_serial: Option<std::collections::HashMap<u32, Vec<String>>>,
pub record_census: RecordCensus,
pub dup_strings: Option<DupStrings>,
pub dup_prim_arrays: Option<super::DupPrimArrays>,
pub boxed_number_holders: Vec<crate::report::BoxedNumberHolder>,
pub arrays_by_size: crate::report::ArraysBySize,
pub incoming_refs_per_class: Vec<u64>,
pub collections: crate::report::CollectionsAnalysis,
pub references: crate::report::ReferencesAnalysis,
pub reference_referent_idx: [Vec<u32>; 3],
pub reference_null_referent_count: [u64; 3],
pub collection_attribution_raw: Option<Vec<AttributionRaw>>,
pub collection_attribution_truncated: bool,
pub fields_by_size_raw: Option<Vec<FieldSizeRaw>>,
pub coll_values_raw: Option<Vec<CollValuesRaw>>,
pub node_kv: Option<std::collections::HashMap<u32, (u32, u32)>>,
pub fwd_field_name_idx: Option<Vec<u16>>,
pub field_name_pool: Option<Vec<String>>,
#[allow(dead_code)]
pub direct_byte_buffer_capacity_sum: u64,
#[allow(dead_code)]
pub thread_local_null_key_count: u64,
pub tl_entry_records: Vec<(bool, u32)>,
pub unreachable_retained: Option<crate::unreachable_retained::UnreachableRetained>,
pub obj_graph_edges: Option<ObjGraphCapture>,
pub type_ref_pairs: Option<HashMap<(u32, u32), u64>>,
pub type_ref_pair_fields: Option<PairFieldTally>,
pub class_ref_field_names: Vec<Vec<String>>,
}
pub struct ObjGraphCapture {
pub n: usize,
pub edges: std::collections::HashMap<u32, Box<[(u32, u16)]>>,
pub inbound: std::collections::HashMap<u32, Box<[(u32, u16)]>>,
pub inbound_truncated: crate::bitset::Bitset,
pub field_name_pool: Vec<String>,
pub captured: crate::bitset::Bitset,
}
impl ObjGraphCapture {
pub fn empty() -> Self {
Self {
n: 0,
edges: std::collections::HashMap::new(),
inbound: std::collections::HashMap::new(),
inbound_truncated: Default::default(),
field_name_pool: vec![String::new()],
captured: Default::default(),
}
}
#[inline]
pub fn edges_of(&self, i: usize) -> &[(u32, u16)] {
self.edges
.get(&(i as u32))
.map(|b| b.as_ref())
.unwrap_or(&[])
}
#[inline]
pub fn inbound_of(&self, i: usize) -> &[(u32, u16)] {
self.inbound
.get(&(i as u32))
.map(|b| b.as_ref())
.unwrap_or(&[])
}
}
fn name_idx_for(
fwd_names: Option<&Vec<u16>>,
name_pool: Option<&Vec<String>>,
pos: usize,
name_map: &mut std::collections::HashMap<String, u16>,
field_name_pool: &mut Vec<String>,
) -> u16 {
match (fwd_names, name_pool) {
(Some(idx_vec), Some(pool)) => {
let ni = idx_vec[pos] as usize;
let name: &str = if ni < pool.len() { &pool[ni] } else { "" };
if name.is_empty() {
0u16
} else {
let next = name_map.len() as u16;
let idx = *name_map.entry(name.to_owned()).or_insert(next);
if idx as usize >= field_name_pool.len() {
field_name_pool.push(name.to_owned());
}
idx
}
}
_ => 0u16,
}
}
pub fn capture_type_ref_graph(g: &Graph) -> (HashMap<(u32, u32), u64>, PairFieldTally) {
let n = if g.shallow.is_empty() {
g.n
} else {
g.shallow.len()
};
if n == 0 || g.fwd_offsets.is_empty() || g.class_idx.is_empty() {
return (HashMap::new(), HashMap::new());
}
let has_names = g.fwd_field_name_idx.is_some() && g.field_name_pool.is_some();
let mut pair_map: HashMap<(u32, u32), u64> = HashMap::new();
let mut field_tally: HashMap<(u32, u32), HashMap<u16, u32>> = HashMap::new();
for src_idx in 0..n {
let start = g.fwd_offsets[src_idx] as usize;
let end = g.fwd_offsets[src_idx + 1] as usize;
if start == end {
continue;
}
if src_idx >= g.class_idx.len() {
continue;
}
let src_ci = g.class_idx[src_idx];
for pos in start..end {
let dst_idx = g.fwd_targets.get(pos) as usize;
if dst_idx >= g.class_idx.len() {
continue;
}
let dst_ci = g.class_idx[dst_idx];
*pair_map.entry((src_ci, dst_ci)).or_insert(0) += 1;
if has_names {
if let Some(name_idx) = g
.fwd_field_name_idx
.as_ref()
.and_then(|v| v.get(pos))
.copied()
{
if name_idx != 0 {
*field_tally
.entry((src_ci, dst_ci))
.or_default()
.entry(name_idx)
.or_insert(0) += 1;
}
}
}
}
}
let name_pool = g.field_name_pool.as_deref().unwrap_or(&[]);
let pair_fields: std::collections::HashMap<(u32, u32), Vec<(String, u32)>> = field_tally
.into_iter()
.map(|(key, counts)| {
let mut sorted: Vec<(String, u32)> = counts
.into_iter()
.filter_map(|(idx, cnt)| {
let name = name_pool.get(idx as usize)?;
if name.is_empty() {
return None;
}
Some((name.clone(), cnt))
})
.collect();
sorted.sort_unstable_by_key(|a: &(String, u32)| Reverse(a.1));
sorted.truncate(3);
(key, sorted)
})
.filter(|(_, v)| !v.is_empty())
.collect();
(pair_map, pair_fields)
}
pub fn capture_obj_graph_edges(g: &Graph, top_n: usize, edge_cap: usize) -> ObjGraphCapture {
let n = if g.shallow.is_empty() {
g.n
} else {
g.shallow.len()
};
if n == 0 || g.fwd_offsets.is_empty() {
return ObjGraphCapture::empty();
}
let mut cap = ObjGraphCapture::empty();
cap.n = n;
cap.captured = crate::bitset::Bitset::with_len(n);
cap.inbound_truncated = crate::bitset::Bitset::with_len(n);
let mut name_map: std::collections::HashMap<String, u16> = std::collections::HashMap::new();
name_map.insert(String::new(), 0u16);
let fwd_names = g.fwd_field_name_idx.as_ref();
let name_pool = g.field_name_pool.as_ref();
let captured_nodes: Vec<u32> = if top_n >= n {
Vec::new()
} else {
let sample_step = (n / top_n.min(n)).max(1);
let mut sample: Vec<u32> = g.shallow.iter().step_by(sample_step).copied().collect();
sample.sort_unstable_by(|a, b| b.cmp(a));
let threshold = sample
.get(top_n.min(sample.len()).saturating_sub(1))
.copied()
.unwrap_or(0);
drop(sample);
let mut nodes: Vec<u32> = g
.shallow
.iter()
.enumerate()
.filter_map(|(i, &s)| if s >= threshold { Some(i as u32) } else { None })
.collect();
if nodes.len() > top_n {
nodes.sort_unstable_by(|&a, &b| g.shallow[b as usize].cmp(&g.shallow[a as usize]));
nodes.truncate(top_n);
}
nodes
};
let all_captured = top_n >= n;
if all_captured {
for i in 0..n {
cap.captured.set(i);
}
} else {
for &s in &captured_nodes {
cap.captured.set(s as usize);
}
}
{
let iter: Box<dyn Iterator<Item = u32>> = if all_captured {
Box::new(0u32..n as u32)
} else {
Box::new(captured_nodes.iter().copied())
};
for src in iter {
let s = src as usize;
let fwd_start = g.fwd_offsets[s] as usize;
let fwd_end = g.fwd_offsets[s + 1] as usize;
let take = (fwd_end - fwd_start).min(edge_cap);
if take == 0 {
continue;
}
let mut edges: Vec<(u32, u16)> = Vec::with_capacity(take);
for pos in fwd_start..fwd_start + take {
let dst = g.fwd_targets.get(pos);
let name_idx = name_idx_for(
fwd_names,
name_pool,
pos,
&mut name_map,
&mut cap.field_name_pool,
);
edges.push((dst, name_idx));
}
cap.edges.insert(src, edges.into_boxed_slice());
}
}
drop(captured_nodes);
crate::trace::probe("capture_obj_graph: after outbound sparse built");
let mut inbound_acc: std::collections::HashMap<u32, Vec<(u32, u16)>> =
std::collections::HashMap::new();
for src in 0..n as u32 {
let start = g.fwd_offsets[src as usize] as usize;
let end = g.fwd_offsets[src as usize + 1] as usize;
for pos in start..end {
let dst = g.fwd_targets.get(pos) as usize;
if !cap.captured.get(dst) {
continue;
}
let bucket = inbound_acc.entry(dst as u32).or_default();
if bucket.len() < edge_cap {
let name_idx = name_idx_for(
fwd_names,
name_pool,
pos,
&mut name_map,
&mut cap.field_name_pool,
);
bucket.push((src, name_idx));
} else {
cap.inbound_truncated.set(dst);
}
}
}
for (dst, vec) in inbound_acc {
cap.inbound.insert(dst, vec.into_boxed_slice());
}
crate::trace::probe("capture_obj_graph: after inbound sparse built");
cap
}
#[allow(dead_code)]
pub struct InboundBuilder {
pub(crate) source: crate::source::HprofSource,
pub(crate) id_size: u8,
pub(crate) n: usize,
pub(crate) id_map: Option<crate::id_map::IdMap>,
pub(crate) id_map_c: Option<(Vec<u8>, usize)>,
pub(crate) id_map_codec: crate::cvec::Codec,
pub(crate) class_addr_to_hist: HashMap<u64, u32>,
pub(crate) field_plans_dense: Vec<super::FieldPlan>,
pub(crate) in_cursors: Vec<u32>,
pub(crate) total_inb: u64,
pub(crate) synthetic_edges: Vec<(u32, u32)>,
}
pub struct MatOutboundRescanCtx {
pub source: crate::source::HprofSource,
pub id_size: u8,
pub id_map_c: Option<(Vec<u8>, usize)>,
pub id_map_codec: crate::cvec::Codec,
pub class_addr_to_hist: HashMap<u64, u32>,
pub field_plans_dense: Vec<super::FieldPlan>,
}
impl InboundBuilder {
pub fn take_for_outbound_rescan(&mut self) -> MatOutboundRescanCtx {
MatOutboundRescanCtx {
source: self.source.clone(),
id_size: self.id_size,
id_map_c: self.id_map_c.clone(),
id_map_codec: self.id_map_codec,
class_addr_to_hist: self.class_addr_to_hist.clone(),
field_plans_dense: self.field_plans_dense.clone(),
}
}
pub fn compress_id_map(&mut self, codec: crate::cvec::Codec) -> io::Result<()> {
self.id_map_codec = codec;
if codec == crate::cvec::Codec::None {
return Ok(());
}
if let Some(m) = self.id_map.take() {
let (blob, len) = m.compress(codec)?;
self.id_map_c = Some((blob, len));
}
Ok(())
}
pub fn build_from_fwd(
self,
fwd_offsets: Vec<u32>,
mut fwd_targets: crate::chunkvec::ChunkU32,
dfn: &[u32],
) -> io::Result<(Vec<u64>, Vec<u8>)> {
let InboundBuilder {
n,
in_cursors,
total_inb,
synthetic_edges: _,
id_map,
id_map_c,
class_addr_to_hist,
field_plans_dense,
..
} = self;
crate::trace::probe("inbound fwd-transpose: after struct destructure (before any drop)");
drop(id_map);
crate::trace::probe("inbound fwd-transpose: after drop(id_map)");
if let Some((blob, _)) = id_map_c {
crate::trace::drop_vec(blob);
}
crate::trace::probe("inbound fwd-transpose: after drop(id_map_c blob)");
drop(class_addr_to_hist);
drop(field_plans_dense);
crate::trace::probe(
"inbound fwd-transpose: after drop(class_addr_to_hist+field_plans_dense)",
);
crate::trace::trim();
crate::trace::probe("inbound fwd-transpose: after trim (before inb_flat alloc)");
let mut inb_flat = crate::chunkvec::ChunkU32::zeroed(total_inb as usize);
if crate::trace::enabled() {
eprintln!(
"[trace-rss] inbound (fwd-transpose): total_inb={} edges, inb_flat={} MB",
total_inb,
(total_inb as usize * 4) / (1024 * 1024)
);
}
crate::trace::probe("inbound fwd-transpose: after inb_flat alloc");
let mut in_cursors = in_cursors;
let n_nodes = fwd_offsets.len().saturating_sub(1);
let mut buf: Vec<u32> = Vec::with_capacity(4096);
let mut next_fwd_free: usize = 1 << 26; #[cfg(target_os = "linux")]
let fwd_off_ptr = fwd_offsets.as_ptr();
#[cfg(target_os = "linux")]
let mut next_off_free: usize = 1 << 10; for src in 0..n_nodes {
let lo = fwd_offsets[src] as usize;
let hi = fwd_offsets[src + 1] as usize;
if lo == hi {
continue; }
if lo >= next_fwd_free {
fwd_targets.free_below(lo);
next_fwd_free = ((lo >> 26) + 1) << 26; }
#[cfg(target_os = "linux")]
if src >= next_off_free {
let pages_end = src & !(1024 - 1); let len = pages_end * std::mem::size_of::<u32>();
if len > 0 {
unsafe {
libc::madvise(
fwd_off_ptr as *mut libc::c_void,
len,
8,
);
}
}
next_off_free = pages_end + 1024; }
let targets: &[u32] = if let Some(sl) = fwd_targets.range_slice(lo, hi) {
sl
} else {
fwd_targets.copy_range(lo, hi, &mut buf);
&buf
};
for &dst in targets {
let dst = dst as usize;
inb_flat.set(in_cursors[dst] as usize, src as u32);
in_cursors[dst] += 1;
}
}
crate::trace::drop_vec(fwd_offsets);
drop(fwd_targets);
crate::trace::trim();
crate::trace::probe("inbound fwd-transpose: after transpose loop");
crate::trace::probe("inbound: before Phase-4 (after fwd-transpose)");
Self::encode_phase4(n, total_inb, in_cursors, inb_flat, dfn)
}
#[cfg(test)]
pub fn build(self, dfn: &[u32]) -> io::Result<(Vec<u64>, Vec<u8>)> {
let InboundBuilder {
source,
id_size,
n,
id_map,
id_map_c,
id_map_codec,
class_addr_to_hist,
field_plans_dense,
mut in_cursors,
total_inb,
synthetic_edges,
..
} = self;
let id_map = match id_map {
Some(m) => m,
None => {
let (blob, len) = id_map_c.expect("id_map neither live nor compressed");
crate::id_map::IdMap::from_compressed(&blob, len, id_map_codec)?
}
};
let mut inb_flat = crate::chunkvec::ChunkU32::zeroed(total_inb as usize);
if crate::trace::enabled() {
eprintln!(
"[trace-rss] inbound 2b: total_inb={} edges, inb_flat={} MB",
total_inb,
(total_inb as usize * 4) / (1024 * 1024)
);
}
crate::trace::probe("inbound 2b: after inb_flat alloc");
{
let mut r = source.open()?;
let mut scratch: Vec<u8> = Vec::with_capacity(4096);
let mut fwd_t_stub: crate::chunkvec::ChunkU32 = crate::chunkvec::ChunkU32::zeroed(0);
let mut fwd_offsets_stub: Vec<u32> = Vec::new();
loop {
let tag = match r.u1() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
};
let _ts = match r.u4() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
};
let length = match r.u4() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
} as u64;
match tag {
tags::HEAP_DUMP | tags::HEAP_DUMP_SEGMENT => {
Pass2::fill_heap_2b(
&mut r,
id_size,
length,
&id_map,
&class_addr_to_hist,
&field_plans_dense,
&[], false,
true,
&mut fwd_t_stub,
&mut fwd_offsets_stub,
&mut None, &mut Vec::new(),
&mut std::collections::HashMap::new(),
&mut inb_flat,
&mut in_cursors,
&mut scratch,
)?;
}
tags::HEAP_DUMP_END => break,
_ => match r.skip(length) {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
},
}
}
}
drop(id_map);
drop(class_addr_to_hist);
drop(field_plans_dense);
for &(src, dst) in &synthetic_edges {
inb_flat.set(in_cursors[dst as usize] as usize, src);
in_cursors[dst as usize] += 1;
}
crate::trace::probe("inbound: before Phase-4 (after 2b scan + drops)");
Self::encode_phase4(n, total_inb, in_cursors, inb_flat, dfn)
}
fn encode_phase4(
n: usize,
total_inb: u64,
in_cursors: Vec<u32>,
mut inb_flat: crate::chunkvec::ChunkU32,
dfn: &[u32],
) -> io::Result<(Vec<u64>, Vec<u8>)> {
#[allow(clippy::redundant_locals)]
let in_cursors = in_cursors;
let mut inb_block_off: Vec<u64> = Vec::with_capacity(n / INB_BLOCK + 2);
let mut inb_data: Vec<u8> = Vec::new();
#[cfg(target_pointer_width = "64")]
let six_gb: usize = 6 * 1024 * 1024 * 1024;
#[cfg(not(target_pointer_width = "64"))]
let six_gb: usize = usize::MAX;
let inb_data_cap = ((total_inb as usize).saturating_mul(2)).min(six_gb);
inb_data.reserve(inb_data_cap);
let mut start = 0usize;
let mut nb: Vec<u32> = Vec::new();
let mut next_free_at: usize = 1 << 26;
for i in 0..n {
let end = in_cursors[i] as usize;
if i % INB_BLOCK == 0 {
inb_block_off.push(inb_data.len() as u64);
}
let count = end - start;
if count == 0 {
vbyte::encode(0, &mut inb_data);
start = end;
if start >= next_free_at {
inb_flat.free_below(start);
next_free_at = start + (1 << 26);
}
if i == n / 2 {
crate::trace::probe("inbound Phase-4: midpoint (inb_flat+inb_data coexist)");
}
continue;
}
let mut w = 0usize;
if let Some(raw) = inb_flat.range_slice(start, end) {
nb.clear();
nb.reserve(raw.len());
for &raw_val in raw {
let node = (raw_val & 0x7fff_ffff) as usize;
let pre = dfn[node];
if pre != u32::MAX {
nb.push(pre);
w += 1;
}
}
} else {
inb_flat.copy_range(start, end, &mut nb);
for r in 0..nb.len() {
let node = (nb[r] & 0x7fff_ffff) as usize;
let pre = dfn[node];
if pre != u32::MAX {
nb[w] = pre;
w += 1;
}
}
}
let unique_end = if w <= 1 {
w
} else {
let pre_slice = &mut nb[..w];
pre_slice.sort_unstable();
let mut write = 1usize;
for read in 1..pre_slice.len() {
if pre_slice[read] != pre_slice[write - 1] {
pre_slice[write] = pre_slice[read];
write += 1;
}
}
write
};
vbyte::encode(unique_end as u32, &mut inb_data);
let mut prev: u32 = 0;
for &pre in &nb[..unique_end] {
vbyte::encode(pre - prev, &mut inb_data);
prev = pre;
}
start = end;
if start >= next_free_at {
inb_flat.free_below(start);
next_free_at = start + (1 << 26);
}
if i == n / 2 {
crate::trace::probe("inbound Phase-4: midpoint (inb_flat+inb_data coexist)");
}
}
drop(nb);
drop(inb_flat);
drop(in_cursors);
inb_block_off.push(inb_data.len() as u64);
if crate::trace::enabled() {
eprintln!(
"[trace-rss] inbound Phase-4: inb_data len={} MB cap={} MB block_off len={}",
inb_data.len() / (1024 * 1024),
inb_data.capacity() / (1024 * 1024),
inb_block_off.len()
);
}
crate::trace::probe("inbound Phase-4: after inb_data built");
Ok((inb_block_off, inb_data))
}
pub fn build_mat_scan<F>(
self,
dfn: &[u32],
mut on_outbound: F,
) -> io::Result<(Vec<u64>, Vec<u8>)>
where
F: FnMut(usize, Vec<u32>) -> io::Result<()>,
{
let InboundBuilder {
source,
id_size,
n,
id_map,
id_map_c,
id_map_codec,
class_addr_to_hist,
field_plans_dense,
mut in_cursors,
total_inb,
synthetic_edges,
..
} = self;
let id_map = match id_map {
Some(m) => m,
None => {
let (blob, len) = id_map_c.expect("id_map neither live nor compressed");
crate::id_map::IdMap::from_compressed(&blob, len, id_map_codec)?
}
};
let mut inb_flat = crate::chunkvec::ChunkU32::zeroed(total_inb as usize);
if crate::trace::enabled() {
eprintln!(
"[trace-rss] inbound mat-scan: total_inb={} edges, inb_flat={} MB",
total_inb,
(total_inb as usize * 4) / (1024 * 1024)
);
}
crate::trace::probe("inbound mat-scan: after inb_flat alloc");
{
let mut r = source.open()?;
let mut scratch: Vec<u8> = Vec::with_capacity(4096);
let ids = id_size as u64;
let mut cache = crate::id_map::IndexCache::new();
loop {
let tag = match r.u1() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
};
let _ts = match r.u4() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
};
let length = match r.u4() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
} as u64;
match tag {
tags::HEAP_DUMP | tags::HEAP_DUMP_SEGMENT => {
Self::scan_inb_and_outbound(
&mut r,
id_size,
ids,
length,
&id_map,
&class_addr_to_hist,
&field_plans_dense,
&mut inb_flat,
&mut in_cursors,
&mut scratch,
&mut cache,
&mut on_outbound,
)?;
}
tags::HEAP_DUMP_END => break,
_ => match r.skip(length) {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
},
}
}
}
drop(id_map);
drop(class_addr_to_hist);
drop(field_plans_dense);
for &(src, dst) in &synthetic_edges {
inb_flat.set(in_cursors[dst as usize] as usize, src);
in_cursors[dst as usize] += 1;
}
crate::trace::probe("inbound: before Phase-4 (after mat-scan + drops)");
Self::encode_phase4(n, total_inb, in_cursors, inb_flat, dfn)
}
#[allow(clippy::too_many_arguments)]
fn scan_inb_and_outbound(
r: &mut HprofReader,
id_size: u8,
ids: u64,
mut remaining: u64,
id_map: &crate::id_map::IdMap,
class_addr_to_hist: &HashMap<u64, u32>,
field_plans_dense: &[super::FieldPlan],
inb_flat: &mut crate::chunkvec::ChunkU32,
in_cursors: &mut Vec<u32>,
scratch: &mut Vec<u8>,
cache: &mut crate::id_map::IndexCache,
on_outbound: &mut dyn FnMut(usize, Vec<u32>) -> io::Result<()>,
) -> io::Result<()> {
use crate::types::heap;
macro_rules! checked_sub {
($rem:expr, $sz:expr) => {
$rem = $rem
.checked_sub($sz)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "segment overrun"))?;
};
}
macro_rules! try_read {
($e:expr) => {
match $e {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(()),
other => other?,
}
};
}
while remaining > 0 {
let sub_tag = try_read!(r.u1());
checked_sub!(remaining, 1u64);
match sub_tag {
heap::ROOT_SYSTEM_CLASS
| heap::ROOT_UNKNOWN
| heap::ROOT_MONITOR_USED
| heap::ROOT_INTERNED_STRING
| heap::ROOT_DEBUGGER
| heap::ROOT_VM_INTERNAL => {
try_read!(r.skip(ids));
checked_sub!(remaining, ids);
}
heap::ROOT_JNI_GLOBAL => {
try_read!(r.skip(2 * ids));
checked_sub!(remaining, 2 * ids);
}
heap::ROOT_JNI_LOCAL | heap::ROOT_JAVA_FRAME | heap::ROOT_JNI_MONITOR => {
try_read!(r.skip(ids + 8));
checked_sub!(remaining, ids + 8);
}
heap::ROOT_NATIVE_STACK | heap::ROOT_THREAD_BLOCK => {
try_read!(r.skip(ids + 4));
checked_sub!(remaining, ids + 4);
}
heap::ROOT_STICKY_CLASS | heap::ROOT_THREAD_OBJ => {
let skip = if sub_tag == heap::ROOT_THREAD_OBJ {
ids + 8
} else {
ids
};
try_read!(r.skip(skip));
checked_sub!(remaining, skip);
}
heap::CLASS_DUMP => {
let consumed = match Pass2::fill_class_dump_edges(
r,
id_size,
id_map,
false, true, &mut crate::chunkvec::ChunkU32::zeroed(0),
&mut Vec::new(),
&mut None,
inb_flat,
in_cursors,
) {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(()),
other => other?,
};
checked_sub!(remaining, consumed);
}
heap::INSTANCE_DUMP => {
let addr = try_read!(r.id());
try_read!(r.skip(4)); let class_id = try_read!(r.id());
let data_len = try_read!(r.u4()) as u64;
try_read!(r.read_bytes_reuse(scratch, data_len as usize));
checked_sub!(remaining, ids + 4 + ids + 4 + data_len);
let src_idx = match id_map.index_of(addr) {
Some(i) => i,
None => continue,
};
let mut fwd: Vec<u32> = Vec::new();
if let Some(dst) = cache.index_of(id_map, class_id) {
inb_flat.set(in_cursors[dst] as usize, src_idx as u32);
in_cursors[dst] += 1;
fwd.push(dst as u32);
}
if let Some(&cidx) = class_addr_to_hist.get(&class_id) {
if (cidx as usize) < field_plans_dense.len() {
for &(off, _excluded) in &field_plans_dense[cidx as usize] {
let off = off as usize;
if off + id_size as usize <= scratch.len() {
let ref_val =
super::read_ref(&scratch[off..], id_size as usize);
if ref_val != 0 {
if let Some(dst) = cache.index_of(id_map, ref_val) {
inb_flat.set(in_cursors[dst] as usize, src_idx as u32);
in_cursors[dst] += 1;
fwd.push(dst as u32);
}
}
}
}
}
}
on_outbound(src_idx, fwd)?;
}
heap::OBJ_ARRAY_DUMP => {
let addr = try_read!(r.id());
try_read!(r.skip(4)); let count = try_read!(r.u4()) as u64;
let _elem_class_id = try_read!(r.id());
let byte_len = count.saturating_mul(ids);
try_read!(r.read_bytes_reuse(scratch, byte_len as usize));
checked_sub!(remaining, ids + 4 + 4 + ids + byte_len);
let src_idx = match id_map.index_of(addr) {
Some(i) => i,
None => continue,
};
let mut fwd: Vec<u32> = Vec::new();
for i in 0..count as usize {
let off = i * id_size as usize;
if off + id_size as usize <= scratch.len() {
let ref_val = super::read_ref(&scratch[off..], id_size as usize);
if ref_val != 0 {
if let Some(dst) = cache.index_of(id_map, ref_val) {
inb_flat.set(in_cursors[dst] as usize, src_idx as u32);
in_cursors[dst] += 1;
fwd.push(dst as u32);
}
}
}
}
on_outbound(src_idx, fwd)?;
}
heap::PRIM_ARRAY_NODATA_DUMP => {
try_read!(r.skip(ids + 4 + 4 + 1));
checked_sub!(remaining, ids + 4 + 4 + 1);
}
heap::PRIM_ARRAY_DUMP => {
let addr = try_read!(r.id());
try_read!(r.skip(4));
let count = try_read!(r.u4()) as u64;
let elem_type = try_read!(r.u1());
let elem_size = crate::types::HprofType::from_code(elem_type)
.map(|t| t.byte_size() as u64)
.unwrap_or(1);
let byte_len = count.saturating_mul(elem_size);
try_read!(r.skip(byte_len));
checked_sub!(remaining, ids + 4 + 4 + 1 + byte_len);
if let Some(src_idx) = id_map.index_of(addr) {
on_outbound(src_idx, Vec::new())?;
}
}
heap::HEAP_DUMP_INFO => {
try_read!(r.skip(4 + ids));
checked_sub!(remaining, 4 + ids);
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unknown heap sub-tag {sub_tag:#x}"),
));
}
}
}
Ok(())
}
}
pub fn rescan_outbound(
ctx: &MatOutboundRescanCtx,
fwd_off: &mut Vec<u32>,
fwd_tgt: &mut Vec<u32>,
) -> io::Result<()> {
use crate::types::tags;
use std::io::ErrorKind;
let id_map = match ctx.id_map_c.as_ref() {
Some((blob, len)) => crate::id_map::IdMap::from_compressed(blob, *len, ctx.id_map_codec)?,
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"rescan_outbound: id_map not compressed (unexpected)",
));
}
};
let ids = ctx.id_size as u64;
let mut scratch: Vec<u8> = Vec::with_capacity(4096);
let mut cache = crate::id_map::IndexCache::new();
let mut r = ctx.source.open()?;
loop {
let tag = match r.u1() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
};
let _ts = match r.u4() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
};
let length = match r.u4() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
} as u64;
match tag {
tags::HEAP_DUMP | tags::HEAP_DUMP_SEGMENT => {
scan_fwd_segment(
&mut r,
ctx.id_size,
ids,
length,
&id_map,
&ctx.class_addr_to_hist,
&ctx.field_plans_dense,
fwd_off,
fwd_tgt,
&mut scratch,
&mut cache,
)?;
}
tags::HEAP_DUMP_END => break,
_ => match r.skip(length) {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
},
}
}
Ok(())
}
fn skip_class_dump(r: &mut HprofReader, id_size: u8, ids: u64) -> io::Result<u64> {
use super::scan::value_size;
let mut consumed = 0u64;
r.skip(ids + 4 + ids + ids + ids * 4 + 4)?;
consumed += ids + 4 + ids + ids + ids * 4 + 4;
let cp = r.u2()? as u64;
consumed += 2;
for _ in 0..cp {
r.skip(2)?;
consumed += 2; let tp = r.u1()?;
consumed += 1;
let vs = value_size(tp, id_size);
r.skip(vs)?;
consumed += vs;
}
let sc = r.u2()? as u64;
consumed += 2;
for _ in 0..sc {
r.skip(ids)?;
consumed += ids; let tp = r.u1()?;
consumed += 1;
let vs = value_size(tp, id_size);
r.skip(vs)?;
consumed += vs;
}
let ic = r.u2()? as u64;
consumed += 2;
let ic_skip = ic.saturating_mul(ids + 1);
r.skip(ic_skip)?;
consumed += ic_skip;
Ok(consumed)
}
#[allow(clippy::too_many_arguments)]
fn scan_fwd_segment(
r: &mut HprofReader,
id_size: u8,
ids: u64,
mut remaining: u64,
id_map: &crate::id_map::IdMap,
class_addr_to_hist: &HashMap<u64, u32>,
field_plans_dense: &[super::FieldPlan],
fwd_off: &mut Vec<u32>,
fwd_tgt: &mut Vec<u32>,
scratch: &mut Vec<u8>,
cache: &mut crate::id_map::IndexCache,
) -> io::Result<()> {
use crate::types::heap;
macro_rules! checked_sub {
($rem:expr, $sz:expr) => {
$rem = $rem
.checked_sub($sz)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "segment overrun"))?;
};
}
while remaining > 0 {
let sub_tag = r.u1()?;
checked_sub!(remaining, 1u64);
match sub_tag {
heap::ROOT_SYSTEM_CLASS
| heap::ROOT_UNKNOWN
| heap::ROOT_MONITOR_USED
| heap::ROOT_INTERNED_STRING
| heap::ROOT_DEBUGGER
| heap::ROOT_VM_INTERNAL => {
r.skip(ids)?;
checked_sub!(remaining, ids);
}
heap::ROOT_JNI_GLOBAL => {
r.skip(2 * ids)?;
checked_sub!(remaining, 2 * ids);
}
heap::ROOT_JNI_LOCAL | heap::ROOT_JAVA_FRAME | heap::ROOT_JNI_MONITOR => {
r.skip(ids + 8)?;
checked_sub!(remaining, ids + 8);
}
heap::ROOT_NATIVE_STACK | heap::ROOT_THREAD_BLOCK => {
r.skip(ids + 4)?;
checked_sub!(remaining, ids + 4);
}
heap::ROOT_STICKY_CLASS | heap::ROOT_THREAD_OBJ => {
let skip = if sub_tag == heap::ROOT_THREAD_OBJ {
ids + 8
} else {
ids
};
r.skip(skip)?;
checked_sub!(remaining, skip);
}
heap::CLASS_DUMP => {
let consumed = skip_class_dump(r, id_size, ids)?;
checked_sub!(remaining, consumed);
}
heap::INSTANCE_DUMP => {
let addr = r.id()?;
r.skip(4)?;
let class_id = r.id()?;
let data_len = r.u4()? as u64;
r.read_bytes_reuse(scratch, data_len as usize)?;
checked_sub!(remaining, ids + 4 + ids + 4 + data_len);
let src_idx = match id_map.index_of(addr) {
Some(i) => i,
None => continue,
};
if let Some(dst) = cache.index_of(id_map, class_id) {
scatter_edge(fwd_off, fwd_tgt, src_idx, dst);
}
if let Some(&cidx) = class_addr_to_hist.get(&class_id) {
if (cidx as usize) < field_plans_dense.len() {
for &(off, _excluded) in &field_plans_dense[cidx as usize] {
let off = off as usize;
if off + id_size as usize <= scratch.len() {
let ref_val = super::read_ref(&scratch[off..], id_size as usize);
if ref_val != 0 {
if let Some(dst) = cache.index_of(id_map, ref_val) {
scatter_edge(fwd_off, fwd_tgt, src_idx, dst);
}
}
}
}
}
}
}
heap::OBJ_ARRAY_DUMP => {
let addr = r.id()?;
r.skip(4)?;
let count = r.u4()? as u64;
let _elem_class_id = r.id()?;
let byte_len = count.saturating_mul(ids);
r.read_bytes_reuse(scratch, byte_len as usize)?;
checked_sub!(remaining, ids + 4 + 4 + ids + byte_len);
let src_idx = match id_map.index_of(addr) {
Some(i) => i,
None => continue,
};
for i in 0..count as usize {
let off = i * id_size as usize;
if off + id_size as usize <= scratch.len() {
let ref_val = super::read_ref(&scratch[off..], id_size as usize);
if ref_val != 0 {
if let Some(dst) = cache.index_of(id_map, ref_val) {
scatter_edge(fwd_off, fwd_tgt, src_idx, dst);
}
}
}
}
}
heap::PRIM_ARRAY_NODATA_DUMP => {
r.skip(ids + 4 + 4 + 1)?;
checked_sub!(remaining, ids + 4 + 4 + 1);
}
heap::PRIM_ARRAY_DUMP => {
let addr = r.id()?;
r.skip(4)?;
let count = r.u4()? as u64;
let elem_type = r.u1()?;
let elem_size = crate::types::HprofType::from_code(elem_type)
.map(|t| t.byte_size() as u64)
.unwrap_or(1);
r.skip(count.saturating_mul(elem_size))?;
checked_sub!(remaining, ids + 4 + 4 + 1 + count.saturating_mul(elem_size));
let _ = addr; }
heap::HEAP_DUMP_INFO => {
r.skip(4 + ids)?;
checked_sub!(remaining, 4 + ids);
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unknown heap sub-tag {sub_tag:#x}"),
));
}
}
}
Ok(())
}
#[inline]
fn scatter_edge(fwd_off: &mut Vec<u32>, fwd_tgt: &mut Vec<u32>, src_idx: usize, dst: usize) {
let pos = fwd_off[src_idx] as usize;
if pos < fwd_tgt.len() {
fwd_tgt[pos] = dst as u32;
fwd_off[src_idx] += 1;
}
}
#[cfg(test)]
mod obj_graph_tests {
use super::*;
fn make_graph(fwd_offsets: Vec<u32>, fwd_targets_data: Vec<u32>) -> Graph {
let n = fwd_offsets.len() - 1;
Graph {
n,
format: String::new(),
file_size: 0,
source_name: String::new(),
file_path: String::new(),
id_size: 4,
ref_size: 4,
header_timestamp_ms: 0,
gc_root_indices: vec![],
gc_root_types: vec![],
shallow: vec![100u32; n],
class_idx: vec![],
class_names: vec![],
class_loader_id: vec![],
loader_labels: Default::default(),
thread_stacks: vec![],
thread_props: Default::default(),
thread_local_counts: Default::default(),
thread_local_samples: Default::default(),
thread_local_frame_samples: Default::default(),
system_properties: vec![],
jvm_version: None,
class_obj_class_idx: Default::default(),
fwd_offsets,
fwd_targets: crate::chunkvec::ChunkU32::from_vec(fwd_targets_data),
synthetic_root_count: 0,
system_classloader_shallow: None,
idom: vec![],
retained: vec![],
has_same_class_ancestor: crate::bitset::Bitset::default(),
alloc_stack_serial: vec![],
alloc_frames_by_serial: None,
record_census: Default::default(),
dup_strings: None,
dup_prim_arrays: None,
boxed_number_holders: vec![],
arrays_by_size: Default::default(),
incoming_refs_per_class: vec![],
collections: Default::default(),
references: Default::default(),
reference_referent_idx: Default::default(),
reference_null_referent_count: Default::default(),
collection_attribution_raw: None,
collection_attribution_truncated: false,
fields_by_size_raw: None,
coll_values_raw: None,
node_kv: None,
fwd_field_name_idx: None,
field_name_pool: None,
direct_byte_buffer_capacity_sum: 0,
thread_local_null_key_count: 0,
tl_entry_records: vec![],
unreachable_retained: None,
obj_graph_edges: None,
type_ref_pairs: None,
type_ref_pair_fields: None,
class_ref_field_names: vec![],
}
}
#[test]
fn inbound_edges_captured_for_captured_dst() {
let g = make_graph(vec![0, 2, 3, 3], vec![1, 2, 2]);
let cap = capture_obj_graph_edges(&g, usize::MAX, 100);
let inb2 = cap.inbound_of(2);
let srcs: Vec<u32> = inb2.iter().map(|&(s, _)| s).collect();
assert!(srcs.contains(&0), "0→2 should be captured as inbound");
assert!(srcs.contains(&1), "1→2 should be captured as inbound");
let inb1 = cap.inbound_of(1);
assert_eq!(inb1.len(), 1);
assert_eq!(inb1[0].0, 0);
}
#[test]
fn inbound_truncated_at_edge_cap() {
let g = make_graph(vec![0, 3, 3], vec![1, 1, 1]);
let cap = capture_obj_graph_edges(&g, usize::MAX, 2);
assert!(
cap.inbound_truncated.get(1),
"inbound for 1 should be truncated"
);
assert_eq!(cap.inbound_of(1).len(), 2);
}
#[test]
fn inbound_not_captured_for_uncaptured_dst() {
let g = make_graph(vec![0, 1, 1], vec![1]);
let cap = capture_obj_graph_edges(&g, 1, 100);
assert_eq!(cap.inbound_of(1).len(), 0);
}
}