use std::collections::HashMap;
use std::io;
use crate::{pass1::Pass1, types::HprofType};
use super::{
ThreadProps, ThreadStack, collect_blobs, decode_java_string, field_offset, read_ref,
scan_class_dumps,
};
type HolderOffsets = (Option<usize>, Option<usize>, Option<usize>);
pub(crate) fn build_thread_stacks(p1: &Pass1) -> Vec<ThreadStack> {
let resolve = |id: u64| -> Option<&str> { p1.strings.get(&id).map(|s| s.as_str()) };
let class_name_of = |serial: u32| -> Option<&str> {
let addr = *p1.class_serial_to_addr.get(&serial)?;
let ci = p1.class_map.get(&addr)?;
p1.strings.get(&ci.name_id).map(|s| s.as_str())
};
let mut out: Vec<ThreadStack> = Vec::new();
for (&stack_serial, frame_ids) in p1.stack_traces.iter() {
if frame_ids.is_empty() {
continue;
}
let thread_serial = p1
.stack_trace_thread
.get(&stack_serial)
.copied()
.unwrap_or(0);
let thread_obj_idx = p1
.thread_serial_to_obj_id
.get(&thread_serial)
.and_then(|&addr| p1.id_map.index_of(addr))
.map(|i| i as u32)
.unwrap_or(u32::MAX);
let mut frames = Vec::with_capacity(frame_ids.len());
for &fid in frame_ids {
let Some(f) = p1.stack_frames.get(&fid) else {
frames.push(format!("<unknown frame {fid:#x}>"));
continue;
};
let class = class_name_of(f.class_serial).map(pretty_binary_name);
let method = resolve(f.method_name_id);
let source = resolve(f.source_file_id);
frames.push(render_frame(
class.as_deref(),
method,
source,
f.class_serial,
f.line_number,
));
}
out.push(ThreadStack {
thread_serial,
thread_obj_idx,
frames,
});
}
out.sort_by_key(|t| t.thread_serial);
out
}
pub(crate) fn resolve_alloc_frames(p1: &Pass1) -> std::collections::HashMap<u32, Vec<String>> {
let resolve = |id: u64| -> Option<&str> { p1.strings.get(&id).map(|s| s.as_str()) };
let class_name_of = |serial: u32| -> Option<&str> {
let addr = *p1.class_serial_to_addr.get(&serial)?;
let ci = p1.class_map.get(&addr)?;
p1.strings.get(&ci.name_id).map(|s| s.as_str())
};
let mut distinct: std::collections::HashSet<u32> = std::collections::HashSet::new();
for &s in &p1.alloc_stack_serial {
if s != 0 {
distinct.insert(s);
}
}
let mut map: std::collections::HashMap<u32, Vec<String>> =
std::collections::HashMap::with_capacity(distinct.len());
for &serial in &distinct {
let frames = match p1.stack_traces.get(&serial) {
Some(frame_ids) => {
let mut frames = Vec::with_capacity(frame_ids.len());
for &fid in frame_ids {
let Some(f) = p1.stack_frames.get(&fid) else {
frames.push(format!("<unknown frame {fid:#x}>"));
continue;
};
let class = class_name_of(f.class_serial).map(pretty_binary_name);
let method = resolve(f.method_name_id);
let source = resolve(f.source_file_id);
frames.push(render_frame(
class.as_deref(),
method,
source,
f.class_serial,
f.line_number,
));
}
frames
}
None => Vec::new(),
};
map.insert(serial, frames);
}
map
}
pub(crate) fn resolve_thread_names<O>(
open: O,
p1: &Pass1,
prefetched_thread_blobs: HashMap<u64, (u64, Vec<u8>)>,
) -> io::Result<HashMap<u32, ThreadProps>>
where
O: Fn() -> io::Result<crate::reader::HprofReader>,
{
let mut props: HashMap<u32, ThreadProps> = HashMap::new();
if p1.thread_serial_to_obj_id.is_empty() {
return Ok(props);
}
let id_size = p1.id_size;
let obj_ref_width = id_size as usize;
let class_map = &p1.class_map;
let strings = &p1.strings;
let read_i32 = |blob: &[u8], o: usize| -> Option<i32> {
blob.get(o..o + 4)
.map(|b| i32::from_be_bytes([b[0], b[1], b[2], b[3]]))
};
let mut inst_blobs_r1 = prefetched_thread_blobs;
let missing_threads: std::collections::HashSet<u64> = p1
.thread_serial_to_obj_id
.values()
.copied()
.filter(|a| !inst_blobs_r1.contains_key(a))
.collect();
if !missing_threads.is_empty() {
let (extra, _, _) = collect_blobs(
&open,
id_size,
&missing_threads,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
)?;
inst_blobs_r1.extend(extra);
}
let mut thread_to_name_addr: HashMap<u64, u64> = HashMap::new();
let mut thread_to_scalars: HashMap<u64, (bool, i32, i32, u64)> = HashMap::new();
let mut thread_to_holder: HashMap<u64, u64> = HashMap::new();
type ThreadOffs = (
Option<usize>,
Option<usize>,
Option<usize>,
Option<usize>,
Option<usize>,
Option<usize>,
);
let mut off_cache: HashMap<u64, ThreadOffs> = HashMap::new();
for (&addr, &(class_id, ref blob)) in &inst_blobs_r1 {
let offs = *off_cache.entry(class_id).or_insert_with(|| {
let obj_off = |name: &str| match field_offset(
class_id,
name,
"java/lang/Thread",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Object)) => Some(off as usize),
_ => None,
};
let name_off = obj_off("name");
let daemon_off = match field_offset(
class_id,
"daemon",
"java/lang/Thread",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Boolean)) => Some(off as usize),
_ => None,
};
let priority_off = match field_offset(
class_id,
"priority",
"java/lang/Thread",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Int)) => Some(off as usize),
_ => None,
};
let status_off = match field_offset(
class_id,
"threadStatus",
"java/lang/Thread",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Int)) => Some(off as usize),
_ => None,
};
let ctx_off = obj_off("contextClassLoader");
let holder_off = obj_off("holder");
(
name_off,
daemon_off,
priority_off,
status_off,
ctx_off,
holder_off,
)
});
let (name_off, daemon_off, priority_off, status_off, ctx_off, holder_off) = offs;
if let Some(off) = name_off {
if off + obj_ref_width <= blob.len() {
let name_ref = read_ref(&blob[off..], obj_ref_width);
if name_ref != 0 {
thread_to_name_addr.insert(addr, name_ref);
}
}
}
let is_daemon = daemon_off
.and_then(|o| blob.get(o))
.map(|&b| b != 0)
.unwrap_or(false);
let priority = priority_off.and_then(|o| read_i32(blob, o)).unwrap_or(0);
let thread_status = status_off.and_then(|o| read_i32(blob, o)).unwrap_or(0);
let context_loader_addr = ctx_off
.filter(|&o| o + obj_ref_width <= blob.len())
.map(|o| read_ref(&blob[o..], obj_ref_width))
.unwrap_or(0);
thread_to_scalars.insert(
addr,
(is_daemon, priority, thread_status, context_loader_addr),
);
if priority_off.is_none() && daemon_off.is_none() && status_off.is_none() {
if let Some(off) = holder_off {
if off + obj_ref_width <= blob.len() {
let href = read_ref(&blob[off..], obj_ref_width);
if href != 0 {
thread_to_holder.insert(addr, href);
}
}
}
}
}
drop(inst_blobs_r1);
let wanted_strings: std::collections::HashSet<u64> =
thread_to_name_addr.values().copied().collect();
let wanted_holders: std::collections::HashSet<u64> =
thread_to_holder.values().copied().collect();
let wanted_inst_r2: std::collections::HashSet<u64> = wanted_strings
.iter()
.chain(wanted_holders.iter())
.copied()
.collect();
let mut string_to_arr: HashMap<u64, (u64, u8)> = HashMap::new();
let mut holder_scalars: HashMap<u64, (bool, i32, i32)> = HashMap::new();
if !wanted_inst_r2.is_empty() {
let (inst_blobs_r2, _, _) = collect_blobs(
&open,
id_size,
&wanted_inst_r2,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
)?;
let mut str_off_cache: HashMap<u64, Option<(usize, Option<usize>)>> = HashMap::new();
let mut holder_off_cache: HashMap<u64, HolderOffsets> = HashMap::new();
for (&addr, &(class_id, ref blob)) in &inst_blobs_r2 {
if wanted_strings.contains(&addr) {
let offs = *str_off_cache.entry(class_id).or_insert_with(|| {
let value_off = match field_offset(
class_id,
"value",
"java/lang/String",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Object)) => off as usize,
_ => return None,
};
let coder_off = match field_offset(
class_id,
"coder",
"java/lang/String",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Byte)) => Some(off as usize),
_ => None,
};
Some((value_off, coder_off))
});
if let Some((value_off, coder_off)) = offs {
if value_off + obj_ref_width <= blob.len() {
let arr_ref = read_ref(&blob[value_off..], obj_ref_width);
let coder = match coder_off {
Some(co) if co < blob.len() => blob[co],
_ => 1, };
if arr_ref != 0 {
string_to_arr.insert(addr, (arr_ref, coder));
}
}
}
} else if wanted_holders.contains(&addr) {
let (daemon_off, priority_off, status_off) =
*holder_off_cache.entry(class_id).or_insert_with(|| {
let int_off = |name: &str| match field_offset(
class_id,
name,
"java/lang/Thread$FieldHolder",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Int)) => Some(off as usize),
_ => None,
};
let daemon_off = match field_offset(
class_id,
"daemon",
"java/lang/Thread$FieldHolder",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Boolean)) => Some(off as usize),
_ => None,
};
(daemon_off, int_off("priority"), int_off("threadStatus"))
});
let is_daemon = daemon_off
.and_then(|o| blob.get(o))
.map(|&b| b != 0)
.unwrap_or(false);
let priority = priority_off.and_then(|o| read_i32(blob, o)).unwrap_or(0);
let thread_status = status_off.and_then(|o| read_i32(blob, o)).unwrap_or(0);
holder_scalars.insert(addr, (is_daemon, priority, thread_status));
}
}
}
for (&thread_addr, &holder_addr) in &thread_to_holder {
if let Some(&(d, p, s)) = holder_scalars.get(&holder_addr) {
if let Some(entry) = thread_to_scalars.get_mut(&thread_addr) {
entry.0 = d;
entry.1 = p;
entry.2 = s;
}
}
}
for (&serial, &thread_addr) in &p1.thread_serial_to_obj_id {
if let Some(&(is_daemon, priority, thread_status, ctx)) =
thread_to_scalars.get(&thread_addr)
{
props.insert(
serial,
ThreadProps {
name: String::new(),
is_daemon,
priority,
thread_status,
context_loader_addr: ctx,
},
);
}
}
if string_to_arr.is_empty() {
return Ok(props);
}
let wanted_arrays: std::collections::HashSet<u64> =
string_to_arr.values().map(|&(a, _)| a).collect();
let (_, arr_blobs, _) = collect_blobs(
&open,
id_size,
&std::collections::HashSet::new(),
&wanted_arrays,
&std::collections::HashSet::new(),
)?;
for (&serial, &thread_addr) in &p1.thread_serial_to_obj_id {
let Some(&name_addr) = thread_to_name_addr.get(&thread_addr) else {
continue;
};
let Some(&(arr_addr, coder)) = string_to_arr.get(&name_addr) else {
continue;
};
let Some(bytes) = arr_blobs.get(&arr_addr) else {
continue;
};
let text = decode_java_string(bytes, coder);
if !text.is_empty() {
props.entry(serial).or_default().name = text;
}
}
Ok(props)
}
pub(crate) const MAX_PROP_ENTRIES: usize = 4096;
pub(crate) type SystemProps = (Vec<(String, String)>, Option<String>);
pub(crate) fn resolve_system_properties<O>(
open: O,
p1: &Pass1,
prefetched_props_addr: u64,
) -> io::Result<SystemProps>
where
O: Fn() -> io::Result<crate::reader::HprofReader>,
{
let empty = (Vec::new(), None);
let id_size = p1.id_size;
let obj_ref_width = id_size as usize;
let class_map = &p1.class_map;
let strings = &p1.strings;
let props_addr = if prefetched_props_addr != 0 {
prefetched_props_addr
} else {
let mut found: u64 = 0;
scan_class_dumps(&open, id_size, |class_obj_id, statics| {
if found != 0 {
return;
}
let cname = class_map
.get(&class_obj_id)
.and_then(|ci| strings.get(&ci.name_id))
.map(|s| s.as_str())
.unwrap_or("");
if cname != "java/lang/System" {
return;
}
for &(name_id, type_code, value) in statics {
if HprofType::from_code(type_code) != Some(HprofType::Object) {
continue;
}
let fname = strings.get(&name_id).map(|s| s.as_str()).unwrap_or("");
if fname == "props" && value != 0 {
found = value;
}
}
})?;
found
};
if props_addr == 0 {
return Ok(empty);
}
let wanted_inst_r1: std::collections::HashSet<u64> = std::iter::once(props_addr).collect();
let (inst_blobs_r1, _, _) = collect_blobs(
&open,
id_size,
&wanted_inst_r1,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
)?;
let mut table_addr: u64 = 0;
if let Some(&(class_id, ref blob)) = inst_blobs_r1.get(&props_addr) {
let off = match field_offset(
class_id,
"table",
"java/util/Hashtable",
class_map,
strings,
obj_ref_width,
) {
Some((o, HprofType::Object)) => o as usize,
_ => 0,
};
if off != 0 && off + obj_ref_width <= blob.len() {
table_addr = read_ref(&blob[off..], obj_ref_width);
}
}
if table_addr == 0 {
return Ok(empty);
}
let wanted_obj_r2: std::collections::HashSet<u64> = std::iter::once(table_addr).collect();
let (_, _, obj_blobs_r2) = collect_blobs(
&open,
id_size,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&wanted_obj_r2,
)?;
let mut entry_addrs: Vec<u64> = Vec::new();
if let Some(elem_bytes) = obj_blobs_r2.get(&table_addr) {
for chunk in elem_bytes.chunks_exact(obj_ref_width) {
if entry_addrs.len() >= MAX_PROP_ENTRIES {
break;
}
let r = read_ref(chunk, obj_ref_width);
if r != 0 {
entry_addrs.push(r);
}
}
}
if entry_addrs.is_empty() {
return Ok(empty);
}
let wanted_entries: std::collections::HashSet<u64> = entry_addrs.iter().copied().collect();
let (entry_blobs, _, _) = collect_blobs(
&open,
id_size,
&wanted_entries,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
)?;
let mut all_entry_blobs = entry_blobs;
let mut entry_off_cache: HashMap<u64, Option<(usize, usize, usize)>> = HashMap::new();
let mut chained_addrs: std::collections::HashSet<u64> = std::collections::HashSet::new();
for (&_addr, &(class_id, ref blob)) in &all_entry_blobs {
let offs = entry_off_cache.entry(class_id).or_insert_with(|| {
let key_off = match field_offset(
class_id,
"key",
"java/util/Hashtable$Entry",
class_map,
strings,
obj_ref_width,
) {
Some((o, HprofType::Object)) => o as usize,
_ => return None,
};
let value_off = match field_offset(
class_id,
"value",
"java/util/Hashtable$Entry",
class_map,
strings,
obj_ref_width,
) {
Some((o, HprofType::Object)) => o as usize,
_ => return None,
};
let next_off = match field_offset(
class_id,
"next",
"java/util/Hashtable$Entry",
class_map,
strings,
obj_ref_width,
) {
Some((o, HprofType::Object)) => o as usize,
_ => return None,
};
Some((key_off, value_off, next_off))
});
if let Some((_, _, next_off)) = *offs {
if next_off + obj_ref_width <= blob.len() {
let next_ref = read_ref(&blob[next_off..], obj_ref_width);
if next_ref != 0 && !all_entry_blobs.contains_key(&next_ref) {
chained_addrs.insert(next_ref);
}
}
}
}
let mut depth = 0u32;
while !chained_addrs.is_empty() && all_entry_blobs.len() < MAX_PROP_ENTRIES && depth < 64 {
depth += 1;
let (more_blobs, _, _) = collect_blobs(
&open,
id_size,
&chained_addrs,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
)?;
chained_addrs.clear();
for (&_addr, &(class_id, ref blob)) in &more_blobs {
let offs = entry_off_cache.entry(class_id).or_insert_with(|| {
let key_off = match field_offset(
class_id,
"key",
"java/util/Hashtable$Entry",
class_map,
strings,
obj_ref_width,
) {
Some((o, HprofType::Object)) => o as usize,
_ => return None,
};
let value_off = match field_offset(
class_id,
"value",
"java/util/Hashtable$Entry",
class_map,
strings,
obj_ref_width,
) {
Some((o, HprofType::Object)) => o as usize,
_ => return None,
};
let next_off = match field_offset(
class_id,
"next",
"java/util/Hashtable$Entry",
class_map,
strings,
obj_ref_width,
) {
Some((o, HprofType::Object)) => o as usize,
_ => return None,
};
Some((key_off, value_off, next_off))
});
if let Some((_, _, next_off)) = *offs {
if next_off + obj_ref_width <= blob.len() {
let next_ref = read_ref(&blob[next_off..], obj_ref_width);
if next_ref != 0
&& !all_entry_blobs.contains_key(&next_ref)
&& all_entry_blobs.len() + chained_addrs.len() < MAX_PROP_ENTRIES
{
chained_addrs.insert(next_ref);
}
}
}
}
all_entry_blobs.extend(more_blobs);
}
let mut key_val: HashMap<u64, (u64, u64)> = HashMap::new();
for (&addr, &(class_id, ref blob)) in &all_entry_blobs {
let Some(&Some((key_off, value_off, _next_off))) = entry_off_cache.get(&class_id) else {
continue;
};
if key_off + obj_ref_width > blob.len() || value_off + obj_ref_width > blob.len() {
continue;
}
let key_ref = read_ref(&blob[key_off..], obj_ref_width);
let value_ref = read_ref(&blob[value_off..], obj_ref_width);
key_val.insert(addr, (key_ref, value_ref));
}
drop(all_entry_blobs);
if key_val.is_empty() {
return Ok(empty);
}
let mut wanted_strings: std::collections::HashSet<u64> = std::collections::HashSet::new();
for &(k, v) in key_val.values() {
if k != 0 {
wanted_strings.insert(k);
}
if v != 0 {
wanted_strings.insert(v);
}
}
let (str_blobs, _, _) = collect_blobs(
&open,
id_size,
&wanted_strings,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
)?;
let mut string_to_arr: HashMap<u64, (u64, u8)> = HashMap::new();
let mut str_off_cache: HashMap<u64, Option<(usize, Option<usize>)>> = HashMap::new();
for (&addr, &(class_id, ref blob)) in &str_blobs {
let offs = *str_off_cache.entry(class_id).or_insert_with(|| {
let value_off = match field_offset(
class_id,
"value",
"java/lang/String",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Object)) => off as usize,
_ => return None,
};
let coder_off = match field_offset(
class_id,
"coder",
"java/lang/String",
class_map,
strings,
obj_ref_width,
) {
Some((off, HprofType::Byte)) => Some(off as usize),
_ => None,
};
Some((value_off, coder_off))
});
if let Some((value_off, coder_off)) = offs {
if value_off + obj_ref_width <= blob.len() {
let arr_ref = read_ref(&blob[value_off..], obj_ref_width);
let coder = match coder_off {
Some(co) if co < blob.len() => blob[co],
_ => 1,
};
if arr_ref != 0 {
string_to_arr.insert(addr, (arr_ref, coder));
}
}
}
}
drop(str_blobs);
let wanted_arrays: std::collections::HashSet<u64> =
string_to_arr.values().map(|&(a, _)| a).collect();
let (_, arr_blobs, _) = collect_blobs(
&open,
id_size,
&std::collections::HashSet::new(),
&wanted_arrays,
&std::collections::HashSet::new(),
)?;
let decode = |str_addr: u64| -> Option<String> {
if str_addr == 0 {
return None;
}
let &(arr_addr, coder) = string_to_arr.get(&str_addr)?;
let bytes = arr_blobs.get(&arr_addr)?;
Some(decode_java_string(bytes, coder))
};
let mut pairs: Vec<(String, String)> = Vec::new();
for &(k, v) in key_val.values() {
let (Some(key), Some(value)) = (decode(k), decode(v)) else {
continue;
};
if key.is_empty() {
continue;
}
pairs.push((key, value));
}
pairs.sort();
pairs.dedup();
let find = |key: &str| -> Option<String> {
pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
};
let jvm_version = find("java.vm.version").or_else(|| find("java.version"));
Ok((pairs, jvm_version))
}
pub(crate) fn render_frame(
class: Option<&str>,
method: Option<&str>,
source: Option<&str>,
class_serial: u32,
line_number: i32,
) -> String {
let class = class
.map(|c| c.to_string())
.unwrap_or_else(|| format!("<class#{class_serial}>"));
let method = method.unwrap_or("<method>");
let source = source.unwrap_or("Unknown Source");
let loc = match line_number {
n if n > 0 => format!("{source}:{n}"),
-2 => format!("{source}(Compiled Method)"),
-3 => "Native Method".to_string(),
_ => source.to_string(),
};
format!("{class}.{method} ({loc})")
}
pub(crate) fn pretty_binary_name(name: &str) -> String {
let trimmed = name.strip_prefix('L').unwrap_or(name);
let trimmed = trimmed.strip_suffix(';').unwrap_or(trimmed);
trimmed.replace('/', ".")
}