Say whether an object is stuck, and which reference made it so - #2952
Merged
Conversation
Someone reading a heap dump knows things the inspectors can't: a cache that is meant to hold what it holds, a destroyed activity deliberately kept for one more frame. So a status can be set on an object, and it wins over what the inspectors made of it — with the reason kept, since a status nobody can check makes every other status in the dump worth less, and with what it overruled recorded after it. Passed into every question whose answer it changes rather than held by the tree, because the tree is read from one thread while a window is composed on another: overrides in the tree would draw a chain from one set of them and the row above it from another. findLeaks() deliberately takes none, so that a leak's fingerprint stays comparable to the one LeakCanary prints for the same objects. Two statuses can contradict each other, since each decides what the objects above or below it are, so leakStatusConflictsWith answers which ones before any of them is written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Under the title that says which object, in the colours a chain draws a status in, since it is the same answer read in one place rather than a dozen. Loud for the two statuses that mean something and quiet for the third: most of a heap dump is objects nothing knows either way about, so a shaded Unknown on every tab would be a banner nobody reads by the time it says something. "Set by hand…" asks for a status and the reason, and then, only when there is one, for what the new status disagrees with: every status it cannot be true alongside, with the reason each was given, because whoever is about to overrule one is the only person who can weigh the two. Keeping the new one flips them and says so in their reasons; undoing leaves the heap dump as it was. Nothing is written until that is answered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The status of the object a tab is on moves out of the title row and into the top of "What it is", under a header, because it is a conclusion and that panel is the evidence for it. Above the bitmap preview, so a tall screenshot can't push it out of view, and the "Set by hand…" button is now a pencil left of the answer, where the eye already is. "Leaking" and "Not leaking" become "Shouldn't be here" and "Meant to be here", "Unknown" becomes "Nobody knows". LeakCanary's leak traces print `Leaking: YES / NO / UNKNOWN`, and matching them was the case for the old words, but calling an object "leaking" reads as it leaking something as easily as being the thing left behind — and a leak here is a reference, not an object. The words the inspectors' answer deserves are the ones this module's own prose already used. Only the display text moves: LeakStatus, LEAKING and leakStatusesOf keep Shark's names. And findLeaks is read through the statuses too, which it wasn't. Setting one changes which objects are leaks, not only how one of them reads: mark an object leaking halfway up a chain and it becomes a leak, while what it holds drops off the list, being only in memory because of it. So the candidate set follows the statuses, RootPathSearch goes round what a hand marked exactly as it goes round what the inspectors found, and the list is worked out again per set of statuses. The cost is that a leak fingerprint matches LeakCanary's only while nothing is set by hand — which is the stretch of chain a status is set to move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The verdict on an object is read a dozen times down one chain and once per row of the leaks screen, so it has to be a label rather than a sentence: "Shouldn't be here" and "Meant to be here" were neither skimmable there nor short enough for a 320dp panel. "Leaked" keeps what the longer wording was for — this repo's rule is that a leak is a reference and not an object, and "leaking" reads as the object leaking something as easily as being the thing left behind, which the past tense settles. "Needed" is what an inspector actually recognizes about the other end. The panel's header carries the question so the three labels don't have to, in one word like the Retained and Shallow beside it, and the checkbox that shades them over the map says "Leaked" too rather than being the one surface left calling it something else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No word built on "leak" belongs on an object. A leak is one faulty reference that should have been cleared, and everything under it is retained by that single mistake — so "Leaked" on twenty objects points a reader at the twenty rather than at the one thing to fix. "Stuck" says the object's situation without accusing it, and it is the only candidate that asks a question instead of closing one: something is holding this, what? "Expected" says its presence in memory is legitimate at this point in the app's life. No other analyser has a verdict like this to borrow words from, checked before choosing: JProfiler classifies objects by reference type and by age, YourKit by reachability scope, MAT and dotMemory name places rather than objects. What they share is the frame — JProfiler asks whether objects "are still legitimately on the heap or if a faulty reference keeps them alive" — which is where the name for the culprit edge comes from, and it is now what the leaks screen and the KDoc call it. An object nothing reaches any more stays Stuck like any other: it was expected to be gone, and only the collector not having run keeps it here. A fourth value would have made the verdict mean something different in one corner of the window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pyricau
force-pushed
the
change-it-to-unknown-not-leaki
branch
from
August 20, 2026 14:51
0e13ae8 to
06cb6e0
Compare
The chain already said which objects are stuck and which belong in memory. What it didn't say is which reference between them is the leak, which is the one thing to go and change: a reader following the statuses is being pointed at what the leak left behind rather than at the leak. So the one step that goes from an Expected object straight to a Stuck one reads "Holder.activity · faulty reference", bold, in the red of the objects under it, and the hover says why that reference and not another. Only that shape is marked. A stretch of several references between the two verdicts is a fault at one of those steps with nothing saying which, and a chain with nothing Expected above the stuck object can have its fault further up than the path reaches — a chain of Cleaners nothing recognizes would otherwise get its top reference marked for being where the walk started. So the mark is either the answer or absent, and setting a verdict by hand is what turns a stretch into one step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pyricau
force-pushed
the
change-it-to-unknown-not-leaki
branch
from
August 20, 2026 14:55
06cb6e0 to
45b05cb
Compare
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.
Shark Explorer already worked out a
LeakStatusfor every object of every chain it drew. Two things were missing: the status of the object the tab is on, and any way to disagree with it.The verdict on an object is the first thing "What it is" says
At the top of the details panel, under the object's name, above its size — a conclusion at the head of the column that holds the evidence for it — in the colours the chain beside it uses, with the reason under it, because half of these are about another object: an activity is red because its own
mDestroyedis true, and the view under it is red because the activity is.Loud for the two statuses that mean something, quiet for the third. Most of a heap dump is objects nothing knows either way about, so a shaded, bold answer on every one of them would be a line nobody reads by the time it says something. A glyph as well as a colour (
✓ ? ✗), so it doesn't rest on the colour alone. Nothing at all on the tab a window opens with: the whole heap dump is no object of it.The words are new, and no word built on "leak" is on an object.
Leaking/Not leaking/Unknownbecame Stuck / Expected / Unknown, under a header sayingVerdict— one word each, like theRetainedandShallowlabels beside them, because a verdict is read a dozen times down one chain and once per row of the leaks screen.A leak is one faulty reference that should have been cleared, and everything under it is retained by that single mistake — so
LeakingorLeakedon twenty objects points a reader at the twenty rather than at the one thing to fix.Stucksays the object's situation without accusing it, and it is the only candidate that asks a question instead of closing one: something is holding this, what?Expectedsays its presence in memory is legitimate at this point in the app's life. The two aren't antonyms on purpose — an object in use can't be collected either, so the good side has to answer a different question than "can it leave".No other analyser has a verdict like this to borrow words from, which is worth knowing before bikeshedding it. JProfiler classifies objects by reference type ("strongly referenced", "retained by soft references") and by age (
Mark Heap, then "new" and "old"); YourKit by reachability scope (strong, softly, weakly reachable, unreachable, pending finalization); Eclipse MAT names places rather than objects (leak suspect, accumulation point, keep-alive path); dotMemory has key retention paths. None of them labels an object as leaking, because none has watched objects or framework inspectors to do it with. What they share is the frame, and JProfiler says it outright: whether objects "are still legitimately on the heap or if a faulty reference keeps them alive". That is now what this repo calls the culprit — the reference between the lastExpectedobject and the firstStuckone, which is whatLeakGroup.suspectPathstarts at and what every row of the Leaks screen is named after.The verdict means the same thing everywhere, including on a watched object nothing reaches any more: it was expected to be gone, and only the collector not having run keeps it here, so it reads
Stucklike the rest. Where it sits on that scale is what the Leaks screen'sUnreachablesection is for; a fourth value would have made the verdict mean something different in one corner of the window.One
LeakStatus.statusTextis where the words live, so the chain, the panel, the dialog, the checkbox that shades them over the map and the reasons propagated along a chain (Activity↓ is expected,Activity↑ is stuck) all say the same thing. The identifiers don't move:LeakStatus,LEAKING,leakStatusesOfstay Shark's names, because the code is where agreeing withshark.LeakTraceObject.LeakingStatusmatters.The chain marks the faulty reference
A verdict is about an object, and propagation paints every object under a leak as stuck — twenty rows of red for one mistake, which is the shape of the misdirection the words above were changed to avoid. So the chain marks the reference itself:
Holder.activity · faulty reference, bold, in the red of the objects it left behind, on the one step that goes from anExpectedobject straight to aStuckone. It is the one line of a chain that says where to go and change code, and where a leak is a single reference it is the same one the Leaks screen names that leak after, so a row there and the chain opened from it name one thing.It is decided once, over the whole path —
faultyReferenceIndexOrNull, called fromwithLeakStatuses— and carried onPathReference.isFaultyfor the drawing to read. Deciding it in the window from the steps on screen looks equivalent and isn't: a pane draws stretches of a chain (stepsBelow,stepsAfter, a swapped-in way round), so the object that ends the stretch can sit above what it shows.Nothing is marked unless one step crosses between the two verdicts. LeakCanary underlines the whole stretch between them, and
suspectSubpathstill names a leak after all of it — but a mark is a claim about one reference, and there are two shapes where the path doesn't support one. With objects nothing knows either way about in between, the fault is at one of those steps and the heap dump doesn't say which. With nothingExpectedabove the stuck object at all, what holds it may be something that should have let go too, so the fault can be further up than the path reaches — the first version of this marked the top of the stretch and put· faulty referenceon the top reference of a chain ofCleaners with no verdict on any object of it, which is a reference named for being where the walk started. A guess drawn in the same bold red as an answer costs more than no mark, because being the one line to act on is the whole of what the mark is for. Overruling a verdict is what closes either gap: say what you know about an object in between, and the mark appears on the step that leaves.A pencil overrules it
Left of the answer, where the eye already is, rather than a "Set by hand…" button after it. It takes one of the three statuses and a reason, and the reason is not optional: a status set by hand overrules the heap dump, so without the why it is an assertion the next reader — a colleague, an agent, the same person in a month — has no way to check. What the inspectors said is kept as the record of what was overruled, exactly the way a conflict between two inspectors is recorded, and
set by hand — …marks it wherever it is read.Overriding always wins, which is the one place this differs from two inspectors disagreeing. There the object still being needed wins, because two inspectors are two halves of one automated reading. A hand is not that, and a rule that weighed the two would mean a status that can't be set to the one an inspector already picked.
And what it contradicts is shown, not settled quietly
A status decides what the objects it holds or is held by are, so two hand-set statuses disagree exactly when one of the objects reaches the other. When the one being set does that, every status it disagrees with is listed by name, with the reason it was given and what it would become. Keep this and flip those keeps the new one and flips them, with what they said kept as part of the new reason; Undo leaves the heap dump exactly as it was. Nothing is written until one of the two is picked, and then it is one write of the lot.
Flipping always resolves it, which is why solving is one button:
NOT_LEAKINGpropagates upwards only andLEAKINGdownwards only, so the pair that can disagree is always those two.The leaks are read through them too
Setting a verdict changes which objects are leaks, not only how one of them reads. Mark an object as stuck halfway up a chain and it becomes a leak, while whatever it was holding drops off the list — that object is now only in memory because of this one, which is the rule
foldedIntoWhatHoldsThemalready applied to what the inspectors found. Mark a leak as expected and it leaves the list, and what it held can become a leak of its own.So the candidate set is the dump's own minus everything set to anything but
LEAKINGplus everything set to it;RootPathSearchgoes round what a hand marked exactly as it goes round what the inspectors did, or a leak would be grouped by a chain that disagrees with the statuses drawn on it; and the list is worked out again per set of statuses and kept until the next one, since a status is set by hand and this is seconds.The price is that a
LeakGroup.leakFingerprintmatches the one LeakCanary prints for the same objects only while nothing is set by hand — the fingerprint hashes the stretch between the last expected object and the first stuck one, and moving that stretch is what setting a verdict is for.Notes on the shape of it
summarize,rootPathTo,independentPathsBetween,independentPathsFromRoots,findLeaksandisBelowLeakingObjectall take aLeakStatusOverrides, and the window's effects are keyed on it. A value rather than state, because the tree is read from one thread while the window is composed on another: overrides in the tree would mean a chain drawn from one set of them and the panel beside it from another.~/.shark-explorer/leak-statuses, columns named at the top, reasons escaped, lines sorted by address — so it reads as evidence and can be diffed or pasted into an issue. A line that can't be read is skipped with a log line rather than thrown over, since it is hand editable on purpose.Testing
LeakStatusTestfor the propagation with statuses set by hand in it,HeapLeakStatusTestfor the panel, the chain,reaches, the conflicts and the leaks list changing under a status — including a chain that stops going round an object once a hand says it is expected —LeakStatusFileTestfor the round trip through disk, andLeakStatusSectionTestfor the panel, the pencil, the reason being required, taking one back off, the conflict flow both ways, and the leaks screen following a status set before the window opened.For the mark:
HeapLeaksTestfor it being the very reference the Leaks screen names the leak after, and for the three ways a chain carries none — nothing stuck on it, nothingExpectedabove what is stuck, and two references between the two verdicts;HeapLeakStatusTestfor it appearing one step further down once a hand says the activity is expected; andLeakStatusSectionTestfor the window drawing it and for it going away — the step still drawn — once the verdict under it is overruled. Two heap dumps inLeakHeapDumps.ktare new, anApplicationholding a destroyed activity directly and through a holder, since every dump there until now had objects nothing knows either way about above the leak../gradlew :shark:shark-explorer:shark-explorer-core:check :shark:shark-explorer:shark-explorer-app:checkpasses.🤖 Generated with Claude Code