Skip to content

Releases: johnneerdael/heaptrail

heaptrail v1.4.1

Choose a tag to compare

@johnneerdael johnneerdael released this 25 Oct 19:37

v1.4.1 — the user guide is a site

The user guide now lives at https://heaptrail.neerdael.nl/. Analysis output is unchanged.

  • A new 67-page guide: tutorials, how-tos, recipes, concepts and a full reference. Every block of output in it comes from a real run, and CI fails if a page quotes a line, flag or command the tool did not produce.
  • README.md is a short front door, USERGUIDE.md maps the old sections to the new pages, and glossary.md is replaced by the guide's glossary.
  • --help for --preview-bytes and --target-glob names the guide page instead of a USERGUIDE.md section.
  • The analysing-heap-dumps Claude Code plugin is now 1.4.1 and its skill links to the guide. Update the plugin to get it.
  • The crate links to the guide as its documentation, and the published package no longer carries the guide sources (56 files).
  • The CI clippy step passes again on Rust 1.99 (as_chunks in preview.rs).

Upgrade with cargo install heaptrail, or download a binary below.

Validation:

  • cargo fmt --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --bin heaptrail (161 tests)
  • --preview-bytes output identical before and after the clippy fix on JAVA_PROFILE_1.0.2 and 1.0.3
  • mkdocs build --strict and the guide's output checks (67 pages, 0 errors)

heaptrail v1.4.0

Choose a tag to compare

@johnneerdael johnneerdael released this 25 May 16:00

v1.4.0 — Android capture tmp cleanup

Prevents Android device /data pressure from repeated heap captures.

  • Deletes stale /data/local/tmp/*.hprof files before each android-capture run.
  • Removes each device-side HProf after it has been pulled locally.
  • Includes cleanup commands in the capture transcript for provenance.
  • Updates manual adb dumpheap guidance to clean /data/local/tmp before and after captures.

Validation:

  • cargo fmt --check
  • cargo test --bin heaptrail (161 tests)

heaptrail 1.3.1

Choose a tag to compare

@johnneerdael johnneerdael released this 24 May 19:15

Android capture reliability

  • Wait for device-side HProf files to become nonzero/stable before pulling.
  • Fail after 60s of unchanged remote size instead of repeatedly pulling empty dumps.
  • Always attempt am profile stop <pid> cleanup for allocation-tracked captures.
  • Report allocation-tracked capture stalls as unsupported/failed on that device so agents fall back to diff-series, leak suspects, holders, and paths.
  • Add android-capture --series <COUNT> and --series-delay-seconds <SECONDS> to collect validated HProf series for --diff-series.

Validation: cargo test (161 tests) and cargo package --allow-dirty passed.

heaptrail v1.3.0

Choose a tag to compare

@johnneerdael johnneerdael released this 24 May 18:31

Highlights

  • Added --diff-series for ordered 3+ snapshot playback timelines with per-step deltas, first-to-last totals, monotonic growth candidates, JSON output, and mapping support.
  • Added --group-holders for referrer reports to group noisy holders by owner family, holder class, and field label.
  • Added root metadata fallback for --paths-from-id when thread names/frames are absent from the HProf.
  • Added --native-context to annotate diff-series reports with bounded Android dumpsys meminfo PSS totals.
  • Updated README, USERGUIDE, and the heap-dump Claude skill for the new playback debugging workflow.

Verification

  • cargo test passed: 155 tests.
  • cargo build --release passed.
  • cargo package --allow-dirty verified the 1.3.0 crate package.
  • Validated diff-series, grouped holders, root fallback, and native context against the real Nexio HProf artifacts.

heaptrail v1.2.0

Choose a tag to compare

@johnneerdael johnneerdael released this 24 May 17:33

What's new\n\n- Adds R8/ProGuard mapping support with --mapping for Android release-build heap dumps.\n- Adds --auto-mapping to match the installed package version to local Gradle outputs and select the correct mapping file.\n- Deobfuscates summary, diff, referrers, paths, merged paths, leak suspects, and allocation-site reports.\n- Keeps obfuscated_class_name in summary/diff JSON where names were deobfuscated.\n- Records mapping path/source and R8 pg_map_id / pg_map_hash in android-capture transcripts.\n- Defaults auto-mapping project root to the current directory and prefers a single universal variant when split outputs match.\n\n## Verification\n\n- cargo test: 144 passed\n- cargo build --release: passed\n- cargo package --allow-dirty: passed\n- cargo publish --dry-run --allow-dirty: passed

heaptrail v1.1.1 — modern Android dump robustness

Choose a tag to compare

@johnneerdael johnneerdael released this 11 May 20:07

v1.1.1 — modern Android dump robustness

A point fix for v1.1.0. No new features, no behavior change for dumps that already worked.

The bug

v1.1.0 panicked with

thread 'hprof-recorder' panicked at src/result_recorder.rs:625:14:
class id must have a class definition

during summary aggregation on dumps where an InstanceDump referenced a class id that was never emitted as a ClassDump / LoadClass record. Modern Android dumps (am dumpheap on recent ART builds — Android 14+, app-image and zygote-shared classes elided by the dumper) routinely do this. On affected dumps the entire summary mode was unusable; investigation modes that didn't depend on per-class field reconstruction still worked but were unreachable because the recorder thread panicked before they could be invoked.

The fix

Both class-lookup paths in result_recorder now degrade gracefully:

  • calculate_instance_size_recursive returns bare object-header size when a class id has no ClassDump (best available lower bound — field reconstruction isn't possible without the definition).
  • get_class_name_string returns <unknown class #<id>> instead of panicking.
  • Missing class ids are deduped into a HashSet and reported once on stderr at the end of summary aggregation:
warning: 886 class id(s) referenced without a ClassDump/LoadClass record;
affected instances counted with bare object-header size only. Common on
modern Android dumps where some boot-classpath or zygote-shared classes
are elided.

The operator now knows the summary's class table is a lower bound when those elided classes happened to dominate the heap — without per-class log spam.

Verification

  • 235 MiB / 686 K-instance nexio-validation-v0.56.hprof Android dump (886 elided classes) now runs summary, --retained-size, and --leak-suspects --exclude-soft-weak --preview-bytes 200 end to end.
  • All 92 existing tests pass.
  • Reference dumps JAVA_PROFILE_1.0.2.hprof and JAVA_PROFILE_1.0.3.hprof produce byte-identical output (zero missing-class warnings).
  • All other modes (--retained-size, --leak-suspects, --find-referrers, --paths-from-id, --diff-from/--diff-to) are unchanged — they index by object id and never depended on per-class field reconstruction. They work on the affected dumps as well.

Docs

  • README: new "v1.1.1 — modern Android dump robustness" section.
  • USERGUIDE: top-of-file callout with the upgrade command.
  • plugins/analysing-heap-dumps/skills/analysing-heap-dumps/SKILL.md: bumped source line to 1.1.1+, updated install / upgrade instructions, and added the panic message to the Common Mistakes table so the skill catches it in user conversations.

Plugin update

/plugin marketplace update johnneerdael/heaptrail
/plugin update analysing-heap-dumps@analysing-heap-dumps

Install / upgrade

cargo install heaptrail --force
cargo install --git https://github.com/johnneerdael/heaptrail --force

Pre-built binaries for Linux / macOS / Windows × x86_64 / aarch64 attached below by the release workflow.

heaptrail v1.1.0

Choose a tag to compare

@johnneerdael johnneerdael released this 10 May 01:14

v1.1.0 — MAT-grade leak hunting

Four new opt-in flags bring heaptrail to feature parity with MAT's daily Android leak-hunting workflow:

  • --exclude-soft-weak — drop outgoing edges from java.lang.ref.{Soft,Weak,Phantom}Reference subclasses across path walks and the retained-size graph. MAT's default leak-hunting filter; required on Android dumps where LeakCanary watchers and framework weak-refs would otherwise bury the real strong reference.
  • --leak-suspects[=THRESHOLD] — auto-rank dominators by retained share, cluster each subtree by class, emit narrative + path-to-root + content preview per suspect. The "what's wrong with this dump?" entry point — no need to know what class to grep for first.
  • --merge-paths — modifier on --paths-from-id that folds paths-to-root for all instances of the target class into a single tree with branch counts. "47 leaked MainActivity instances share the same EventBus chain" in one command instead of 47.
  • --bitmaps — list top-N android.graphics.Bitmap instances by pixel-byte size with dimensions, config, location, and holder summary. Handles pre-O Java-heap and O+ native-pixel sizing.

Recommended Android workflow

heaptrail -i heap.hprof --leak-suspects --exclude-soft-weak --preview-bytes 200

This is the "open MAT, click Leak Suspects" workflow — at the CLI, in seconds, with content snippets inline.

Why these landed together

Spec §1 identifies four distinct pain points: weak-ref noise drowning paths, no automatic suspect ID, one-instance-at-a-time path walks, invisible bitmaps. They share infrastructure (the v1.0.0 dominator pipeline) but each closes a different gap. Sequenced so reference-strength filtering lands first; Leak Suspects' default narrative would surface false positives without it.

Memory and wall time

Worst-case combined working memory: ~225 MiB on a 200 MiB Android dump (v1.0.0 budget + ~12 MiB dom_children + ~400 KiB bitmap entries). All four flags opt-in; default v1.0.x behavior preserved.

Real diagnostic example (1.0.2 fixture)

Heap: 141.81 MiB total, 106.60 MiB retained-reachable.
Threshold: 5.0 % retained share. Showing 3 suspect(s) (3 above threshold).

Suspect 1 — 27.98 MiB (26.2 % of heap)
  dominator: java.util.ArrayList (object_id=361762400)
  accumulating: 262517 instances of MetaPreview, total 26.04 MiB
  path to GC root:
    ... HomeViewModel.fullCatalogRows -> ViewModelStore.map -> ...

Compatibility

  • All v1.0.x CLI invocations produce byte-identical output unless one of the new flags is set.
  • ReferenceGraph, lengauer_tarjan, RetainedAnalysis unchanged from v1.0.0 (internal API contract per v1.0 §3.6).
  • No new dependencies.

Plugin update

/plugin marketplace update johnneerdael/heaptrail
/plugin update analysing-heap-dumps@analysing-heap-dumps

Install

cargo install heaptrail
cargo install --git https://github.com/johnneerdael/heaptrail

Pre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.

heaptrail v1.0.0

Choose a tag to compare

@johnneerdael johnneerdael released this 10 May 00:21

v1.0.0 — Retained size via dominator tree

A new flag, --retained-size, surfaces dominator-tree retained sizes across summary, --paths-from-id, and --find-referrers. This is the last MAT-grade datum heaptrail didn't surface; v1.0.0 closes the gap.

Why this exists

Real triage friction: a class like ResolvedDisplayItem is 88 bytes shallow but holds a 12-element ResolvedDisplayFieldSlots and an ArtworkBundle. For 35K instances, shallow says 3 MB and ranks the class low. The actual question — "is this 35K-instance retention 35 MB or 350 MB?" — could only be answered by following references manually for one instance and multiplying out, or by reaching for Eclipse MAT.

heaptrail v1.0.0 brings retained size — the metric MAT computes via dominator-tree analysis — to the CLI. Same diagnostic shape as --preview-bytes for v0.9.0: the data was always there, but heaptrail wasn't surfacing it.

What's new

  • --retained-size flag (default off). Recommended for triage runs.
  • summary class table re-sorts by retained bytes and adds a retained column.
  • New "Largest retained instances" block listing top-N (object_id, class, retained_bytes).
  • --paths-from-id annotates each hop with (retained=<size>).
  • --find-referrers adds a class retained column to holder rows.

Sample output (1.0.2 fixture, JVM)

Top 5 classes by retained heap (dominator tree):
+----------+-----------+-----------+-------------+---------------------------------------+
|  Shallow | Instances |  Retained |     Largest | Class name                            |
+----------+-----------+-----------+-------------+---------------------------------------+
|  2.80MiB |    122154 | 126.84MiB |  24.00bytes | java.util.ArrayList                   |
|  1.53MiB |     28697 |  58.58MiB |  56.00bytes | com.nexio.tv.domain.model.CatalogRow  |
| 56.89MiB |    573552 |  57.37MiB | 104.00bytes | com.nexio.tv.domain.model.MetaPreview |
|  8.25KiB |       352 |  15.18MiB |  24.00bytes | kotlin.SynchronizedLazyImpl           |
+----------+-----------+-----------+-------------+---------------------------------------+

SynchronizedLazyImpl at 8.25 KiB shallow / 15.18 MiB retained is exactly the wrapper-vs-subgraph case shallow size buries.

Algorithm

In-memory CSR object-reference graph + Lengauer–Tarjan immediate dominators (O(N α(N))) + iterative post-order DFS for retained sums. Class-level rollup and top-N hot list fall out of a single pass.

Reference strength

v1.0.0 includes weak / soft / phantom-reference edges (strict graph-theoretic dominator definition). Eclipse MAT excludes them by default in its leak-hunting workflow, so a side-by-side comparison may show MAT's retained smaller than heaptrail's. By design. Selective exclusion ships in v1.1+ as --exclude-soft-weak.

Memory cost

~200 MiB working memory + ~1–3 s wall time on a 200 MiB Android dump (~3M objects). Negligible on typical JVM dumps.

Compatibility

  • Existing CLI invocations produce byte-identical output unless --retained-size is set.
  • JSON schema gains optional class_retained_by_name / top_retained_instances fields, both #[serde(skip_serializing_if = "Option::is_none")].
  • No new dependencies.

Plugin update

/plugin marketplace update johnneerdael/heaptrail
/plugin update analysing-heap-dumps@analysing-heap-dumps

Roadmap

v1.1+ depends on the v1.0 dominator pipeline:

  • --exclude-soft-weak — rebuild graph dropping Reference subclass edges
  • --leak-suspects — rank dominator-tree subtrees by retained share (MAT-style narrative report)
  • --merge-paths — fold paths converging at the same dominator

Install

cargo install heaptrail               # crates.io 1.0.0
cargo install --git https://github.com/johnneerdael/heaptrail

Pre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.

heaptrail v0.9.0

Choose a tag to compare

@johnneerdael johnneerdael released this 09 May 23:16

v0.9.0 — Content preview for primitive arrays

A single new flag, --preview-bytes N, surfaces the first N bytes/chars of char[] / byte[] / etc. inline in summary, --paths-from-id, --find-referrers id:N, and -l. UTF-8 / UTF-16 BE auto-detect with control-char escaping; falls back to xxd-style hex on binary.

Why this exists

Real session: summary showed a 72 MiB char[]. --paths-from-id walked to a StringBuilder.value held by a Gson serializer. heaptrail told us who held it but not what it contained — investigation needed adb shell for file size + source-grep for serialization candidates. With --preview-bytes 200, the inline <?xml version="1.0"...home_catalog_snapshot... would have identified it as the SharedPreferences XML blob in one command.

What's new

  • --preview-bytes N flag (default 0 = off). Recommended: 200.
  • summary largest-array entries get a preview line.
  • --paths-from-id primitive-array hops show preview blocks.
  • --find-referrers id:N shows preview when target is a primitive array.
  • -l extends to a "Standalone large arrays" section listing every text-like array ≥ 1 KiB. Threshold tunable via --list-arrays-min-bytes.

Memory cost

Truncated capture: at most N bytes per primitive array. ~260 MiB peak on a 200 MiB Android dump with N=200; negligible on typical JVM dumps.

Compatibility

  • Every existing CLI invocation produces byte-identical output unless --preview-bytes is set.
  • Existing JSON schema unchanged (the in-memory preview blobs are #[serde(skip)]).
  • No new dependencies.

Plugin update

/plugin marketplace update johnneerdael/heaptrail
/plugin update analysing-heap-dumps@analysing-heap-dumps

Roadmap

  • v1.0.0 — feature E (full Lengauer–Tarjan dominator tree / retained size).

Install

cargo install heaptrail               # crates.io 0.9.0
cargo install --git https://github.com/johnneerdael/heaptrail

Pre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.

heaptrail v0.8.0

Choose a tag to compare

@johnneerdael johnneerdael released this 09 May 22:18

v0.8.0 — Thread metadata, allocation sites, glob targeting, path indices

Four metadata-surfacing wins, no parser-mode changes, fully backwards-compatible.

A — Thread name + top frame on thread-owned roots

When --paths-from-id terminates at RootJavaFrame, RootThreadObject, RootJniLocal, or RootJniMonitor, the output now includes:

  → reached GC root: RootJavaFrame
        thread "pool-7-thread-2"
        at android.app.SharedPreferencesImpl$EditorImpl.commitToMemory(SharedPreferencesImpl.java:478)

Engineering motivation: a real session ended with → reached GC root: RootJavaFrame for a 72 MiB char[]. We had the chain up to the frame but not which thread or method — that pinned 20 minutes on source-grepping gson.toJson|moshi.*toJson candidates. The HPROF spec already includes thread serial and frame index on RootJavaFrame; the data was there, just not rendered.

C — --allocation-sites mode + always-on summary hint

For dumps captured under allocation tracking (am profile start <pid> before am dumpheap), heaptrail now prints the top-N allocation sites with resolved Java stack traces:

Top 20 allocation sites by bytes_allocated (of 12,453 total):
  ─ 1.21 GiB  /  4,812,000 instances  com.nexio.tv.domain.model.MetaPreview#<init>
        at com.squareup.moshi.adapters.ClassJsonAdapter.fromJson(ClassJsonAdapter.java:128)
        at com.squareup.moshi.JsonAdapter$1.fromJson(JsonAdapter.java:194)
        ...

Summary always reports presence/absence: AllocationSites: 12,453 sites … or AllocationSites: not present (capture with am profile start ).

Engineering motivation: skip the source-grep entirely when alloc-tracking is on. Most direct shortcut from "this class is huge" to "this is the line that allocated it."

D — Object[] element index in path hops

Path hops through Object[] now show the matched slot:

  hop 5  ── id=518041528  (via java.lang.Object[][12])

Engineering motivation: a path through ArrayList.elementData used to render as a generic via java.lang.Object[][] — you could see that an array held it, but not where in the array. With paged results or sparse caches, the slot index is the load-bearing piece of information.

F — --target-glob shell-style targeting

A new top-level flag matches any class against a globset pattern:

heaptrail -i my.hprof --target-glob 'com.example.**' --hops 2
heaptrail -i my.hprof --target-glob '**$Itr'           # all iterator inner classes

* stays within a package level; ** crosses levels; ? matches one character; [abc] is a character class. Mutually exclusive with --find-referrers. Output prepends a list of matched classes with live instance counts.

Engineering motivation: you see 35K instances spread across *$Itr / *$KeyIterator / *$EntryIterator and want to chase "what's producing all these iterators?" — that's six exact-match runs vs one --target-glob '**$Itr'.

Compatibility

  • Every existing CLI invocation produces byte-identical output unless the dump itself contains the relevant metadata.
  • Existing JSON output gains optional fields (root_thread_name, root_frame, array_index, matched_classes); no fields removed.
  • One new dependency: globset 0.4. Binary size delta: negligible.

Plugin update

The Claude Code plugin manifest is also bumped to 0.8.0 — run:

/plugin marketplace update johnneerdael/heaptrail
/plugin update analysing-heap-dumps@analysing-heap-dumps

Roadmap

  • v0.9.0 — feature B (--preview-bytes content preview)
  • v1.0.0 — feature E (full Lengauer–Tarjan dominator tree / retained size)

Both have their own design specs in docs/superpowers/specs/.

Install

cargo install heaptrail               # crates.io 0.8.0
cargo install --git https://github.com/johnneerdael/heaptrail   # from source

Pre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.