Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 9 additions & 4 deletions shark/shark-explorer/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Shark Explorer — agent guide

A desktop app that renders a heap dump's dominator tree as a navigable treemap, as rings around a
centre, or as a stack of rows the way a profiler draws a call tree. The long term goal is a YourKit-style
heap explorer; these are the first surfaces.
centre, or as a stack of rows the way a profiler draws a call tree — and the heap dump's own references
as a graph of circles and arrows expanded a click at a time. The long term goal is a YourKit-style heap
explorer; these are the first surfaces.

This file is scoped to `shark/shark-explorer/`. It only records things an agent would get wrong by
reading the source alone — everything else is in the code. Keep it that way.
Expand Down Expand Up @@ -42,6 +43,10 @@ What follows for the UI: a composable never holds a tree, only what was already
laid out, labelled view is a `TreemapPresentation` or a `RadialPresentation`, and a selection is a
`HeapObjectSummary`; both arrive a little after whatever asked for them changed.

**The graph shape is the exception, deliberately**: what it draws was read into an `ObjectGraph` a
circle at a time already, so arranging it reads nothing and happens in composition — see
`notes/decisions.md`. A `ViewRequest` is never made for it, and building one throws.

The one thing that isn't thread safe is a `Sequence` a `HeapGraph` hands out — iterating one reads
through it — so a thread reading `graph.objects` needs its own rather than a shared one.

Expand Down Expand Up @@ -380,8 +385,8 @@ Design decisions and findings, kept current as the work proceeds:

- `notes/decisions.md` — stack and structure decisions, with rationale
- `notes/dominator-tree.md` — dominator algorithm findings, memory/perf numbers
- `notes/treemap-rendering.md` — adaptive depth model, the two shapes, bugs in the existing Android
treemap
- `notes/treemap-rendering.md` — adaptive depth model, the shapes and what each is for, bugs in the
existing Android treemap
- `notes/bitmaps.md` — which Android versions put a bitmap's pixels in the heap dump, and the two ways
the ones that don't are fetched off the device

Expand Down
21 changes: 21 additions & 0 deletions shark/shark-explorer/notes/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,27 @@ What made this affordable, on the 38 MB dump in the repo, over all 4,572 rectang
expensive question — are **only asked for the object clicked**, once its chain has come back, and the
answer is drawn into that chain. The pointer has nowhere to put a question that expensive.

## On the graph, a click expands rather than goes there

The graph is the one shape where a click doesn't move the window. It draws the heap dump's references
outwards from where the view is rooted, and the whole of what it says is what has been expanded, so a
click that re-rooted it would throw that away every time — the reader would be building the picture and
losing it in the same gesture. Pressing a circle opens it, pressing it again closes it, and pressing the
cell counting what didn't fit reveals another page (`ObjectGraph.pressing`).

Which leaves the panels as the way to go somewhere from here: a click still describes what it landed on,
so the chain, the details panel and the bar above the view all follow the circle pressed, and going to it
is a click on the name there. That is the same route every other screen uses, so nothing new to learn —
and it keeps "go there" and "show me more" as two different gestures rather than a click and a
double click on the same circle.

**Arranging the graph is done in composition, not on the heap dump's thread.** Everything drawn was read
into `ObjectGraph` already, a circle at a time, so laying it out reads nothing and is a pure function of
state the window holds. Putting it on that thread — where the other three shapes' layouts belong, since
they read the heap dump for every label — cost a spinner flash on every click and three misleading "read
the graph rooted at" lines per shape switch. `ViewRequest` refuses to be built for this shape and the
layout `when` errors on it, so there is one way for it to be arranged rather than two.

## The chain from a GC root is a pane, not a popover

Hovering used to draw the tree's containers as a grey popover following the pointer. What it said was a
Expand Down
58 changes: 53 additions & 5 deletions shark/shark-explorer/notes/treemap-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

Implemented in `shark-explorer-core`: `Squarify.kt` (row layout), `TreemapLayout.kt` (adaptive depth
and hit testing), `RadialLayout.kt` (the same, as rings), `StackLayout.kt` (the same, as a row per
level), `TreemapRect.kt`, `LayoutCell.kt` (what the three layouts have in common),
`HeapDominatorTreemap.kt` (the dominator tree as a `TreemapTree`, and `present()`, which labels and
colours the cells a layout produced), `TreemapPresentation.kt` (a presentation per shape, each with an
`of()` pairing a layout with that). Drawn by `TreemapView`, `RadialView` and `StackView` in
`shark-explorer-app`.
level), `TreemapRect.kt`, `LayoutCell.kt` (what the layouts have in common), `HeapDominatorTreemap.kt`
(the dominator tree as a `TreemapTree`, and `present()`, which labels and colours the cells a layout
produced), `TreemapPresentation.kt` (a presentation per shape, each with an `of()` pairing a layout
with that), plus `ObjectGraph.kt` and `GraphLayout.kt` for the fourth shape, which is expanded rather
than cut. Drawn by `TreemapView`, `RadialView`, `StackView` and `GraphView` in `shark-explorer-app`.

**`of()` is a presentation's own, not a method per shape on `HeapDominatorTreemap`.** It used to be the
other way round, and adding a third shape is what moved it: that class is a 1,200 line heap dump reader
Expand All @@ -18,6 +18,9 @@ strength off a `CellSubject`, and every shape's cells are those. **So a fourth s

## Three shapes, one cut of the tree

(Four shapes now; the fourth is not a cut of the tree and has its own section below. Everything here is
about the three that are.)

A cell is a `LayoutCell`: a `CellSubject` — one node, or the children a node didn't draw — plus a
depth, a weight and whatever geometry its layout adds. `TreemapCell` adds a rectangle, `RadialCell` an
annular sector, `StackCell` a rectangle again, on a row. Everything downstream of the geometry works off
Expand Down Expand Up @@ -69,6 +72,51 @@ It skips one thing the treemap does: **a row doesn't draw its bitmap**. A row is
and a bitmap fitted into 18 dp is a smear — the picture is the treemap's contribution, and asking for it
here would cost a heap dump read and a decode per row for nothing.

## The graph: the heap dump's own references, a click at a time

The other three shapes divide an area between the nodes of the dominator tree. The graph divides
nothing: circles joined by arrows, expanded outwards from the node the view is rooted at, the way Xcode's
memory graph is read. Which makes it **the one shape that draws the heap dump's own references**, so the
only one that can say *which field* holds an object, and whether anything else does.

That difference is what shapes everything about it:

- **What is drawn is state, not a cut.** `ObjectGraph` is which circles have been expanded and what each
of them references, immutable and grown a click at a time; `GraphLayout` arranges it into columns and
rows. A treemap is a function of the tree and the viewport, so it is laid out again on every resize; a
graph is a function of what the reader asked for, so **resizing it doesn't change it** and the reader
pans and zooms instead of the picture fitting the window.
- **It reads the heap dump a circle at a time**, in `referencesFrom` — expanding one object is one read
of what that object points at, kept for as long as the window is open. Collapsing keeps it, so opening
a circle again reads nothing (`GraphViewTest` asserts exactly that). Nothing else about the shape
touches the heap dump.
- **The root is an object, and the whole heap dump is not one.** Rooted at the top of the tree there is
no object to read references off, so `referencesFrom` answers for that node with the tree's own
children — `reference = null`, `isDominator = true` — which draws as unnamed arrows to what the GC
roots hold. One code path, not a mode: every other node answers the same call with real references.
- **Fan-out is paged, not truncated.** A node with 4,000 references is 4,000 circles nobody can read, so
a page is the `REFERENCES_PER_PAGE` (24) heaviest and the rest become one `CellSubject.Group` cell
that reveals another page when pressed — the same subject the other three shapes use for the children
they didn't draw, so labels, colours and selection needed nothing new.
- **Expanding never moves what is already on screen.** `GraphLayout` normalizes the root's centre to
(0, 0) and lays every column out from there, so a circle expanded at the bottom of the picture adds
rows below without sliding the row the reader is looking at.
- **An object is drawn once**, at the shallowest depth it was reached by; a second arrow to it crosses
the picture rather than duplicating the circle. Which is what makes a cycle finite, and what makes
"two things hold this" visible at all.

**Dominators are ringed, and the ring is the same one the chain draws.** `dominatorTree.immediateDominatorOf`
is asked per arrow, so an arrow says both *how* an object is reached and *whether that is the reason it is
alive* — the pair of questions this whole app is about, on one picture.

**The style is shared with the chain pane, in `PathStyle.kt`.** A circle, its badge letter, its ring, the
connector colours, the dash for a weak reference and the arrow head are one set of functions, used both by
`PathDrawing` (a column of circles joined top to bottom) and by `GraphView` (circles joined left to right).
They are the same picture in two arrangements, and two drawings of the same heap dump that don't match are
two things a reader has to learn instead of one. What each keeps for itself is its arrangement: row height,
where the gutter runs, what a click does. `drawArrowHead` takes a direction because of this — the chain's
arrows point down, the graph's point right, and it is the same arrow.

## Depth is area-driven, not a fixed level

A heap dump's dominator tree has ~1 M nodes; a treemap can usefully show a few thousand rectangles.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,17 @@ internal enum class ViewShape(val displayName: String) {
* doesn't spend area on nesting, so the deep end of a chain is drawn and named at full size — and
* therefore the one shape taller than the window, which is why it scrolls.
*/
STACK("Stack")
STACK("Stack"),

/**
* Circles joined by arrows, expanded a click at a time from the node the view is rooted at: the one
* shape that draws the heap dump's own references rather than the tree over them, so the one that can
* say *which* field holds an object and whether anything else does.
*
* Nothing is divided between anything here, so how much is drawn is entirely what was clicked, and the
* view pans and zooms instead of fitting the window. See [shark.explorer.GraphLayout].
*/
GRAPH("Graph")
}

/**
Expand Down Expand Up @@ -191,6 +201,18 @@ internal val STACK_ROW_HEIGHT = 18.dp
internal val MIN_SUBDIVIDE_STACK_WIDTH = 6.dp
internal val MIN_DRAW_STACK_WIDTH = 2.dp

/**
* How far apart the columns of the graph are: a name's width, the arrow between two circles, and room
* on it for the field the reference is held in.
*/
internal val GRAPH_COLUMN_WIDTH = 220.dp

/** And how much room one circle gets down the picture: two lines of text, with air around them. */
internal val GRAPH_ROW_HEIGHT = 44.dp

/** How wide the name beside a circle is, which is the rest of what a click on a node hits. */
internal val GRAPH_LABEL_WIDTH = 170.dp

internal val LABEL_PADDING = 3.dp
internal val MIN_LABEL_WIDTH = 24.dp
internal val MIN_LABEL_HEIGHT = 13.dp
Expand Down
Loading