Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "midden"
version = "0.1.1"
version = "0.2.0"
edition = "2024"
rust-version = "1.88"
description = "Find what is holding a JVM heap dump's memory: retained sizes, owners, leak suspects."
Expand All @@ -13,6 +13,7 @@ categories = ["command-line-utilities", "development-tools::debugging"]
[dependencies]
clap = { version = "4.6.4", features = ["derive"] }
flate2 = "1"
zerocopy = { version = "0.8", features = ["alloc"] }
serde_json = "1.0.151"

[lints.rust]
Expand Down
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ boxed, references, garbage, system, direct and baseline. Pick them with `--only`

## Benchmark

midden 0.1.0 compared with Eclipse MAT 1.17, JProfiler 16.2.1 and the VisualVM 2.2.2 heap engine,
midden 0.2.0 compared with Eclipse MAT 1.17, JProfiler 16.2.1 and the VisualVM 2.2.2 heap engine,
run on 8 CPUs with 4 GiB of memory. Each dump was in the page cache before its runs,
and the tables show the best of 2 or 3 runs.
and the tables show the best of 2 or 3 runs. The other tools were measured alongside midden 0.1.0 on the
same machine.

### Cold first run

Expand All @@ -72,30 +73,30 @@ Suspects. JProfiler is `jpanalyze -retained=true`, which builds the index its GU

| Dump | Objects | midden | midden, 1 thread | MAT | JProfiler | VisualVM |
|---|---:|---:|---:|---:|---:|---:|
| Paper server, 938 MB | 11.2 M | **1.95 seconds** | 3.62 seconds | 20.95 seconds | 12.05 seconds | 55.8 seconds |
| mixed-small, 414 MB | 4.1 M | **0.68 seconds** | 1.21 seconds | 7.09 seconds | 4.11 seconds | 772 seconds |
| mixed-1g, 1.2 GB | 11.8 M | **1.79 seconds** | 3.41 seconds | 17.15 seconds | 10.25 seconds | over 1,800 seconds, stopped |
| arrays-1g, 1.2 GB | 37 k | **0.10 seconds** | 0.33 seconds | 1.84 seconds | 0.97 seconds | 0.37 seconds |
| chain-8m, 429 MB | 8.0 M | **0.89 seconds** | 1.36 seconds | 12.26 seconds | 7.01 seconds | 23.8 seconds |
| small-20m, 945 MB | 19.2 M | **6.92 seconds** | 14.95 seconds | 45.71 seconds | 33.46 seconds | 334 seconds |

midden was 7 to 18 times faster than MAT and 5 to 10 times faster than JProfiler, and it was faster on a
| Paper server, 938 MB | 11.2 M | **1.30 seconds** | 3.53 seconds | 20.95 seconds | 12.05 seconds | 55.8 seconds |
| mixed-small, 414 MB | 4.1 M | **0.40 seconds** | 1.11 seconds | 7.09 seconds | 4.11 seconds | 772 seconds |
| mixed-1g, 1.2 GB | 11.8 M | **1.10 seconds** | 3.78 seconds | 17.15 seconds | 10.25 seconds | over 1,800 seconds, stopped |
| arrays-1g, 1.2 GB | 37 k | **0.08 seconds** | 0.26 seconds | 1.84 seconds | 0.97 seconds | 0.37 seconds |
| chain-8m, 429 MB | 8.0 M | **0.85 seconds** | 1.64 seconds | 12.26 seconds | 7.01 seconds | 23.8 seconds |
| small-20m, 945 MB | 19.2 M | **5.85 seconds** | 14.71 seconds | 45.71 seconds | 33.46 seconds | 334 seconds |

midden was 8 to 23 times faster than MAT and 6 to 12 times faster than JProfiler, and it was faster on a
single thread than either of them on eight. MAT's full three-report run (suspects, overview and top
components) took 26 to 38 seconds on Paper.

### Reopening an indexed dump

| Dump | midden | MAT | JProfiler |
|---|---:|---:|---:|
| Paper | 1.66 seconds | 30.59 seconds | **1.54 seconds** |
| small-20m | 6.74 seconds | 18.80 seconds | **3.94 seconds** |
| Paper | **1.06 seconds** | 30.59 seconds | 1.54 seconds |
| small-20m | 5.49 seconds | 18.80 seconds | **3.94 seconds** |

JProfiler reopens faster on the large dumps because its analysis stores the dominator tree. midden's
cache only stores the parse, so it saves at most 0.3 seconds.
JProfiler reopens small-20m faster because its analysis stores the dominator tree. midden's cache only
stores the parse, so it saves 0.2 to 0.4 seconds.

### Memory

Peak RSS on Paper: midden 1.0 GB, JProfiler 1.3 GB, MAT 2.1 to 3.3 GB (at its 3 GB heap), VisualVM 3.5 GB.
Peak RSS on Paper: midden 0.6 GB, JProfiler 1.3 GB, MAT 2.1 to 3.3 GB (at its 3 GB heap), VisualVM 3.5 GB.


### Features
Expand Down
2 changes: 1 addition & 1 deletion src/analysis/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl Heap<'_> {
if loader == crate::dump::NONE {
return "bootstrap".to_string();
}
let id = dump.objects[loader as usize].id;
let id = dump.objects.id(loader as usize);
let first = dump
.classes
.iter()
Expand Down
10 changes: 5 additions & 5 deletions src/analysis/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,13 @@ impl Heap<'_> {
Shape::Array(field) => {
let array = graph.field(collection, field)?;
stats.backing = array;
stats.capacity = u64::from(self.dump.objects[array as usize].len);
stats.capacity = u64::from(self.dump.objects.get(array as usize).len);
stats.entries = graph.edges(array).len() as u64;
}
Shape::Identity(field) => {
let array = graph.field(collection, field)?;
stats.backing = array;
stats.capacity = u64::from(self.dump.objects[array as usize].len) / 2;
stats.capacity = u64::from(self.dump.objects.get(array as usize).len) / 2;
stats.entries = graph
.edges(array)
.labels()
Expand All @@ -183,7 +183,7 @@ impl Heap<'_> {
Shape::Hash { table, next } => {
let Some(buckets) = graph.field(collection, table) else { return Some(stats) };
stats.backing = buckets;
stats.capacity = u64::from(self.dump.objects[buckets as usize].len);
stats.capacity = u64::from(self.dump.objects.get(buckets as usize).len);
stats.used_buckets = graph.edges(buckets).len() as u64;
self.hash_nodes(buckets, next, &mut count);
stats.entries = nodes;
Expand Down Expand Up @@ -353,7 +353,7 @@ impl Heap<'_> {
if !self.reachable(object) {
continue;
}
let record = &dump.objects[object as usize];
let record = dump.objects.get(object as usize);
if record.kind == Kind::ObjectArray {
part.sparse.extend(self.sparse_array(object, ref_size));
continue;
Expand Down Expand Up @@ -399,7 +399,7 @@ impl Heap<'_> {

/// A big plain object array, owned by no collection, under half filled.
fn sparse_array(&self, object: u32, ref_size: u64) -> Option<Sparse> {
let record = &self.dump.objects[object as usize];
let record = self.dump.objects.get(object as usize);
let filled = self.graph.edges(object).len() as u64;
let idom = self.dominators.idom[object as usize];
let backs_collection = idom != self.graph.root && self.shape_of(idom).is_some();
Expand Down
Loading
Loading