///|
pub suberror ParseError {
Invalid(Int, String)
} derive(Debug)
///|
pub extend ParseError with @moonbitlang/core/debug.Debug::{to_repr}
///|
priv struct Reader {
data : Bytes
mut pos : Int
end : Int
id_size : Int
}
///|
fn Reader::skip(self : Reader, count : Int) -> Unit raise ParseError {
if count < 0 || count > self.end - self.pos {
raise Invalid(self.pos, "truncated record")
}
self.pos += count
}
///|
fn Reader::number(self : Reader, width : Int) -> UInt64 raise ParseError {
let start = self.pos
self.skip(width)
let mut value = 0UL
for i in start.. Int raise ParseError {
self.number(1).to_int()
}
///|
fn Reader::u2(self : Reader) -> Int raise ParseError {
self.number(2).to_int()
}
///|
fn Reader::length(self : Reader) -> Int raise ParseError {
let n = self.number(4)
if n > 2_147_483_647UL {
raise Invalid(self.pos - 4, "length exceeds prototype limit")
}
n.to_int()
}
///|
fn Reader::id(self : Reader) -> UInt64 raise ParseError {
self.number(self.id_size)
}
///|
fn type_width(kind : Int, id_size : Int) -> Int raise ParseError {
match kind {
2 => id_size
4 | 8 => 1
5 | 9 => 2
6 | 10 => 4
7 | 11 => 8
_ => raise Invalid(0, "unknown value type: \{kind}")
}
}
///|
/// Counts physical dump records, not retained heap sizes or live-object proofs.
pub struct Summary {
id_size : Int
mut classes : Int
mut instances : Int
mut object_arrays : Int
mut primitive_arrays : Int
priv strings : Map[UInt64, String]
priv class_names : Map[UInt64, UInt64]
priv counts : Map[UInt64, Int]
priv data : Bytes
timestamp_ms : UInt64
priv definitions : Map[UInt64, ClassInfo]
priv objects : Map[UInt64, ObjectInfo]
priv roots_data : Array[RootInfo]
priv frames : Map[UInt64, StackFrame]
priv traces : Map[UInt64, StackTrace]
priv threads : Map[UInt64, ThreadInfo]
priv class_serials : Map[UInt64, UInt64]
priv record_counts : Map[Int, Int]
priv limits : ParseLimits
}
///|
pub fn Summary::count_class(self : Summary, name : String) -> Int {
let mut count = 0
for class_id, string_id in self.class_names {
if self.strings.get(string_id) == Some(name) {
count += self.counts.get(class_id).unwrap_or(0)
}
}
count
}
///|
fn read_class(r : Reader, summary : Summary) -> Unit raise ParseError {
check_limit(
r.pos,
"classes",
summary.definitions.length(),
summary.limits.max_classes,
)
let id = r.id()
if id == 0UL ||
summary.definitions.contains(id) ||
summary.objects.contains(id) {
raise Invalid(r.pos, "duplicate or null class object ID")
}
let stack_serial = r.number(4)
let superclass_id = r.id()
let loader_id = r.id()
let signers_id = r.id()
let protection_domain_id = r.id()
r.skip(r.id_size * 2)
let instance_size = r.length()
let constant_values : Array[ConstantEntry] = []
let constants = r.u2()
for _ in 0.. Unit raise ParseError {
check_limit(
object.payload_offset,
"objects",
summary.objects.length(),
summary.limits.max_objects,
)
if object.id == 0UL ||
summary.objects.contains(object.id) ||
summary.definitions.contains(object.id) {
raise Invalid(object.payload_offset, "duplicate or null object ID")
}
summary.objects[object.id] = object
}
///|
fn read_root(r : Reader, tag : Int, summary : Summary) -> Unit raise ParseError {
check_limit(
r.pos,
"roots",
summary.roots_data.length(),
summary.limits.max_roots,
)
let object_id = r.id()
let related_id = if tag == 1 { r.id() } else { 0UL }
let thread_serial = match tag {
2 | 3 | 4 | 6 | 8 => r.number(4)
_ => 0UL
}
let frame_number = match tag {
2 | 3 | 8 => r.number(4)
_ => 0UL
}
summary.roots_data.push({
tag,
object_id,
related_id,
thread_serial,
frame_number,
})
}
///|
fn read_heap(r : Reader, summary : Summary) -> Unit raise ParseError {
while r.pos < r.end {
let offset = r.pos
let tag = r.u1()
match tag {
0xff | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 => read_root(r, tag, summary)
0x20 => {
read_class(r, summary)
summary.classes += 1
}
0x21 => {
let id = r.id()
let stack_serial = r.number(4)
let class_id = r.id()
let length = r.length()
let payload_offset = r.pos
r.skip(length)
add_object(summary, {
id,
kind: Instance,
class_id,
stack_serial,
primitive_kind: 0,
length: 0,
payload_offset,
payload_bytes: length,
})
summary.instances += 1
summary.counts[class_id] = summary.counts.get(class_id).unwrap_or(0) + 1
}
0x22 => {
let id = r.id()
let stack_serial = r.number(4)
let count = r.length()
let class_id = r.id()
if count > (r.end - r.pos) / r.id_size {
raise Invalid(offset, "truncated object array")
}
let payload_offset = r.pos
r.skip(count * r.id_size)
add_object(summary, {
id,
kind: ObjectArray,
class_id,
stack_serial,
primitive_kind: 2,
length: count,
payload_offset,
payload_bytes: count * r.id_size,
})
summary.object_arrays += 1
}
0x23 => {
let id = r.id()
let stack_serial = r.number(4)
let count = r.length()
let kind = r.u1()
if kind == 2 {
raise Invalid(offset, "object type in primitive array")
}
let width = type_width(kind, r.id_size)
if count > (r.end - r.pos) / width {
raise Invalid(offset, "truncated primitive array")
}
let payload_offset = r.pos
r.skip(count * width)
add_object(summary, {
id,
kind: PrimitiveArray,
class_id: 0UL,
stack_serial,
primitive_kind: kind,
length: count,
payload_offset,
payload_bytes: count * width,
})
summary.primitive_arrays += 1
}
_ => raise Invalid(offset, "unsupported heap subrecord: \{tag}")
}
}
}
///|
/// Prototype for HotSpot JAVA PROFILE 1.0.2, with strict record boundaries.
/// ponytail: retains the input and name tables; streaming/mmap is future work.
pub fn parse(
data : Bytes,
limits? : ParseLimits = ParseLimits::new(),
) -> Summary raise ParseError {
limits.validate()
if data.length() > limits.max_file_bytes {
raise Invalid(0, "input exceeds configured file limit")
}
let r = Reader::{ data, pos: 0, end: data.length(), id_size: 4, }
r.skip(19)
if data[0:19].to_owned() != b"JAVA PROFILE 1.0.2\x00" {
raise Invalid(0, "expected JAVA PROFILE 1.0.2")
}
let id_size = r.length()
if id_size != 4 && id_size != 8 {
raise Invalid(19, "identifier width must be 4 or 8")
}
let timestamp_ms = r.number(8)
let summary = Summary::{
id_size,
classes: 0,
instances: 0,
object_arrays: 0,
primitive_arrays: 0,
strings: {},
class_names: {},
counts: {},
data,
timestamp_ms,
definitions: {},
objects: {},
roots_data: [],
frames: {},
traces: {},
threads: {},
class_serials: {},
record_counts: {},
limits,
}
let mut heap_seen = false
let mut end_seen = false
let mut record_total = 0
while r.pos < r.end {
check_limit(r.pos, "top-level records", record_total, limits.max_records)
record_total += 1
let tag = r.u1()
summary.record_counts[tag] = summary.record_counts.get(tag).unwrap_or(0) + 1
r.skip(4)
let size = r.length()
let start = r.pos
r.skip(size)
let record = Reader::{ data, pos: start, end: r.pos, id_size, }
match tag {
1 => {
check_limit(
start,
"strings",
summary.strings.length(),
limits.max_strings,
)
let id = record.id()
if id == 0UL {
raise Invalid(start, "null string ID")
}
if record.end - record.pos > limits.max_text_bytes {
raise Invalid(start, "string exceeds configured text limit")
}
if summary.strings.contains(id) {
raise Invalid(start, "duplicate string ID")
}
summary.strings[id] = decode_hprof_text(
data[record.pos:record.end].to_owned(),
)
}
2 => {
let serial = record.number(4)
let class_id = record.id()
record.skip(4)
summary.class_names[class_id] = record.id()
summary.class_serials[serial] = class_id
if record.pos != record.end {
raise Invalid(start, "invalid LOAD_CLASS size")
}
}
4 => {
let id = record.id()
let method_name_id = record.id()
let signature_id = record.id()
let source_name_id = record.id()
let class_serial = record.number(4)
let line_number = record.number(4).to_int()
if record.pos != record.end || summary.frames.contains(id) {
raise Invalid(start, "invalid or duplicate stack frame")
}
summary.frames[id] = {
id,
method_name_id,
signature_id,
source_name_id,
class_serial,
line_number,
}
}
5 => {
let serial = record.number(4)
let thread_serial = record.number(4)
let count = record.length()
if count > limits.max_trace_frames {
raise Invalid(start, "trace frame limit exceeded")
}
if count > (record.end - record.pos) / id_size {
raise Invalid(start, "truncated stack trace")
}
let frame_ids = []
for _ in 0.. {
let serial = record.number(4)
let object_id = record.id()
let stack_serial = record.number(4)
let name_id = record.id()
let group_name_id = record.id()
let parent_group_name_id = record.id()
if record.pos != record.end || summary.threads.contains(serial) {
raise Invalid(start, "invalid or duplicate thread")
}
summary.threads[serial] = {
serial,
object_id,
stack_serial,
name_id,
group_name_id,
parent_group_name_id,
}
}
0x0c | 0x1c => {
if end_seen {
raise Invalid(start, "heap segment after end")
}
read_heap(record, summary)
heap_seen = true
if tag == 0x0c {
end_seen = true
}
}
0x2c => {
if size != 0 || !heap_seen || end_seen {
raise Invalid(start, "invalid HEAP_DUMP_END")
}
end_seen = true
}
// Other top-level records are length-delimited and can be skipped safely.
_ => ()
}
}
if !heap_seen || !end_seen {
raise Invalid(r.pos, "incomplete heap dump")
}
summary
}