#[derive(Debug, Clone, PartialEq)]
pub enum Explanation {
#[cfg_attr(not(test), allow(dead_code))]
Order { members: usize },
#[cfg_attr(not(test), allow(dead_code))]
TieBreak { key: String },
Rounding { expected: String, mat: String },
NoCounterpart { note: String },
MatClassObjectRootingGap { proof: String },
}
impl Explanation {
fn label(&self) -> &'static str {
match self {
Explanation::Order { .. } => "order(i)",
Explanation::TieBreak { .. } => "tie-break(ii)",
Explanation::Rounding { .. } => "rounding(iii)",
Explanation::NoCounterpart { .. } => "no-counterpart(iv)",
Explanation::MatClassObjectRootingGap { .. } => "MatClassObjectRootingGap",
}
}
fn evidence(&self) -> String {
match self {
Explanation::Order { members } => {
format!("set-equal, {members} members, order differs")
}
Explanation::TieBreak { key } => format!("identical sort key: {key}"),
Explanation::Rounding { expected, mat } => {
format!("our value renders '{expected}' == MAT '{mat}'")
}
Explanation::NoCounterpart { note } => note.clone(),
Explanation::MatClassObjectRootingGap { proof } => proof.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Tier {
Match,
Explainable(Explanation),
Fail,
}
#[derive(Debug, Clone)]
pub struct FieldDiff {
pub field: String,
pub ours: String,
pub mat: String,
pub tier: Tier,
}
impl FieldDiff {
pub(crate) fn matched(
field: impl Into<String>,
ours: impl Into<String>,
mat: impl Into<String>,
) -> Self {
FieldDiff {
field: field.into(),
ours: ours.into(),
mat: mat.into(),
tier: Tier::Match,
}
}
pub(crate) fn explained(
field: impl Into<String>,
ours: impl Into<String>,
mat: impl Into<String>,
e: Explanation,
) -> Self {
FieldDiff {
field: field.into(),
ours: ours.into(),
mat: mat.into(),
tier: Tier::Explainable(e),
}
}
pub(crate) fn failed(
field: impl Into<String>,
ours: impl Into<String>,
mat: impl Into<String>,
) -> Self {
FieldDiff {
field: field.into(),
ours: ours.into(),
mat: mat.into(),
tier: Tier::Fail,
}
}
}
#[derive(Debug, Default)]
pub struct DiffResult {
pub fields: Vec<FieldDiff>,
pub skipped: Vec<FieldDiff>,
}
impl DiffResult {
pub fn n_match(&self) -> usize {
self.fields.iter().filter(|f| f.tier == Tier::Match).count()
}
pub fn n_explainable(&self) -> usize {
self.fields
.iter()
.filter(|f| matches!(f.tier, Tier::Explainable(_)))
.count()
}
pub fn n_fail(&self) -> usize {
self.fields.iter().filter(|f| f.tier == Tier::Fail).count()
}
pub fn render_text(&self) -> String {
let mut out = String::new();
out.push_str("=== hprof-analyzer --diff (MAT report vs our JSON) ===\n\n");
for f in &self.fields {
let (mark, detail) = match &f.tier {
Tier::Match => ("MATCH ".to_string(), String::new()),
Tier::Explainable(e) => (
"EXPLAINABLE".to_string(),
format!(" [{}: {}]", e.label(), e.evidence()),
),
Tier::Fail => ("FAIL ".to_string(), String::new()),
};
out.push_str(&format!(
" {} {:<28} ours={:<16} mat={}{}\n",
mark, f.field, f.ours, f.mat, detail
));
}
if !self.skipped.is_empty() {
out.push_str("\n-- skipped (no counterpart, tier iv) --\n");
for f in &self.skipped {
let note = match &f.tier {
Tier::Explainable(e) => e.evidence(),
_ => String::new(),
};
out.push_str(&format!(
" SKIP {:<28} ours={:<16} mat={} [{}]\n",
f.field, f.ours, f.mat, note
));
}
}
out.push_str(&format!(
"\nsummary: MATCH={} EXPLAINABLE={} FAIL={} SKIP={}\n",
self.n_match(),
self.n_explainable(),
self.n_fail(),
self.skipped.len(),
));
out
}
pub fn render_json(&self) -> String {
fn esc(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn field_json(f: &FieldDiff) -> String {
let (tier, reason, evidence) = match &f.tier {
Tier::Match => ("MATCH", String::new(), String::new()),
Tier::Fail => ("FAIL", String::new(), String::new()),
Tier::Explainable(e) => ("EXPLAINABLE", e.label().to_string(), e.evidence()),
};
format!(
"{{\"field\":\"{}\",\"ours\":\"{}\",\"mat\":\"{}\",\"tier\":\"{}\",\"reason\":\"{}\",\"evidence\":\"{}\"}}",
esc(&f.field),
esc(&f.ours),
esc(&f.mat),
tier,
esc(&reason),
esc(&evidence),
)
}
let mut out = String::from("{\n \"fields\": [\n");
let all: Vec<String> = self.fields.iter().map(field_json).collect();
out.push_str(
&all.iter()
.map(|s| format!(" {s}"))
.collect::<Vec<_>>()
.join(",\n"),
);
out.push_str("\n ],\n \"skipped\": [\n");
let sk: Vec<String> = self.skipped.iter().map(field_json).collect();
out.push_str(
&sk.iter()
.map(|s| format!(" {s}"))
.collect::<Vec<_>>()
.join(",\n"),
);
out.push_str(&format!(
"\n ],\n \"summary\": {{\"match\": {}, \"explainable\": {}, \"fail\": {}, \"skip\": {}}}\n}}\n",
self.n_match(),
self.n_explainable(),
self.n_fail(),
self.skipped.len(),
));
out
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatHistRow {
pub class_name: String,
pub objects: u64,
pub shallow: u64,
pub retained: Option<u64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatSuspect {
pub class_name: String,
pub instance_count: Option<u64>,
pub retained: u64,
pub pct: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatComponent {
pub name: String,
pub pct: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatBiggestObject {
pub class_name: String,
pub shallow: u64,
pub retained: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatBiggestClass {
pub class_name: String,
pub objects: u64,
pub retained: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatPackageRow {
pub depth: usize,
pub segment: String,
pub dotted_path: String,
pub retained: u64,
pub top_dominators: u64,
}
#[derive(Debug, Default, Clone)]
pub struct MatReport {
pub used_heap_dump: Option<String>, pub number_of_objects: Option<u64>,
pub number_of_classes: Option<u64>,
pub number_of_class_loaders: Option<u64>,
pub number_of_gc_roots: Option<u64>,
pub format: Option<String>,
pub file_length: Option<u64>,
pub histogram: Vec<MatHistRow>,
pub histogram_total_objects: Option<u64>,
pub histogram_total_shallow: Option<u64>,
pub suspects: Vec<MatSuspect>,
pub components: Vec<MatComponent>,
pub biggest_objects: Vec<MatBiggestObject>,
pub biggest_classes: Vec<MatBiggestClass>,
pub packages: Vec<MatPackageRow>,
}