Read the heap dump's domination from the classes up - #2933
Open
pyricau wants to merge 1 commit into
Open
Conversation
A fourth view beside the treemap, the rings and the stack, and the first one that isn't the dominator tree read from the roots down. Every object of the dump goes on the row of its class along the bottom, and above each row are the classes of the objects dominating it — an icicle chart of a reverse call tree, with "what retains this" in place of "who called this". So a column reads `byte[]` at the bottom, `Bitmap` above it, `LruCache` above that, and how wide it is says how much of the heap's `byte[]` bytes that accounts for. Which is the question the other three views can't answer. They start from one object and walk out, so "what holds all the byte[] arrays, by class" means finding 16,730 rectangles scattered over the picture and adding them up by eye. Here it is the widest row, and what holds them is the row above it. A row is weighed by the objects at the bottom of its column, in their own bytes. That is the one weighting that makes rows comparable: retained size would count an object's bytes again on every row above it, so the rows would add up to several times the heap and "a fifth of the dump" would mean nothing. Shallow bytes add up to the dump exactly once, so the reverse root weighs exactly what the dominator tree's root weighs, a row's width is its share of the whole heap at every level, and a row's children cover it to the byte. Two kinds of row stop a column rather than leaving width over: objects nothing in particular dominates, and objects only uncollected garbage does. It costs no second dominator computation — only immediateDominatorOf, one object at a time — and nothing at all until the view is opened. On large-dump.hprof (39 MB, 387,971 objects, 2.4 s to open) the class rows are gathered in 0.2 s and the level above the widest row in 0.01 s. What it holds between reads is at most one entry per object of the dump however many levels are open, because expanding a row hands its entries to the rows above and drops its own. On that dump the opening picture is `16,730 × byte[]` at 10 MB, with `151 × Bitmap` accounting for 7.7 MB of it, and above the bitmaps two map entries, a LinkedHashMap and an LruCache each holding 3.9 MB of those bytes. Three things were needed to make it a view rather than a screen. `StackLayout` takes a rowsGoUp flag, applied to the finished rows since a row's distance from the bottom isn't known until the last one is placed, and StackView scrolls it with reverseScrolling so that scrollTo(0) still means "where it opens". The two trees share the root node id, so switching shape substitutes the root in the view request rather than in the history, which leaves the path into the tree being left there to come back to. And every cell here is a pile of objects, including the root, so a row is coloured and outlined like an object instead — washing all of them out would spend the whole picture saying something no cell of it contradicts — with the count in the label saying "pile" and the two rows that stop a column keeping their own look. That shared root is also the one thing a node id can't answer for, so what a clicked cell is, is read off the shape being drawn: the whole heap dump is a row of this view and the root of the other, and the classes a row had no room for are named after the cell they were left out of, which on a dump with 8,679 classes is that very node. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A fourth view in Shark Explorer, and the first one that isn't the dominator tree read from the roots down: every object of the heap dump on the row of its class along the bottom, and above each row the classes of the objects dominating it. An icicle chart of a reverse call tree, with "what retains this" in place of "who called this".
A column reads
byte[]at the bottom,Bitmapabove it,LruCacheabove that, and how wide it is says how much of the heap'sbyte[]bytes that accounts for.Which is the question the other three views can't answer. They start from one object and walk out, so "what holds all the
byte[]arrays, by class" means finding 16,730 rectangles scattered over the picture and adding them up by eye. Here it's the widest row, and what holds them is the row above it.What the picture looks like
large-dump.hprof, 39 MB, 387,971 objects — the opening view of the classes tab:16,730 × byte[]— 10 MB, a third of the heap151 × Bitmapaccounting for 7.7 MB of those bytes, and14,874 × Classfor 2.2 MB2 × LinkedHashMap$LinkedHashMapEntry,1 × LinkedHashMapand1 × LruCache, each holding 3.9 MB of themNothing in particular, the objects the dominator tree hangs off the whole heap dump because they're held from more than one placeSix rows of one column, in the picture the view opens on, with no chain walked and nothing clicked.
The weighting, which is the whole design
A row is weighed by the objects at the bottom of its column, in their own bytes. That's the one weighting that makes rows comparable: retained size would count an object's bytes again on every row above it, so the rows would add up to several times the heap and "a fifth of the dump" would mean nothing.
Shallow bytes add up to the dump exactly once, so:
Two kinds of row stop a column rather than leaving width over: objects nothing in particular dominates, and objects only uncollected garbage does.
What it costs
No second dominator computation — only
immediateDominatorOf, one object at a time, plus the tree's nodes for shallow sizes and to know what Shark folded. And nothing at all until the view is opened. Onlarge-dump.hprof, which takes 2.4 s to open:What it holds between reads is at most one entry per object of the heap dump, however many levels are open: expanding a row hands its entries to the rows above it and drops its own, so the live entries stand for disjoint sets of the objects at the bottom of the columns.
Three decisions worth reviewing
Going up is done to the finished layout.
StackLayoutgains arowsGoUpflag and flips every row oncerowCountis known, because a row's distance from the bottom isn't known until the last row has been placed.contentHeightis clamped to the viewport when growing up, so the root row sits on the bottom edge of the view rather than of the rows, andStackViewscrolls it withreverseScrollingsoscrollTo(0)still means "where it opens".The two trees share the root node id, so switching shape substitutes the root in the view request rather than in the history — which is what leaves the path into the tree being left there to come back to. Every other node is told apart by a range check at the far end of the range the treemap takes its pile ids from.
Every cell here is a pile of objects, including the root, so a row is coloured and outlined like an object instead of like a pile: washing all of them out and dashing all of their edges would spend the whole picture saying something no cell of it contradicts, and lose the hues that make a column readable. The count in the label (
151 × Bitmap) says "pile" instead, and the two rows that stop a column keep their own look — slate and dashed forNothing in particular, purple for the garbage.That shared root is also the one thing a node id can't answer for, so what a clicked cell is, is read off the shape being drawn: the whole heap dump is a row of this view and the root of the other, and the classes a row had no room for are named after the cell they were left out of, which on a dump with 8,679 classes is that very node.
Tests
ReverseDominatorTreeTest— 13 cases on the tree: the rows gather by class weighed by shallow bytes, they add up to what the forward root weighs, the row above one is its dominators split by class in proportion, every row's children cover it exactly, both kinds of terminal row,pathToOpen, the column read nearest-first, and that a node of one tree is no node of the other.StackLayoutTest— four upward cases: the root row across the bottom withcontentHeightclamped to the viewport, a stack taller than the viewport starting at its own bottom, "going up changes nothing but where a row is", and hit testing.ClassesViewTest— the window: the bottom row is the whole heap dump, pointing at a row says which class it gathers, clicking one says what holds it going down, and the classes a row had no room for say they're classes.TreeLayoutTest— switching to the classes view lays the tree out exactly once, since that first layout is a pass over every object of the dump.notes/treemap-rendering.mdandnotes/dominator-tree.mdcarry the design and the numbers; the module'sAGENTS.mdgains the one trap — the shared root node.Try it:
./gradlew :shark:shark-explorer:shark-explorer-app:run \ --args="--title=\"Classes view\" leakcanary/leakcanary-android-instrumentation/src/androidTest/assets/large-dump.hprof"Pick Classes in the Shape row, then read up a column: click a row to give what dominates it the whole width, and the details panel lists the rows under it, nearest first.
🤖 Generated with Claude Code