use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::io::{self, ErrorKind};
use crate::{
reader::HprofReader,
types::{HprofType, heap, tags},
};
use super::scan::{skip_class_dump, sub_remaining};
const DUP_PRIM_TOP_N: usize = 10;
const DUP_ARRAY_HOLDER_TOP_N: usize = 20;
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct DupPrimArrayRow {
pub array_class: String,
pub duplicated_groups: u64,
pub wasted_bytes: u64,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct DupArrayHolder {
pub class_name: String,
pub array_refs: u64,
}
#[derive(
Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct DupPrimArrays {
pub total_wasted_bytes: u64,
pub rows: Vec<DupPrimArrayRow>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub top_array_holders: Vec<DupArrayHolder>,
}
fn elem_type_name(code: u8) -> &'static str {
match code {
4 => "boolean[]",
5 => "char[]",
6 => "float[]",
7 => "double[]",
8 => "byte[]",
9 => "short[]",
10 => "int[]",
11 => "long[]",
_ => "unknown[]",
}
}
pub(crate) fn compute_dup_prim_arrays<O>(
open: O,
id_size: u8,
) -> io::Result<(DupPrimArrays, HashSet<u64>)>
where
O: Fn() -> io::Result<HprofReader>,
{
let ids = id_size as u64;
let mut r = open()?;
let mut scratch: Vec<u8> = Vec::with_capacity(4096);
let mut hash_map: HashMap<u64, (u32, u64, u8)> = HashMap::new();
let mut addr_to_hash: HashMap<u64, u64> = HashMap::new();
loop {
let tag = match r.u1() {
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
other => other?,
};
let _ts = r.u4()?;
let length = r.u4()? as u64;
match tag {
tags::HEAP_DUMP | tags::HEAP_DUMP_SEGMENT => {
let mut remaining = length;
while remaining > 0 {
let sub_tag = r.u1()?;
sub_remaining(&mut remaining, 1)?;
match sub_tag {
heap::ROOT_SYSTEM_CLASS
| heap::ROOT_UNKNOWN
| heap::ROOT_MONITOR_USED
| heap::ROOT_STICKY_CLASS
| heap::ROOT_INTERNED_STRING
| heap::ROOT_DEBUGGER
| heap::ROOT_VM_INTERNAL => {
r.skip(ids)?;
sub_remaining(&mut remaining, ids)?;
}
heap::ROOT_JNI_GLOBAL => {
r.skip(2 * ids)?;
sub_remaining(&mut remaining, 2 * ids)?;
}
heap::ROOT_JNI_LOCAL
| heap::ROOT_JAVA_FRAME
| heap::ROOT_JNI_MONITOR
| heap::ROOT_THREAD_OBJ => {
r.skip(ids + 8)?;
sub_remaining(&mut remaining, ids + 8)?;
}
heap::ROOT_NATIVE_STACK | heap::ROOT_THREAD_BLOCK => {
r.skip(ids + 4)?;
sub_remaining(&mut remaining, ids + 4)?;
}
heap::HEAP_DUMP_INFO => {
r.skip(4 + ids)?;
sub_remaining(&mut remaining, 4 + ids)?;
}
heap::CLASS_DUMP => {
let consumed = skip_class_dump(&mut r, id_size)?;
sub_remaining(&mut remaining, consumed)?;
}
heap::INSTANCE_DUMP => {
r.skip(ids + 4)?;
let _class_id = r.id()?;
let data_len = r.u4()? as u64;
r.skip(data_len)?;
sub_remaining(&mut remaining, ids + 4 + ids + 4 + data_len)?;
}
heap::OBJ_ARRAY_DUMP => {
r.skip(ids + 4)?;
let count = r.u4()? as u64;
r.skip(ids)?;
let byte_len = count.saturating_mul(ids);
r.skip(byte_len)?;
sub_remaining(&mut remaining, ids + 4 + 4 + ids + byte_len)?;
}
heap::PRIM_ARRAY_NODATA_DUMP => {
r.skip(ids + 4 + 4 + 1)?;
sub_remaining(&mut remaining, ids + 4 + 4 + 1)?;
}
heap::PRIM_ARRAY_DUMP => {
let obj_addr = r.id()?;
r.skip(4)?; let count = r.u4()? as u64;
let elem_type = r.u1()?;
let esz = HprofType::from_code(elem_type)
.map(|t| t.byte_size() as u64)
.unwrap_or(1);
let byte_len = count.saturating_mul(esz);
sub_remaining(&mut remaining, ids + 4 + 4 + 1 + byte_len)?;
r.read_bytes_reuse(&mut scratch, byte_len as usize)?;
let mut h = std::collections::hash_map::DefaultHasher::new();
elem_type.hash(&mut h);
scratch.hash(&mut h);
let hv = h.finish();
let e = hash_map.entry(hv).or_insert((0, byte_len, elem_type));
e.0 = e.0.saturating_add(1);
addr_to_hash.insert(obj_addr, hv);
}
other => {
return Err(io::Error::new(
ErrorKind::InvalidData,
format!(
"unknown heap sub-tag 0x{other:02x} in dup-prim-arrays scan"
),
));
}
}
}
}
tags::HEAP_DUMP_END => break,
_ => r.skip(length)?,
}
}
let mut by_type: HashMap<u8, (u64, u64)> = HashMap::new();
let mut total_wasted: u64 = 0;
let mut dup_hashes: HashSet<u64> = HashSet::new();
for (&hv, &(count, shallow, elem_type)) in &hash_map {
if count <= 1 {
continue;
}
dup_hashes.insert(hv);
let wasted = (count as u64).saturating_sub(1).saturating_mul(shallow);
total_wasted = total_wasted.saturating_add(wasted);
let e = by_type.entry(elem_type).or_insert((0, 0));
e.0 = e.0.saturating_add(wasted);
e.1 = e.1.saturating_add(1);
}
let dup_addrs: HashSet<u64> = addr_to_hash
.into_iter()
.filter(|(_, hv)| dup_hashes.contains(hv))
.map(|(addr, _)| addr)
.collect();
let mut rows: Vec<DupPrimArrayRow> = by_type
.into_iter()
.map(
|(code, (wasted_bytes, duplicated_groups))| DupPrimArrayRow {
array_class: elem_type_name(code).to_string(),
duplicated_groups,
wasted_bytes,
},
)
.collect();
rows.sort_unstable_by(|a, b| {
b.wasted_bytes
.cmp(&a.wasted_bytes)
.then(a.array_class.cmp(&b.array_class))
});
rows.truncate(DUP_PRIM_TOP_N);
Ok((
DupPrimArrays {
total_wasted_bytes: total_wasted,
rows,
top_array_holders: Vec::new(),
},
dup_addrs,
))
}
pub(crate) fn compute_dup_array_holders<O>(
open: O,
p1: &crate::pass1::Pass1,
dup_addrs: &HashSet<u64>,
id_size: u8,
) -> io::Result<Vec<DupArrayHolder>>
where
O: Fn() -> io::Result<HprofReader>,
{
use super::strings::scan_all_instances;
use super::{build_field_plans, read_ref};
let obj_ref_width = id_size as usize;
let field_plans = build_field_plans(&p1.class_map, &p1.strings, id_size as usize);
let mut class_counter: HashMap<u64, u64> = HashMap::new();
scan_all_instances(&open, id_size, |_obj_addr, class_id, blob| {
let Some(plan) = field_plans.get(&class_id) else {
return;
};
let mut hits: u64 = 0;
for &(offset, _excluded) in plan {
let off = offset as usize;
if off + obj_ref_width > blob.len() {
continue;
}
let r = read_ref(&blob[off..], obj_ref_width);
if r != 0 && dup_addrs.contains(&r) {
hits += 1;
}
}
if hits > 0 {
*class_counter.entry(class_id).or_insert(0) += hits;
}
})?;
let class_map = &p1.class_map;
let strings = &p1.strings;
let mut holders: Vec<DupArrayHolder> = class_counter
.into_iter()
.map(|(class_addr, array_refs)| {
let class_name = class_map
.get(&class_addr)
.and_then(|ci| strings.get(&ci.name_id))
.map(|s| s.replace('/', "."))
.unwrap_or_else(|| format!("0x{class_addr:x}"));
DupArrayHolder {
class_name,
array_refs,
}
})
.collect();
holders.sort_unstable_by(|a, b| {
b.array_refs
.cmp(&a.array_refs)
.then(a.class_name.cmp(&b.class_name))
});
holders.truncate(DUP_ARRAY_HOLDER_TOP_N);
Ok(holders)
}