use super::*;
use crate::pass2::Graph;
#[inline]
pub(crate) fn class_obj_repr(g: &Graph, i: usize) -> u32 {
g.class_obj_class_idx
.get(&(i as u32))
.copied()
.unwrap_or(u32::MAX)
}
pub(crate) fn gc_root_type_label(ty: u8) -> &'static str {
use crate::types::heap;
match ty {
heap::ROOT_SYSTEM_CLASS => "System Class",
heap::ROOT_JNI_GLOBAL => "JNI Global",
heap::ROOT_JNI_LOCAL => "JNI Local",
heap::ROOT_JAVA_FRAME => "Java Frame",
heap::ROOT_NATIVE_STACK => "Native Stack",
heap::ROOT_STICKY_CLASS => "Sticky Class",
heap::ROOT_THREAD_BLOCK => "Thread Block",
heap::ROOT_MONITOR_USED => "Busy Monitor",
heap::ROOT_THREAD_OBJ => "Thread",
heap::ROOT_INTERNED_STRING => "Interned String",
heap::ROOT_DEBUGGER => "Debugger",
heap::ROOT_VM_INTERNAL => "VM Internal",
heap::ROOT_JNI_MONITOR => "JNI Monitor",
_ => "Unknown",
}
}
pub(crate) fn escape_string_cell(s: &str) -> String {
s.chars()
.map(|c| match c {
'\n' | '\r' | '\t' => ' ',
'`' => '\'',
c if (c as u32) < 0x20 || c as u32 == 0x7f => '\u{fffd}',
c => c,
})
.collect::<String>()
.replace('|', "\\|")
}
fn is_prim_array_desc(name: &str) -> bool {
name.len() == 2
&& name.as_bytes()[0] == b'['
&& matches!(
name.as_bytes()[1],
b'Z' | b'C' | b'F' | b'D' | b'S' | b'I' | b'J' | b'B'
)
}
pub(crate) fn class_row_remap(g: &Graph) -> Vec<u32> {
let class_count = g.class_names.len();
let mut remap: Vec<u32> = (0..class_count as u32).collect();
let mut canonical: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();
for (row, name) in g.class_names.iter().enumerate() {
if name == "java/lang/Class" || is_prim_array_desc(name) {
let canon = *canonical.entry(name.as_str()).or_insert(row as u32);
remap[row] = canon;
}
}
remap
}
pub(crate) fn gc_root_type_label_opt(code: u8) -> Option<&'static str> {
use crate::types::heap;
match code {
heap::ROOT_JNI_GLOBAL => Some("JNI Global"),
heap::ROOT_JNI_LOCAL => Some("JNI Local"),
heap::ROOT_JAVA_FRAME => Some("Java Frame"),
heap::ROOT_NATIVE_STACK => Some("Native Stack"),
heap::ROOT_STICKY_CLASS => Some("Sticky Class"),
heap::ROOT_THREAD_BLOCK => Some("Thread Block"),
heap::ROOT_MONITOR_USED => Some("Busy Monitor"),
heap::ROOT_THREAD_OBJ => Some("Thread"),
heap::ROOT_SYSTEM_CLASS => Some("System Class"),
heap::ROOT_INTERNED_STRING => Some("Interned String"),
heap::ROOT_DEBUGGER => Some("Debugger"),
heap::ROOT_VM_INTERNAL => Some("VM Internal"),
heap::ROOT_JNI_MONITOR => Some("JNI Monitor"),
_ => None,
}
}
pub fn now_iso8601() -> String {
#[cfg(not(target_arch = "wasm32"))]
{
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
format_epoch_nanos(now.as_secs(), now.subsec_nanos())
}
#[cfg(target_arch = "wasm32")]
{
format_epoch_nanos(0, 0)
}
}
pub fn format_epoch_ms(ms: i64) -> String {
let secs = if ms < 0 { 0 } else { (ms / 1000) as u64 };
let full = format_epoch_nanos(secs, 0);
match (full.find('.'), full.rfind('Z')) {
(Some(dot), Some(z)) if dot < z => format!("{}{}", &full[..dot], &full[z..]),
_ => full,
}
}
fn format_epoch_nanos(secs: u64, nanos: u32) -> String {
let days = secs / 86_400;
let rem = secs % 86_400;
let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60);
let z = days as i64 + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}Z",
year, m, d, hh, mm, ss, nanos
)
}
pub fn format_bytes(n: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
const TB: u64 = 1024 * GB;
const PB: u64 = 1024 * TB;
if n < KB {
return format!("{} B", n);
}
let kb = n as f64 / KB as f64;
if n < MB && (kb * 10.0).round() < 1024.0 * 10.0 {
return format!("{:.1} KB", kb);
}
let mb = n as f64 / MB as f64;
if n < GB && (mb * 10.0).round() < 1024.0 * 10.0 {
return format!("{:.1} MB", mb);
}
let gb = n as f64 / GB as f64;
if n < TB && (gb * 100.0).round() < 1024.0 * 100.0 {
return format!("{:.2} GB", gb);
}
let tb = n as f64 / TB as f64;
if n < PB && (tb * 100.0).round() < 1024.0 * 100.0 {
return format!("{:.2} TB", tb);
}
format!("{:.2} PB", n as f64 / PB as f64)
}
pub const SIZE_BASIS_CAPTION: &str =
"_All sizes are binary (1 KB = 1024 bytes, 1 MB = 1024 KB, and so on)._";
pub(crate) fn fmt_count(n: u64) -> String {
group_thousands(&n.to_string())
}
pub(crate) fn plural_objects(n: u64) -> &'static str {
if n == 1 { "object" } else { "objects" }
}
pub(crate) fn group_thousands(digits: &str) -> String {
let mut result = String::new();
for (i, c) in digits.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
result.chars().rev().collect()
}
pub(crate) const DEPTH_DIST_CAPTION: &str = "_How many dominator hops each object sits \
below a GC root. A spike at depth 1–3 is normal; a long tail at depth 10+ points to \
deeply nested containers or linked structures._\n\n";
pub(crate) struct DepthStats {
pub(crate) rows: Vec<(u32, u64, f64, f64)>,
pub(crate) median_depth: u32,
pub(crate) max_depth: u32,
}
pub(crate) fn depth_stats(hist: &[DepthBucket]) -> Option<DepthStats> {
let total: u64 = hist.iter().map(|b| b.objects).sum();
if total == 0 {
return None;
}
let total_f = total as f64;
let mut rows = Vec::with_capacity(hist.len());
let mut running: u64 = 0;
let mut median_depth = hist.last().map(|b| b.depth).unwrap_or(0);
let mut median_found = false;
for b in hist {
running += b.objects;
let pct = b.objects as f64 / total_f * 100.0;
let cum = running as f64 / total_f * 100.0;
if !median_found && running * 2 >= total {
median_depth = b.depth;
median_found = true;
}
rows.push((b.depth, b.objects, pct, cum));
}
let max_depth = hist.last().map(|b| b.depth).unwrap_or(0);
Some(DepthStats {
rows,
median_depth,
max_depth,
})
}
pub(crate) fn depth_summary_line(s: &DepthStats) -> String {
format!(
"_Half of all live objects sit within {} hop{} of a GC root; the deepest chain is {} hop{}._\n\n",
s.median_depth,
if s.median_depth == 1 { "" } else { "s" },
s.max_depth,
if s.max_depth == 1 { "" } else { "s" },
)
}
pub(crate) fn fmt_pct(p: f64) -> String {
if p > 0.0 && p < 0.05 {
return "<0.1%".to_string();
}
format!("{p:.1}%")
}
pub const HEAP_BASIS_LABEL: &str = "reachable heap";
pub const HEAP_SCALAR_LABEL: &str = "Total Reachable Heap";
pub(crate) fn pct_of_heap(part: u64, total: u64) -> f64 {
if total == 0 {
return 0.0;
}
(part as f64 / total as f64 * 100.0).min(100.0)
}
pub fn pretty_class_name(raw: &str) -> String {
if raw.is_empty() {
return raw.to_string();
}
if !raw.starts_with('[') {
return raw.replace('/', ".");
}
let dims = raw.chars().take_while(|&c| c == '[').count();
let rest = &raw[dims..];
let base = if rest.len() == 1 {
match rest.chars().next().unwrap() {
'Z' => "boolean",
'B' => "byte",
'C' => "char",
'S' => "short",
'I' => "int",
'J' => "long",
'F' => "float",
'D' => "double",
_ => rest,
}
.to_string()
} else if rest.starts_with('L') && rest.ends_with(';') {
rest[1..rest.len() - 1].replace('/', ".")
} else {
rest.replace('/', ".")
};
format!("{}{}", base, "[]".repeat(dims))
}
pub(crate) fn object_kind(g: &Graph, i: usize) -> &'static str {
if class_obj_repr(g, i) != u32::MAX {
return "Class Objects";
}
let raw = match g.class_names.get(g.class_idx[i] as usize) {
Some(r) => r,
None => return "Instances",
};
if is_prim_array_desc(raw) {
"Primitive Arrays"
} else if raw.starts_with('[') {
"Object Arrays"
} else {
"Instances"
}
}
pub(crate) fn package_path(name: &str) -> String {
let mut s = name;
while s.starts_with('[') {
s = &s[1..];
}
if s.starts_with('L') && s.ends_with(';') {
s = &s[1..s.len() - 1];
}
if s.is_empty() || matches!(s, "B" | "C" | "D" | "F" | "I" | "J" | "S" | "Z") {
return "(primitives)".to_string();
}
if s.ends_with("[]") {
return "(primitives)".to_string();
}
let s = s.replace('/', ".");
match s.rfind('.') {
Some(dot) => s[..dot].to_string(),
None => "(default)".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_bytes_units_and_boundaries() {
assert_eq!(format_bytes(0), "0 B");
assert_eq!(format_bytes(512), "512 B");
assert_eq!(format_bytes(1023), "1023 B");
assert_eq!(format_bytes(1024), "1.0 KB");
assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
assert_eq!(format_bytes(1024 * 1024 - 1), "1.0 MB");
}
#[test]
fn format_bytes_tb_pb() {
const TB: u64 = 1024u64.pow(4);
const PB: u64 = 1024u64.pow(5);
assert_eq!(format_bytes(TB), "1.00 TB");
assert_eq!(format_bytes(5 * TB + TB / 2), "5.50 TB");
assert_eq!(format_bytes(PB), "1.00 PB");
assert_eq!(format_bytes(3 * PB), "3.00 PB");
assert_eq!(format_bytes(TB - 1), "1.00 TB");
}
#[test]
fn fmt_pct_tiny_nonzero_is_lt_point_one() {
assert_eq!(fmt_pct(0.0), "0.0%");
assert_eq!(fmt_pct(0.01), "<0.1%");
assert_eq!(fmt_pct(0.049), "<0.1%");
assert_eq!(fmt_pct(0.05), "0.1%");
assert_eq!(fmt_pct(12.34), "12.3%");
assert_eq!(fmt_pct(100.0), "100.0%");
}
#[test]
fn fmt_count_grouping() {
assert_eq!(fmt_count(0), "0");
assert_eq!(fmt_count(999), "999");
assert_eq!(fmt_count(1_000), "1,000");
assert_eq!(fmt_count(1_234_567), "1,234,567");
}
#[test]
fn pct_of_heap_clamps_and_guards_zero() {
assert_eq!(pct_of_heap(0, 0), 0.0);
assert_eq!(pct_of_heap(50, 100), 50.0);
assert_eq!(pct_of_heap(150, 100), 100.0);
}
}