Skip to content

Draw the treemap into the conversation an agent is in - #2970

Open
pyricau wants to merge 1 commit into
mainfrom
build-that-for-mcp-in-shark-di
Open

Draw the treemap into the conversation an agent is in#2970
pyricau wants to merge 1 commit into
mainfrom
build-that-for-mcp-in-shark-di

Conversation

@pyricau

@pyricau pyricau commented Aug 29, 2026

Copy link
Copy Markdown
Member

Adds draw_treemap: the treemap itself, drawn where the person is talking to their agent, and pressable
from there.

Every other tool on this surface answers a model in words, which is the right shape for a model and the
wrong one for a treemap — "the biggest thing is a 42 MB bitmap cache" is a sentence, and the picture is a
hundred rectangles with the cache filling a quarter of them. This one draws the same layout, colours and
leak shading the window draws, over the same dominator tree.

It is a document, not an image

The drawing is Remote Compose,
played on a canvas by a page this server hands to an MCP Apps host. Which
buys the part worth having: the rectangles carry click areas, so pressing one is the client asking Shark
Dive for the treemap under that address, and the title strip leads back up. No model in that loop, no image
round trip, no token per step — one resources/read, tens of milliseconds. The agent finds out where somebody
got to through ui/update-model-context, which a host defers until the next user message, i.e. exactly when
it needs to know.

The agent cannot see the drawing, so this replaces nothing it would have read: dominator_tree is the
same tree in words and is still the one to call. The tool description says so.

A resource, never a tool result

McpSession.toolResult pretty-prints an answer into the text a model reads. Measured on this branch:

Heap dump Canvas Document As base64 Read
leak_asynctask_o.hprof, 7.8 MB 900×560 178 KB 237 KB 482 ms, then 17 ms
large-dump.hprof, 39 MB 900×560 456 KB 608 KB 1,666 ms, then 63 ms

So a tool result would be a quarter to two thirds of a megabyte of base64 in the context of a model that
cannot see it. As a resource it goes from the server to the page, and the model is told a URI.

And the URI says everything — shark-dive://treemap/{heapDump}/{object}?width&height — which is what keeps
the session stateless: a model never knows the size of the client's canvas, so draw_treemap draws nothing
itself. It checks the object is a node of the tree and hands back a URI; the page fetches at the size it
measured, refetches rooted elsewhere when somebody presses, and lays out again rather than scaling when the
panel is dragged.

Where up leads

A player has no history and no chrome of ours to put a button in, so the title carries a click area and reads
↑ <where you are>. That needed HeapDominatorTreemap.parentOf: the root's children are gathered into piles
by class, so an object nothing in particular holds is drawn inside a pile while its dominator is the whole
heap dump, and a pile has no dominator at all. Going up by the dominator was a drawing of a pile with no way
out of it — found by pressing one in a browser, not by reading the code.

The player is vendored, reproducibly

remote-compose-player.js, 422,192 bytes, built from
camaelon/remotecompose-experiments at
2168eb72a5f2de29029e9e3d88daf9c3abcf5a2a, Apache 2.0, byte identical on a rebuild. Vendored because the
upstream package is private: true — there is nothing to depend on — and because a page that fetched a
player over the network would need a CSP allowing a domain, which is the one thing this app doesn't ask a
host for. Provenance, the exact build command and the licence are in remote-compose-player.LICENSE.txt.

Testing

  • TreemapDocumentTest writes a real document from a synthetic dump and inflates it back, asserting the two
    things the navigation is built on: every named rectangle carries the address a press leads to, and the
    title carries the way back out (and doesn't, for the whole heap dump).
  • HeapDiveTest covers parentOf where it differs from dominatorOf.
  • McpSessionTest covers the page being served with the player in it, the drawings being a template, a
    drawing read as bytes at the size asked for, and an unknown URI being a resource error rather than an
    internal one.
  • The whole loop was driven by hand in headless Chrome against the real --no-ui --mcp-stdio server: a
    host page around the served HTML, resources/read forwarded back to Shark Dive, rectangles pressed over
    CDP. What that ends with is a second resources/read at a different address and a ui/update-model-context
    naming it — a drawing that renders and doesn't navigate looks perfect in a screenshot.

./gradlew build is green.

shark/shark-dive/notes/remote-compose.md has the rest: what the writer can't do (it cannot measure text,
there are no brushes), the player's traps (the canvas is 256×256 until told otherwise; click areas arrive on
a different channel from named actions), and how to read a document back in a test.

🤖 Generated with Claude Code

Every other tool answers a model in words, which is the right shape for
a model and the wrong one for a treemap: "the biggest thing is a 42 MB
bitmap cache" is a sentence, and the picture is a hundred rectangles
with the cache filling a quarter of them. `draw_treemap` puts that
picture where the person is — the same layout, colours and leak shading
the window draws, over the same dominator tree.

It is a Remote Compose document rather than an image, so the rectangles
carry click areas: pressing one is the client asking this server for the
treemap under that address, and the title strip leads back up. Nobody's
model is in that loop, so walking into the heap costs one round trip and
no tokens. The agent finds out where somebody got to through
`ui/update-model-context`, which a host defers until the next message.

And the drawing is an MCP resource, never a tool result. Measured, one
whole-heap-dump drawing of `large-dump.hprof` at 900x560 is 456 KB, 608
KB as base64 — a tool result would put that in the context of a model
that cannot see it. The URI says which heap dump, which object and what
size, so the page fetches at its own canvas size and lays out again
rather than scaling when it is dragged, and the session stays stateless.

The player is vendored because it is not published, built from a pinned
commit with a command that reproduces it byte for byte; the licence file
beside it carries the provenance. `shark/shark-dive/notes/remote-compose.md`
has what the writer cannot do, the player's traps and the measurements.

`HeapDominatorTreemap.parentOf` is new and is what "up" needed: the
root's children are gathered into piles by class, so an object nothing
in particular holds is drawn inside a pile while its dominator is the
whole heap dump, and a pile has no dominator at all. Going up by the
dominator was a drawing of a pile with no way out of it.
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