Skip to content

perf(heap-dump): add SoftReference caches for BFS/string-scan/leak-re… - #395

Open
ziyilin wants to merge 1 commit into
eclipse-jifa:mainfrom
ziyilin:heapdump_analysis_cache
Open

perf(heap-dump): add SoftReference caches for BFS/string-scan/leak-re…#395
ziyilin wants to merge 1 commit into
eclipse-jifa:mainfrom
ziyilin:heapdump_analysis_cache

Conversation

@ziyilin

@ziyilin ziyilin commented Jul 29, 2026

Copy link
Copy Markdown

…port results

Problem: Three HeapDump operations redundantly re-execute expensive MAT computations on every call within the same session:

  1. getMergePathToGCRoots - @Cacheable on queryByCommand always misses because Helper.buildHeapObjectArgument returns a new anonymous IHeapObjectArgument instance each time; Cache.CacheKey uses Arrays.equals which falls back to reference identity → never matches. Each miss triggers a full-heap BFS (O(N) graph traversal + disk I/O).

  2. getLeakReport - only the raw MAT IResult was cached (SoftReference), but the LeakReport Java object and its embedded Shortest Paths (which internally trigger additional BFS calls) were rebuilt from scratch on every invocation.

  3. getStrings - called the 2-arg queryByCommand(context, command) which bypasses the @Cacheable 3-arg version entirely, causing a full-heap string scan on every call with any pattern.

Solution: Add explicit SoftReference-based caches in AnalysisContext, bypassing the broken @Cacheable mechanism:

  • MergePathTreeCacheKey with proper Arrays.equals/hashCode for caching merge_shortest_paths IResultTree by objectIds
  • StringData cache for find_strings full result (".*" pattern), with Java-side regex filtering for specific patterns
  • cachedLeakReport SoftReference for the final built object, avoiding repeated BFS + object reconstruction

All caches use double-checked locking with synchronized(context) for thread safety, and SoftReference to allow GC under memory pressure.

Three HeapDump operations re-executed expensive MAT computations on every
call within the same session, because the @Cacheable annotation on
queryByCommand never produced a hit:

1. getMergePathToGCRoots - Helper.buildHeapObjectArgument returns a new
   anonymous IHeapObjectArgument per call, and Cache.CacheKey compares
   arguments with Arrays.equals, which falls back to reference identity,
   so every call missed and triggered a full-heap BFS.

2. getLeakReport - only the raw MAT IResult was cached; the LeakReport
   object and its embedded shortest paths (each triggering further BFS)
   were rebuilt from scratch every time.

3. getStrings - called the 2-arg queryByCommand, bypassing @Cacheable
   entirely, so every call rescanned all strings in the heap.

Caching: explicit SoftReference caches in AnalysisContext, reclaimable
under memory pressure.

- mergePathTreeCache, keyed by MergePathTreeCacheKey: the objectIds are
  cloned (immune to later mutation by the caller) and sorted, so the same
  object set hits regardless of request order. The merge_shortest_paths
  result does not depend on that order; it only affects sibling
  enumeration in the resulting tree, which no caller relies on.
- leakReportCache holds the built LeakReport, avoiding both the BFS and
  the object reconstruction.
- stringsCache is keyed by the MAT pattern actually used, so paging
  through one search scans the heap once while the retained set stays
  proportional to what was asked for. Entries whose result has been
  reclaimed are pruned on insert.

Locking: the BFS-backed caches use dedicated monitors (mergePathTreeLock,
leakReportLock) instead of the context monitor. AnalysisContext is public
and shared, so running a minutes-long BFS while holding its monitor would
block unrelated code that locks on the context (e.g. the
classLoaderExplorerData path). stringsCache takes no lock: the query is
idempotent, so a cold-start race at worst repeats the scan, whereas a
lock held across a full-heap scan would stall every other search.

findStrings: the pattern is now passed to MAT as a compiled Pattern
argument instead of being spliced into the command string ("find_strings
java.lang.String -pattern <p>"). CommandLine.tokenize splits on
whitespace, so a spliced pattern containing a space would be parsed as
extra arguments and rejected with a SnapshotException.

Matching deliberately stays inside MAT. Serving specific patterns from a
single cached full scan by filtering in Java on the result's first column
was considered and rejected: that column is the display name
("java.lang.String @ 0x..." followed by the value truncated at 256
chars), so filtering on it would match class names and addresses and
would miss content beyond the truncation point. MAT's FindStringsQuery
matches IObject#getClassSpecificName - the value, resolved up to 1024
chars - which is the intended semantics, and the new unit tests guard it:
the display-name prefix must not match every string, a sentinel planted
past the truncation point must still be found, and paging must be served
from one result set.
@ziyilin
ziyilin force-pushed the heapdump_analysis_cache branch from f5d3017 to 4815125 Compare July 30, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant