Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dd39c41
Let an agent investigate the heap dump in a window somebody is watching
pyricau Aug 21, 2026
ab707a7
Name a harness window after the task it is for
pyricau Aug 24, 2026
c22ab28
Say STUCK and EXPECTED everywhere, and Shark's words nowhere
pyricau Aug 25, 2026
7682d4b
Show what an agent did, in the window it did it in
pyricau Aug 25, 2026
acf1974
Name the objects an agent asked about the way a tab does
pyricau Aug 25, 2026
2f96751
Name the solved leak above the chain, and to an agent
pyricau Aug 25, 2026
2873d2a
Let an agent reach every screen and press every button
pyricau Aug 25, 2026
649106b
Serve an agent that has no window to investigate in
pyricau Aug 25, 2026
eae7d4d
Answer an agent with the link to what it showed
pyricau Aug 25, 2026
9a7cc96
Drag the note's bottom edge to make it taller
pyricau Aug 25, 2026
add3fb8
Refuse an argument a tool does not take
pyricau Aug 25, 2026
d13390b
Send an agent to the code, at the version the dump is of
pyricau Aug 25, 2026
c61f063
Write down what an agent concluded, not only that it did
pyricau Aug 25, 2026
375ed39
Name the heap dump a run was pointed at while it is still indexing
pyricau Aug 25, 2026
fb7aa3e
Measure whether an agent can solve a leak
pyricau Aug 25, 2026
1826618
Name the object an agent asked about where the heap dump is open
pyricau Aug 25, 2026
fef4199
Leave the one place no argument named unnamed in a session
pyricau Aug 25, 2026
ffeb51b
List the agents that worked on this heap dump, and no others
pyricau Aug 25, 2026
e450413
Call a tool from a shell, at the window that is already open
pyricau Aug 25, 2026
f18da6c
Answer an agent with what has already been tried on this heap dump
pyricau Aug 26, 2026
5faeb13
Head each heap dump's agent logs with the file, this window's first
pyricau Aug 26, 2026
5a5f9ba
Tell an agent this surface exists, and that it isn't only about leaks
pyricau Aug 26, 2026
102eb34
Link the thing a call was about, and give every call one
pyricau Aug 26, 2026
c0297d6
Name the heap dump in a link, and the window only as a refinement
pyricau Aug 27, 2026
2370e15
Look up where a heap dump is rather than say it in the link
pyricau Aug 27, 2026
5716c2f
Say only which heap dump a link is about, and ask when that isn't enough
pyricau Aug 27, 2026
c0f2d2f
Look a link's heap dump up when the window that has it failed to open it
pyricau Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .claude/skills/shark-explorer/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
name: shark-explorer
description: "Use when investigating an Android or JVM heap dump (.hprof): what is leaking and why, what is holding an object, what the biggest objects are, what a process is spending its memory on. Drives Shark Explorer, which reads the dump in a window a person can watch, from a shell or over MCP."
allowed-tools:
- Bash
---

# Investigating a heap dump with Shark Explorer

Shark Explorer is a desktop app that reads a heap dump, and every screen and button of it is also a tool you
can call. So you read the dump **in the window somebody is looking at**: what you look at, they can look at,
and the verdicts and notes you leave are on their screen and in files that outlive the run.

It is not only for leaks. `list_leaks` is the dump's own answer about what shouldn't be there, and
`dominator_tree` is what the memory is actually going on, which is a different question — a heap where nothing
is leaking still has a biggest object.

## Start by working out which case you are in

**Something is already open.** Ask, and the answer carries the method to follow, the file names every other
tool names a dump by, and any verdicts somebody has already reached:

```bash
"/Applications/Shark Explorer.app/Contents/MacOS/Shark Explorer" --agent open_heap_dumps \
reason="Finding out what is already open"
```

If that says nothing is open, it opened a window for you, so the same command again lists it.

**You have a file.** A dump that came with a bug report, or one you took earlier:

```bash
… --agent open_heap_dump path=/absolute/path/bug-4821.hprof reason="The dump the report came with"
```

It answers once the dump is readable, which on a large one is a wait rather than a moment.

**You need to take one.** From a device or emulator `adb` is connected to:

```bash
… --agent list_devices reason="Finding the device"
… --agent list_devices device=emulator-5554 reason="Finding the process to dump"
… --agent dump_heap device=emulator-5554 process=com.example.app reason="Reproduced the bug, dumping now"
```

`dump_heap` collects the garbage first, writes the dump on the device, pulls it and opens it — minutes on a
large app, and one call that does not come back until it is readable. A process can only be dumped if the app
was built debuggable or the whole device build is; `list_devices` says which.

**And before investigating anything, read what has already been tried on that dump:**

```bash
… --agent agent_log reason="Finding out whether somebody has already been through this"
```

An investigation somebody already ran is either the answer or the half of the dump not worth doing again.

## The command line

```bash
"/Applications/Shark Explorer.app/Contents/MacOS/Shark Explorer" --agent <tool> name=value …
```

- `--agent-help` prints every tool, with its arguments and what each one is for. `--agent-help <tool>` prints
one tool instead of all of them. **Read that rather than guessing at a tool**, and rather than trusting a
list in a file like this one, which goes stale.
- **Find the launcher first** — the path above is where a `.dmg` install puts it, and the space in it has to
stay quoted:
```bash
ls -d /Applications/"Shark Explorer.app" ~/Applications/"Shark Explorer.app" 2>/dev/null
```
- **Every tool takes `reason`**, which is why you are making the call. It is logged beside the reads it causes
and read afterwards by a person on the *Agent logs* screen, so write the sentence you would say to somebody
watching over your shoulder.
- **Exit code 0** means the answer is the JSON on stdout. **2 means the call was refused**, and the refusal on
stderr is the next thing to do, not an error to retry. **1** means nothing was there to answer it.
- **Addresses are `0x…`, exactly as the surface writes them.** Never decimal: a heap dump's addresses do not
survive a JSON number.
- **A call is about one heap dump**, and `heapDump=<file name>` says which — needed once more than one is
open, and the window id instead in the one case a name cannot answer, which is the same file open twice.
The `shark://` link `show` and `conclude` answer with names the dump too, so it still opens after this run
has ended: **put those links in your reply** rather than describing which screen to open.
- `--agent-run=<pid>` picks between several open runs. `--agent-session=<name>` says which investigation these
calls are one of; by default one shell is one session, so what you did reads as one row of that screen
rather than a row per call.

**Over MCP instead, if your client can be configured**, which gets the same tools with their schemas in band:

```json
{ "mcpServers": { "shark-explorer": {
"command": "/Applications/Shark Explorer.app/Contents/MacOS/Shark Explorer",
"args": ["--mcp-stdio"]
} } }
```

Add `--no-ui` for a machine with no screen — a build server, or a dump at the far end of an ssh session.
Everything works the same except `show`, which has nowhere to put a tab and says so; it still answers with the
link, since a link names the heap dump rather than a window.

## What to do with it

**The method comes with the tools.** `open_heap_dumps` hands back the whole of it — what a leak is, the three
zones of a chain, how a verdict spreads, and the order that finds the faulty reference. Follow that; it is
[the LeakCanary method](https://engineering.block.xyz/blog/the-leakcanary-method) as the tools enforce it, and
it does not need repeating here.

Two things about it that are easy to miss:

- **`conclude` will refuse you** until the heap dump agrees that one reference is at fault, and the refusal
says which of the three reasons it is. That is the surface working. Go and do what it says — usually
`set_verdict` on the object it named — rather than reporting a root cause it would not accept.
- **Isolating the reference is not the root cause.** It says where the problem is, not how it happened, so the
method sends you to the code at the version this dump is of, and tells you how to work out which version
that is.

**When the question isn't a leak**, the tools are the same and the order is yours. What is big is
`dominator_tree`, top down, and `describe_object` on whatever it names; what is holding one thing is
`ways_held`; what instances of a class there are, and how much they retain between them, is `find_objects`.
`show` puts any of it on the person's screen, and `take_note` writes what you found where they and the next
reader will find it.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,10 @@ val modulesWithoutPublicApi = listOf(
"leakcanary-app-db",
"leakcanary-app-service",
"shark-cli",
"shark-explorer-agent",
"shark-explorer-app",
"shark-explorer-core",
"shark-explorer-eval",
"shark-explorer-jdwp",
"shark-hprof-test",
"shark-test",
Expand Down
6 changes: 5 additions & 1 deletion config/detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ complexity:
active: false
ignoredLabels: ""
LargeClass:
#LeakCanary - excluded tests, and increased from 600 to 700: a test class is as long as the story it
# walks, which is why TooManyFunctions already excludes them, and shark-explorer-agent's AgentTools is a
# registry whose length is tool descriptions rather than behaviour.
active: true
threshold: 600
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
threshold: 700
# Leave me alone!
# LongMethod:
# #LeakCanary - increased from 60 to 90
Expand Down
51 changes: 49 additions & 2 deletions docs/shark-explorer-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@ uses, without the one for a newly recognized library leak:
* ✨ Right click anything the window can take you to — a tab, a rectangle, a row, a field — and copy a
`shark://` link to it, beside opening it in a new tab. Clicking one brings the app to the front and
opens that place in a new tab: an object, a filtered object list, the leaks with the same groups
unfolded. See [Link to a tab](shark-explorer.md#link-to-a-tab).
unfolded. A link names the **heap dump** and nothing else — `shark://bug-4821.hprof/leaks`, short enough to
read in a sentence — so it goes on working after the window it was copied from has gone: it opens the place
in a window that has that dump, and opens the file in a new window when none has. Where that file is never
goes in the link: every heap dump opened is written down in `~/.shark-explorer/heap-dump-paths`, the last
200 kept, and following a link looks it up there. A link about a heap dump this machine can't find asks for
the file, and one about a name two heap dumps share asks which of them. See
[Link to a tab](shark-explorer.md#link-to-a-tab).
* ✨ **Notes**: every location takes a markdown note, kept between runs, and the tab strip marks the tabs
whose location has one. A note belongs to the location rather than to the tab, so two tabs on one
location are one note. Class names, addresses and `shark://` links written in a note become links back
into the window, shortened to read as prose, and GitHub URLs are shortened the way GitHub shortens them.
See [Take notes](shark-explorer.md#take-notes).
* ✨ Drag the line along the bottom of a note to give it more of the window or less, the same way the edges
between the panes are dragged sideways. Per window rather than per tab, and never more than its share of
the window however far it is dragged. See [Take notes](shark-explorer.md#take-notes).
* ✨ Right click ← or → for the list of everywhere that arrow leads, so going back four moves is one
click rather than four.
* ✨ **Verdict**: whether the object a tab is on is stuck in memory — `✗ Stuck`, `✓ Expected` or `? Unknown`
Expand All @@ -38,9 +47,47 @@ uses, without the one for a newly recognized library leak:
object as stuck makes it a leak and takes whatever it holds off the list. Kept between runs in
`~/.shark-explorer/leak-statuses`, one file per heap dump.
See [The verdict](shark-explorer.md#the-verdict).
* ✨ **Hand a heap dump to an agent**: the window is an MCP server too, so an agent investigates the heap dump
you have open — the same tree, the same verdicts, the same notes — and `show` puts what it is looking at on
your screen. What it can be held to is the point: every call has to say why it was made and lands in the
run's log beside the reads it caused, a verdict needs a reason another reader can check exactly as yours
does, and reporting a root cause is refused until the chain names one faulty reference. There is no screen
it can't reach and no button it can't press — the treemap as a tree of retained sizes, the notes read and
rewritten as well as added to, `Open heap dump…` for a file nobody has open, and `Take heap dump…` down to
picking the process off a device — because a surface with less than that answers "ask your human to click
something". Point any MCP client at the installed app with `--mcp-stdio`.
See [Hand it to an agent](shark-explorer.md#hand-it-to-an-agent).
* ✨ **An agent no longer needs a window to have been opened for it.** With nothing running, `--mcp-stdio`
opens one — on the heap dump its command line named, if it named one — and leaves it open for whoever comes
back to it. And with `--no-ui`, the tools are served from that process with no window anywhere, for a build
server or a heap dump at the end of an ssh session: everything works the same except `show`, which says it
has nowhere to put a tab rather than answering that it showed you something — and hands back the link all
the same, since a link names the heap dump, so whoever reads the answer can open the place nobody saw.
Notes and verdicts were never on the screen, so a heap dump investigated with no window opens in one later
with all of it on.
See [Hand it to an agent](shark-explorer.md#hand-it-to-an-agent).
* ✨ **The method sends an agent to the code, at the version the heap dump is of.** Isolating the reference
says where the problem is and not how it happened, so the method that comes with the tools also says how to
work out which framework, app and library versions this dump is of — and what to ask for rather than guess,
since an app's own version number never reaches the heap.
See [Hand it to an agent](shark-explorer.md#hand-it-to-an-agent).
* ✨ **An agent answers with links into the window.** `show` and `conclude` hand back the `shark://` link to
what they put on screen, and the method the tools come with tells an agent to put those links in its reply —
so a sentence in a chat window, a pull request comment or a bug report carries a way into the heap dump
rather than instructions for finding the object again by hand.
See [Hand it to an agent](shark-explorer.md#hand-it-to-an-agent).
* ✨ **Agent logs**: every agent that has connected to the app is a row on a screen of its own, and opening
one is everything that agent did — what each call did, which object it did it to, and the sentence it gave
for making it, with the refusals in red. A row leads where the call went, so reading what an agent did and
going to look at it are one move — including a row about a heap dump this window hasn't got, which opens
that dump, and whose link is there to copy like every other. Kept in `~/.shark-explorer/agents/sessions`,
one file per session and the newest hundred kept, so a session outlives the window it was worked in.
See [Hand it to an agent](shark-explorer.md#hand-it-to-an-agent).
* ✨ **The chain marks the faulty reference**: the one step going from an `Expected` object straight to a
`Stuck` one reads `Holder.activity · faulty reference`, which is the leak itself rather than one of the
objects it left behind, and the same reference the **Leaks** screen names that leak after. A chain whose
two verdicts are further apart than one step carries no mark, since which reference in between is at fault
is what isn't known — overrule a verdict in between and the mark appears.
is what isn't known — overrule a verdict in between and the mark appears. Once there is a mark, a
`Leak solved` line above **What holds it** names that reference where the eye starts, rather than leaving
it to be found tens of steps down a chain, and an agent reading the chain is answered with the same name.
See [The verdict](shark-explorer.md#the-verdict).
Loading