Repository navigation
Releases: johnneerdael/heaptrail
Release list
heaptrail v1.4.1
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.mdis a short front door,USERGUIDE.mdmaps the old sections to the new pages, andglossary.mdis replaced by the guide's glossary.--helpfor--preview-bytesand--target-globnames the guide page instead of aUSERGUIDE.mdsection.- The
analysing-heap-dumpsClaude 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_chunksinpreview.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-bytesoutput 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
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
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
Highlights
- Added
--diff-seriesfor ordered 3+ snapshot playback timelines with per-step deltas, first-to-last totals, monotonic growth candidates, JSON output, and mapping support. - Added
--group-holdersfor referrer reports to group noisy holders by owner family, holder class, and field label. - Added root metadata fallback for
--paths-from-idwhen thread names/frames are absent from the HProf. - Added
--native-contextto annotate diff-series reports with bounded Androiddumpsys meminfoPSS totals. - Updated README, USERGUIDE, and the heap-dump Claude skill for the new playback debugging workflow.
Verification
cargo testpassed: 155 tests.cargo build --releasepassed.cargo package --allow-dirtyverified 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
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
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_recursivereturns bare object-header size when a class id has noClassDump(best available lower bound — field reconstruction isn't possible without the definition).get_class_name_stringreturns<unknown class #<id>>instead of panicking.- Missing class ids are deduped into a
HashSetand 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 200end to end. - All 92 existing tests pass.
- Reference dumps
JAVA_PROFILE_1.0.2.hprofandJAVA_PROFILE_1.0.3.hprofproduce 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 --forcePre-built binaries for Linux / macOS / Windows × x86_64 / aarch64 attached below by the release workflow.
heaptrail v1.1.0
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 fromjava.lang.ref.{Soft,Weak,Phantom}Referencesubclasses 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-idthat folds paths-to-root for all instances of the target class into a single tree with branch counts. "47 leakedMainActivityinstances share the sameEventBuschain" in one command instead of 47.--bitmaps— list top-Nandroid.graphics.Bitmapinstances 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 200This 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,RetainedAnalysisunchanged 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/heaptrailPre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.
heaptrail v1.0.0
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-sizeflag (default off). Recommended for triage runs.summaryclass table re-sorts by retained bytes and adds aretainedcolumn.- New "Largest retained instances" block listing top-N
(object_id, class, retained_bytes). --paths-from-idannotates each hop with(retained=<size>).--find-referrersadds aclass retainedcolumn 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-sizeis set. - JSON schema gains optional
class_retained_by_name/top_retained_instancesfields, 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 droppingReferencesubclass 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/heaptrailPre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.
heaptrail v0.9.0
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 Nflag (default 0 = off). Recommended: 200.summarylargest-array entries get a preview line.--paths-from-idprimitive-array hops show preview blocks.--find-referrers id:Nshows preview when target is a primitive array.-lextends 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-bytesis 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/heaptrailPre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.
heaptrail v0.8.0
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-bytescontent 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 sourcePre-built binaries for Linux/macOS/Windows × x86_64/aarch64 attached below.