Skip to content

Give each way out of memory its own section on the leaks screen - #2953

Merged
pyricau merged 4 commits into
mainfrom
in-shark-explorer-in-leak-asyn
Aug 11, 2026
Merged

Give each way out of memory its own section on the leaks screen#2953
pyricau merged 4 commits into
mainfrom
in-shark-explorer-in-leak-asyn

Conversation

@pyricau

@pyricau pyricau commented Aug 11, 2026

Copy link
Copy Markdown
Member

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:

DisplayListCanvas.sPool → Pools$SimplePool.mPool → Object[][x] → Canvas.mFinalizer
  → NativeAllocationRegistry$CleanerRunner.cleaner
  → Cleaner.prev ×41 → Cleaner.referent → RenderNode.mOwningView

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 Cleaner forty 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:

Cleaner.referent →        3 objects
  DecorView 0x12d4a220
  ActionMenuPresenter$OverflowMenuButton 0x12d4e1d0
  ImageButton 0x12d4d0f0

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.leakFingerprint promises 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:

App leaks · 1 leak, 1 object
  AsyncTask.SERIAL_EXECUTOR →   1 object
    MainActivity 0x12d368b8
Library leaks · none
▸ On their way out · 3 phantom reachable, 2 unreachable

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.hprof

Section Before After
App leaks 4 leaks, 4 objects 1 leak, 1 object
Library leaks 0 0
Softly reachable 0
Weakly reachable 0
Waiting to be finalized 0
Phantom reachable 1 leak, 3 objects
Unreachable 1 leak, 2 objects 1 leak, 2 objects

The app leak that remains is AsyncTask.SERIAL_EXECUTORMainActivity, leak fingerprint 6bf18730, which is the leak and the fingerprint LeakCanary reports for the same dump. The two unreachable ones are ViewRootImpls that nothing points at at all.

🤖 Generated with Claude Code

pyricau and others added 2 commits August 11, 2026 08:21
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>
@pyricau pyricau changed the title Leave what the collector is about to take off the list of leaks Give each way out of memory its own section on the leaks screen Aug 11, 2026
pyricau and others added 2 commits August 11, 2026 15:55
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>
@pyricau
pyricau merged commit 8a176f3 into main Aug 11, 2026
16 checks passed
@pyricau
pyricau deleted the in-shark-explorer-in-leak-asyn branch August 11, 2026 14:08
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