Give each way out of memory its own section on the leaks screen - #2953
Merged
Conversation
The leaks screen listed every object the inspectors said shouldn't be here, however weakly it was held, and only made an exception for the unreachable ones. So a destroyed view whose last holder is a Cleaner — phantom reachable, already finalized, waiting for the runtime to run a thunk and let go — was an app leak to go and fix, named after whatever the walk up to the roots ran into. On leak_asynctask_o.hprof that was three views named `DisplayListCanvas.sPool`, one of them reached through forty one `Cleaner.prev` steps. LeakCanary reports none of them, and finds them the same way: its leaking object finder returns all 26 objects of that dump, the three included, and they are dropped one step later because its analysis follows no soft, weak or phantom referent, so nothing reaches them and they never make it into a report. Same answer here, decided a step earlier and on the strength rather than on whether a path turned up: an object the garbage collector releases on its own is not the app's to fix. It stays on the map, where what it says is which bytes haven't come back yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rather than dropping the objects the collector clears on its own, which was the previous commit and hid twelve of this dump's twenty six behind a log line. They are an answer — an object that was meant to be gone and is on its way is worth saying — and which way it is going is a different answer each time, so it is a section each: softly reachable, weakly reachable, waiting to be finalized, phantom reachable, and the unreachable one that was already there. LeakCanary can tell none of them apart. Its analysis follows no soft, weak or phantom referent, so every one of these is an object no GC root reaches as far as it is concerned, and the whole lot goes in the bucket this screen has been calling unreachable. Splitting that bucket is the thing this window can do that a leak trace can't. None of these groups is named after a chain. What holds a phantom reachable view is a Cleaner forty links down a static list, and naming the leak after the canvas pool at the top of that list said nothing about the view: the class is what tells two objects of one section apart, and the section title says the rest. The chain is still read, because which other leak holds an object is what folds one leaked screen's worth of them into one row instead of nine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A phantom reachable view was named after its class, which says what the object is and nothing about why it is still in memory. The answer is one step above it on the chain: the first reference holding no more firmly than the object itself, which is the one the collector hasn't cleared yet. Everything below it is in memory because that reference still is, so clearing it takes the lot — which makes it what tells one of these leaks from another, the way a suspect stretch of references is what tells one app leak from another. On leak_asynctask_o.hprof the phantom section goes from three rows named DecorView, OverflowMenuButton and ImageButton to one named Cleaner.referent with the three of them under it: one `Cleaner` that still has its referent, holding a screen's worth of views. The fingerprint follows the name and keeps the section in it, so that the same reference under two strengths stays two leaks. Unreachable is the one section left named after a class — nothing holds those objects, so there is no reference to name them after, which is also why it is the one section whose groups still carry a sentence saying what being in it means. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five of the seven sections say the same thing: this object shouldn't be in memory, and nothing you do will get rid of it any sooner. Drawing them like the app's own leaks makes a heap dump with one leak in it read as a dump with seven kinds of problem, so they go under one heading, folded, quieter than a section heading and with no band or bar of their own. Folded says what is in it — "On their way out · 3 phantom reachable, 2 unreachable" — so that folding hides the rows and never the answer, and the paragraph about what being on the way out means waits until someone has pressed it. The count line above the list is split the same way: how many leaks there are to do something about, then how many objects are leaving on their own. Which half a section is in is LeakKind.isOnTheWayOut, and it is exactly the sections a reachability strength names. The heading's own folded state goes in the set of unfolded groups, because it is the same kind of thing — what the reader has asked to see — so it is kept in a note and carried by a link like the rest of it. 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.
The leaks screen listed every object the inspectors said shouldn't be here, however weakly it was held, with an exception only for the unreachable ones. So a destroyed view whose last holder is a
Cleaner— phantom reachable, already finalized, waiting for the runtime to run a thunk and let go — came out as an app leak to go and fix, named after whatever the walk up to the GC roots ran into:Now each way out of memory is a section of its own: softly reachable, weakly reachable, waiting to be finalized, phantom reachable, and the unreachable one that was already there. App leaks and library leaks stay at the top, and they are still the two sections that mean "somebody has to do something".
LeakCanary can tell none of the five apart. Its analysis follows no soft, weak or phantom referent, so every one of them is an object no GC root reaches as far as it is concerned, and the whole lot ends up in the bucket this screen has been calling unreachable. Splitting that bucket is a thing this window can do that a leak trace can't.
Named after the reference that hasn't let go
A group in one of the five is named after the reference the collector hasn't cleared yet: the first one on the chain holding no more firmly than the object itself. Everything below it is in memory because that one reference still is, so clearing it takes the lot — which is what it has in common with the objects beside it, the way a suspect stretch of references is what an app leak's objects have in common.
The class name said what the object is and nothing about why it is still here. And the chain it sits on can't be read whole either: what holds a phantom reachable view is a
Cleanerforty links down a static list, so the first reference of that chain is a canvas pool that says nothing about the view.On this dump that is
Cleaner.referent, and the three views that were three rows are one:Unreachable is the one section still named after a class: nothing holds those objects, so there is no reference to name them after. It is also the one whose groups still carry a sentence saying what being in the section means — everywhere else the reference says it.
The fingerprint follows the name and keeps the section in it, since
LeakGroup.leakFingerprintpromises two groups never share one.The chain is still walked for every section, because which other leak holds an object is what folds one leaked screen's worth of objects into one row instead of nine. Dropping the walk cost exactly that, and the phantom section went from 3 rows to 8 before it was put back.
Folded, because none of it is a leak to fix
The five say the same thing between them — this object shouldn't be in memory, and nothing you do will get rid of it any sooner — so they go under one heading, folded, quieter than a section heading and with no band or bar of their own. Folded it still says what is in it, so folding hides the rows and never the answer:
The paragraph about what being on the way out means waits until someone presses it. The count line above the list is split the same way:
1 leaking object · 5 on their way out.The heading's own folded state goes in the set of unfolded groups, so it is kept in a note and carried by a link like the rest of it.
On
leak_asynctask_o.hprofThe app leak that remains is
AsyncTask.SERIAL_EXECUTOR→MainActivity, leak fingerprint6bf18730, which is the leak and the fingerprint LeakCanary reports for the same dump. The two unreachable ones areViewRootImpls that nothing points at at all.🤖 Generated with Claude Code