Commit 785b68d
Add LinkEvalDominatorTree: exact Lengauer-Tarjan dominator tree
## Background
DominatorTree builds an approximate dominator tree incrementally during
BFS, using Lowest Common Ancestor (LCA) to update immediate dominators
when a node is reached via multiple paths. This approximation breaks
when a cross-edge (to an already-visited node) is processed while the
parent's dominator is still stale — the child's dominator is left too
specific, causing over-attribution of retained sizes.
A convergence loop (re-running LCA over all cross-edges until stable)
was explored as a fix, but benchmarks showed it is unsuitable for
production: on a 25 MB heap dump it required 781 iterations and added
~62 seconds on top of a 1.5-second analysis (~40× overhead). The cost
scales with the depth of the longest stale-dominator propagation chain
and the number of cross-edges, making it unpredictable in general.
## Solution
New class LinkEvalDominatorTree implements the exact Lengauer-Tarjan
algorithm with link-eval (union-find path compression), adapted from
Android Studio's perflib/.../LinkEvalDominators.kt (Apache 2.0,
http://adambuchsbaum.com/papers/dom-toplas.pdf). The result is always
exact regardless of graph topology.
The algorithm runs in 4 phases:
1. Iterative DFS — assigns depth-first numbers (DFNs), records each
node's DFS-tree parent, and accumulates all edges (tree + cross) in
a flat buffer. All GC roots are children of a virtual root at DFN 0.
Children are pushed to the stack unconditionally; duplicates are
detected at pop time (DFN already assigned), deferring the
objectIndex lookup to avoid an extra binary search per edge.
2. CSR predecessor list — two-pass conversion of the flat edge buffer
into a Compressed Sparse Row structure (offsets + packed DFNs). Only
this structure scales with edge count; all other arrays are O(nodes).
3. Lengauer-Tarjan Steps 2+3+4 — reverse-DFS-order computation of
semi-dominators (Step 2) and tentative immediate dominators (Step 3)
via intrusive singly-linked bucket lists; forward pass (Step 4)
resolves deferred assignments. The link-eval compress function is
iterative (not recursive) to avoid stack overflow on deep heap graphs.
4. Populate retained sizes — maps DFN results back to object IDs,
populates a DominatorTree instance, and delegates to
DominatorTree.buildFullDominatorTree.
## Memory design
All large arrays use VarIntArray / VarLongArray (ByteArray-backed,
variable bytes per entry) instead of IntArray / LongArray. For heaps
with up to ~16 M objects bytesPerDfn = 3, saving 25% vs plain IntArray.
The INVALID sentinel is the all-0xFF bit pattern, always > any valid DFN.
HeapObject.objectIndex (a dense 0-based Int) is used as the node key
in Phase 1, replacing the IdentityHashMap from Android Studio's generic
implementation and avoiding the ~20 MB overhead of a LongLongScatterMap.
Memory on a 193 MB heap dump (~983 K reachable objects, 2.5 M edges):
~48 MB peak during CSR construction (edge buffer + CSR arrays alive)
~43 MB during the L-T algorithm
~13 MB during retained-size computation (doms[] + id maps remain
alongside DominatorTree.dominated; freed after buildFullDominatorTree)
For a 50 MB dump (~350 K objects): ~16 MB peak.
## Changes
- Add LinkEvalDominatorTree.kt with full L-T implementation
- Add LinkEvalDominatorTreeTest: diamond-graph test proving correctness
where DominatorTree gives the wrong answer (a.retainedSize=10 not 20)
- DominatorTree: strip the convergence loop API (collectCrossEdges
param, crossEdges field, runConvergenceLoop, pruneSettledCrossEdges)
and update KDoc to document the approximation bug and point here
- DominatorTreeTest: remove convergence loop tests; keep the regression
test documenting the stale-dominator bug, and refine the test DSL
- HprofInMemoryIndex.objectAtIndex: fix off-by-one (index > 0 →
index >= 0) so the class object at index 0 resolves correctly
- Update API dump
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>1 parent 02d0d8b commit 785b68d
File tree
6 files changed
+785
-96
lines changed- shark
- shark-graph/src/main/java/shark/internal
- shark
- api
- src
- main/java/shark
- test/java/shark
- internal
6 files changed
+785
-96
lines changedLines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
210 | 210 | | |
211 | 211 | | |
212 | 212 | | |
213 | | - | |
| 213 | + | |
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
583 | 583 | | |
584 | 584 | | |
585 | 585 | | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
586 | 591 | | |
587 | 592 | | |
588 | 593 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
18 | 36 | | |
19 | 37 | | |
20 | 38 | | |
| |||
56 | 74 | | |
57 | 75 | | |
58 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
59 | 95 | | |
60 | 96 | | |
61 | 97 | | |
| |||
0 commit comments