///|
pub fn cli_help() -> String {
"moon-hprof [id] [options]\n" +
"moon-hprof diff (class-name aggregate comparison)\n" +
" summary: [--class internal/name] [--format text|json]\n" +
" histogram: [--prefix name] [--kind instance|object_array|primitive_array]\n" +
" [--sort count|payload|elements|name] [--format json|csv] [--offset N] [--limit N]\n" +
" object: [--offset N] [--limit N]\n" +
" objects: [--offset N] [--limit N]\n" +
" class: \n" +
" roots: [--offset N] [--limit N]\n" +
" stack: \n" +
" refs: [--limit N]\n" +
" inbound: [--offset N] [--limit N]\n" +
" referenced: [--limit N] (rank direct reference targets)\n" +
" preview: [--limit preview-items]\n" +
" export: [--offset N] [--limit N] (JSON Lines)\n" +
" gc: [--limit N] (GC-root reachability, dominators and retained payload)\n" +
" root-path: (shortest strong path from a GC root)\n" +
" check: validate class layouts and metadata\n" +
"JSON IDs and 64-bit integers are decimal strings; payload sizes are file bytes."
}
///|
pub fn parse_object_id(text : String) -> UInt64 raise ParseError {
let chars = text.to_array()
let hex = text.has_prefix("0x") || text.has_prefix("0X")
let start = if hex { 2 } else { 0 }
let base = if hex { 16UL } else { 10UL }
if chars.length() == start {
raise Invalid(0, "empty object ID")
}
let mut value = 0UL
for i in start..= 48 && char <= 57 {
char - 48
} else if hex && char >= 65 && char <= 70 {
char - 55
} else if hex && char >= 97 && char <= 102 {
char - 87
} else {
raise Invalid(i, "invalid object ID digit")
}
if digit.to_uint64() >= base ||
value > (18446744073709551615UL - digit.to_uint64()) / base {
raise Invalid(i, "object ID out of range")
}
value = value * base + digit.to_uint64()
}
value
}
///|
fn cli_int(text : String) -> Int raise ParseError {
let value = parse_object_id(text)
if value > 2_147_483_647UL {
raise Invalid(0, "integer option out of range")
}
value.to_int()
}
///|
priv struct CliOptions {
command : String
id : UInt64
format : String
offset : Int
limit : Int
name : String
prefix : String
kind : String
sort : HistogramSort
}
///|
fn parse_cli(args : Array[String]) -> CliOptions raise ParseError {
if args.is_empty() {
raise Invalid(0, "missing command")
}
let command = args[0]
if ![
"summary", "histogram", "objects", "object", "class", "roots", "stack", "refs",
"check", "inbound", "preview", "export", "referenced", "gc", "root-path",
].contains(command) {
raise Invalid(0, "unknown command: \{command}")
}
let needs_id = [
"objects", "object", "class", "stack", "refs", "inbound", "preview", "export",
"root-path",
].contains(command)
let id = if needs_id {
if args.length() < 2 {
raise Invalid(0, "missing object ID or trace serial")
}
parse_object_id(args[1])
} else {
0UL
}
let mut format = if command == "summary" { "text" } else { "json" }
let mut offset = 0
let mut limit = 100
let mut name = ""
let mut prefix = ""
let mut kind = ""
let mut sort = HistogramSort::Count
let seen : Map[String, Bool] = Map([])
let mut i = if needs_id { 2 } else { 1 }
while i < args.length() {
let option = args[i]
if seen.contains(option) {
raise Invalid(i, "duplicate option: \{option}")
}
seen[option] = true
if i + 1 >= args.length() {
raise Invalid(i, "missing option value")
}
let value = args[i + 1]
match option {
"--format" if command == "summary" || command == "histogram" =>
format = value
"--class" if command == "summary" => name = value
"--prefix" if command == "histogram" => prefix = value
"--kind" if command == "histogram" => kind = value
"--sort" if command == "histogram" =>
sort = match value {
"count" => Count
"payload" => Payload
"elements" => Elements
"name" => Name
_ => raise Invalid(i, "invalid histogram sort")
}
"--offset" if [
"histogram", "objects", "object", "roots", "inbound", "export",
].contains(command) => offset = cli_int(value)
"--limit" if [
"histogram", "objects", "object", "roots", "refs", "inbound", "preview",
"export", "referenced", "gc",
].contains(command) => limit = cli_int(value)
_ => raise Invalid(i, "option not supported for command: \{option}")
}
i += 2
}
if limit > 100_000 {
raise Invalid(0, "limit must not exceed 100000")
}
if command == "summary" && format != "text" && format != "json" {
raise Invalid(0, "summary format must be text or json")
}
if command == "histogram" && format != "json" && format != "csv" {
raise Invalid(0, "histogram format must be json or csv")
}
{ command, id, format, offset, limit, name, prefix, kind, sort, }
}
///|
/// Pure command rendering: no process globals, filesystem, or console effects.
pub fn execute(
heap : Summary,
arguments : Array[String],
) -> String raise ParseError {
let options = parse_cli(arguments)
match options.command {
"summary" => {
if options.format == "json" {
let json = heap.summary_json()
match json {
Object(fields) =>
if options.name != "" {
fields["class_instances"] = json_int(
heap.count_class(options.name),
)
}
_ => ()
}
return json.stringify()
}
let text = "classes=\{heap.classes}\ninstances=\{heap.instances}\n" +
"object_arrays=\{heap.object_arrays}\nprimitive_arrays=\{heap.primitive_arrays}"
if options.name != "" {
text + "\nclass_instances=\{heap.count_class(options.name)}"
} else {
text
}
}
"histogram" => {
let rows = heap.select_histogram(
prefix=options.prefix,
kind=options.kind,
sort=options.sort,
offset=options.offset,
limit=options.limit,
)
if options.format == "json" {
Json::array(rows.map(row => row.to_json())).stringify()
} else {
let out = StringBuilder()
out.write_string(
"name,class_id,loader_id,kind,count,payload_bytes,elements\r\n",
)
for row in rows {
out.write_string(
[
row.name,
row.class_id.to_string(),
row.loader_id.to_string(),
row.kind,
row.count.to_string(),
row.payload_bytes.to_string(),
row.elements.to_string(),
]
.map(csv_cell)
.join(","),
)
out.write_string("\r\n")
}
out.to_string()
}
}
"objects" =>
Json::array(
heap
.object_ids(
class_id=options.id,
offset=options.offset,
limit=options.limit,
)
.map(json_id),
).stringify()
"object" =>
heap
.object_json(options.id, offset=options.offset, limit=options.limit)
.stringify()
"class" => heap.class_json(options.id).stringify()
"roots" =>
Json::array(
heap
.roots(offset=options.offset, limit=options.limit)
.map(root => root.to_json()),
).stringify()
"stack" =>
Json::array(heap.stack_trace(options.id).map(frame => frame.to_json())).stringify()
"refs" =>
Json::array(
heap
.references(options.id, limit=options.limit)
.map(edge => edge.to_json()),
).stringify()
"inbound" => {
let index = ReferenceIndex::build(heap)
Json::array(
index
.incoming(options.id, offset=options.offset, limit=options.limit)
.map(edge => edge.to_json()),
).stringify()
}
"referenced" => {
let index = ReferenceIndex::build(heap)
Json::array(
index.most_referenced(limit=options.limit).map(row => row.to_json()),
).stringify()
}
"preview" =>
heap.preview(options.id, max_items=options.limit).to_json().stringify()
"gc" => heap.gc_analysis().to_json(limit=options.limit).stringify()
"root-path" =>
heap.gc_analysis().root_path(options.id).to_json().stringify()
"export" =>
heap.objects_jsonl(
class_id=options.id,
offset=options.offset,
limit=options.limit,
)
"check" => {
heap.validate()
"OK"
}
_ => raise Invalid(0, "unreachable command")
}
}