Skip to content

Take a note on any tab, in markdown that knows the heap dump - #2951

Merged
pyricau merged 6 commits into
mainfrom
add-ability-to-add-save-notes
Aug 10, 2026
Merged

Take a note on any tab, in markdown that knows the heap dump#2951
pyricau merged 6 commits into
mainfrom
add-ability-to-add-save-notes

Conversation

@pyricau

@pyricau pyricau commented Aug 10, 2026

Copy link
Copy Markdown
Member

An investigation is a series of questions about one heap dump, and the answers have so far lived in whatever the reader happened to have open beside the app. Every location now takes a note. Two smaller things the same window asked for came with it: the arrows list where they lead, and the window says what it is about.

✎ Add Note hangs under the title that says what the tab is on, and the note it starts is drawn under that row and above the three panes that read it — a note is about the whole of what the tab is showing, so under the title it is about and above everything that describes it. Clicking the button turns that space into a plain markdown box with Save and Cancel; saving draws the note where the box was. A location nobody has written about has nothing there at all, and the button goes away as soon as there is a note, which carries its own ✎ Edit — so a section that is on every tab costs nothing on the tabs it has nothing to say about.

A note belongs to the location, not to the tab: two tabs on one location are one note, and both show what the other is typing. Location rather than object, because three of the keys are lists and one is a group of objects — Place.noteKey is the definition.

What makes it worth having in the app rather than in an editor is that the names written in it are read against the heap dump:

What you write What it reads as What clicking it does
com.example.Holder Holder opens the class in a new tab
0x82182c00 Holder instance (0x82182c00) opens the object in a new tab
shark://abcd2345/starred Starred goes to that window's starred list, or explains that the window is gone
https://github.com/square/leakcanary/pull/2941 square/leakcanary#2941 opens it in the browser
https://example.com/thing unchanged opens it in the browser

A class name that isn't in the dump, or an address nothing lives at, stays exactly as typed — the note is still readable when it's about a dump other than the one open, and nothing is ever rewritten on disk, only on screen. Reading the markdown is in memory; asking the heap dump what the names mean is a read, so it happens once per save, and not at all for a note that mentions nothing.

shark:// links are followed through the same DeepLinkPeers.follow an OS link arrives through, so a link copied from a tab (⌘-click, or the right click menu) pasted into a note goes back to that tab, in that window, even across runs of the app.

What a note is attached to

Place.noteKey() is the whole of it: a note is filed under what its tab is about, not under the tab or the place.

A place carries the state of its screen as well as its subject — what the object list is filtered to, which leaks are unfolded, how many objects a pile of small ones stands for at this window width. Keyed on the place itself, a note would follow the search box: typing a letter into it would be moving to a different notepad. Keyed on the tab, it would die with the tab. So: one note per object, one for the object list however it is filtered, one for the leaks however they are unfolded, one for the starred objects — and the whole heap dump is an object like any other here, so the tab a window opens with is where the note about the dump as a whole goes.

That also means the same object open in two tabs, or in two windows, is one note rather than two saving over each other.

The strip marks a tab whose place has a note with a , because a note nobody is reminded of is a note nobody comes back to. Which tabs those are is one directory listing rather than a file opened per tab.

Where the notes live

Markdown files in the app's own directory: a directory per dump (heap.hprof-1f3a9c0b/), a file per place inside it (heap-dump.md, object-0x82182c00.md, leaks.md). Not beside the heap dump — a dump is opened from a temporary file, a read-only mount, a directory pulled off a device, or a checkout of this repository, so writing beside it means littering some places and failing on the rest. A file per place rather than one document with sections, so a save touches only the note that was typed into and nothing has to be parsed back out of a document that also holds someone's own headings. The box shows the path while you write, and the file is plain markdown, because a note about a leak gets pasted into an issue or read months later.

Not losing what someone typed

The notes are the one thing in the app nobody else has a copy of, so:

  • Nothing is written before the file has been read, and ✎ Add Note is disabled until then. An empty box over a note still arriving off the disk is that note deleted a save later. The read is started by the window rather than by the section, since a section that has nothing to draw yet isn't there to start it.
  • The draft is the run's rather than the screen's, held in PlaceNotes: clicking another tab half way through a sentence and coming back finds the sentence. It is not filed on the way out, which is the same answer Cancel gives.
  • Writes go to a .partial file and are renamed over, so a run killed halfway through leaves the previous note rather than half of the new one.
  • A read or write that fails says so in the section in red and in the log, and the draft is kept so the save can be tried again.

List where an arrow leads on a right click

A tab that has walked twenty objects down a chain is one where getting back to where the walk started is twenty clicks and a guess about which of them it was. Right clicking ← or → now lists everywhere that arrow leads, nearest first and named the way the tab strip names a place, so any one of them is one click.

NavigationHistory.backEntries/forwardEntries are that list and goBack(steps) is the move, clamped rather than checked because the list a click came from and the history it lands in are the same value one recomposition apart. The nearest fifteen, since the menu is drawn where the pointer is and one taller than the window has entries nobody can reach. An arrow that leads nowhere keeps the disabled button it was: an empty menu under the pointer reads as the window having lost the history rather than as there being none.

Say what the middle pane answers, and make the tab's subject a title

The panes either side of the map are headed "What holds it" and "What it is", and the map between them was headed nothing at all, which left the two of them reading as a pair with a picture wedged between rather than as three questions about one object. It gets "What it holds", the name its fold button already used, and the fold control moves into that header like the other two.

That row is now two things with a rule between them: how the tab got here — the arrows — and what it is on, which is the title with what the notes offer under it. One undifferentiated strip of controls is what made anything at the end of it read as belonging to the window rather than to the object named beside it; the grouping says which, so nothing has to be labelled to say so. IntrinsicSize.Min on the row is what makes the rule as tall as the row, the title being one line on the whole heap dump and three on an object.

✎ Add Note sits inside the second of those, under the title, of the four places it could be: before the title puts the action ahead of its subject and moves the title depending on whether a note exists; hugging the end of the title moves the button on every click, the title being the object the tab is on; the far end of the row keeps one position but is as far from the title as the window is wide, and reads as the window's button rather than as this object's. Under the title is where its own result appears — the note opens exactly there — and it reads in the order it happens. What that costs is a line of every tab nobody has written about, so it is drawn small and with no padding of its own, which also lines its label up with the title.

The object the tab is on is set as a title where it is named above the panes — the one place in the window where it is the subject rather than a mention of an object among others, since everything under it is about that object. ObjectIdentity takes the style for its first line rather than picking one, so the same three lines are unchanged everywhere else.

Tests

50 unit tests in shark-explorer-core over the markdown reading, the GitHub shortening, resolution against a real dump and where a note is filed — including that filtering the object list, unfolding a leak and resizing a pile of small objects all stay on one note. 7 more over the history a right click lists and moving several steps at once. 15 headless UI tests in shark-explorer-app cover each row of the table above, the tab a class link opens, that Cancel throws a draft away while Save puts it on disk, that changing a note starts from what was saved, that a note is only about the place its own tab is at, that two tabs on one place are one note, and that a place nobody has written about is the button and nothing else until saving takes the button away.

🤖 Generated with Claude Code

@pyricau
pyricau force-pushed the add-ability-to-add-save-notes branch from b204b11 to d21eace Compare August 10, 2026 11:32
@pyricau pyricau changed the title Take notes on a heap dump, in markdown that knows the dump Take a note on any tab, in markdown that knows the heap dump Aug 10, 2026
@pyricau
pyricau force-pushed the add-ability-to-add-save-notes branch 2 times, most recently from c474389 to 717f65b Compare August 10, 2026 14:45
pyricau and others added 3 commits August 10, 2026 17:09
An investigation is a series of questions about one heap dump, and the answers
have so far lived in whatever the reader happened to have open beside the app.
Every tab now takes a note: ✎ Add Note, at the end of the row that says where the
tab is, turns into a plain markdown box with save and cancel, and saving draws the
note under that row and above the panes. There because a note is about the whole
of what the tab is showing, and only there once there is one — a tab nobody has
written about is the button and nothing else, and the button goes away as soon as
there is a note, which carries its own ✎ Edit.

What makes it worth having in the app rather than in an editor is that the names
in it are read against the heap dump. A class name that exists shows up as its
simple name and opens the class in a new tab. Anything starting with 0x that
resolves reads as "Holder instance (0x82182c00)" and opens the object. A
shark:// link is followed exactly as one arriving from the OS is, so a link
copied from a tab goes back to that tab. GitHub URLs shorten the way GitHub
shortens them, and every other http link opens in the browser. Anything this
dump has nothing for is left exactly as it was typed.

A note is filed under what its tab is about rather than under the tab or the
place: the object, the object list however it is filtered, the leaks however
they are unfolded. Keyed on the place itself it would follow the search box, and
keyed on the tab it would die with it. So the same object in two tabs, or in two
windows, is one note, and the tab strip marks the tabs that have one from a
single directory listing.

The notes are markdown files in the app's own directory, one per dump and one
per place, because a dump is opened from wherever it came from and writing
beside it means littering some places and failing on the rest. Nothing is
written before the file has been read, and the button that starts a note is
disabled until then, so a slow or failed read can never be written over by an
empty note. The draft being typed is the run's rather than the screen's, so
leaving the tab half way through a sentence and coming back finds the sentence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tab that has walked twenty objects down a chain is one where getting back to
where the walk started is twenty clicks and a guess about which of them it was.
Right clicking ← or → now lists everywhere that arrow leads, nearest first and
named the way the tab strip names a place, so any one of them is one click.

NavigationHistory.backEntries and forwardEntries are that list and goBack(steps)
is the move, clamped rather than checked because the list a click came from and
the history it lands in are the same value one recomposition apart. The nearest
fifteen, since the menu is drawn where the pointer is and one taller than the
window has entries nobody can reach; everything past them is still one click of
the arrow at a time away.

An arrow that leads nowhere keeps the disabled button it was, because an empty
menu under the pointer reads as the window having lost the history rather than
as there being none. Naming the entries is why every place of every history is
named now rather than only the one each tab is on: a menu entry called "…" is a
move nobody will make.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panes either side of the map are headed "What holds it" and "What it is",
and the map between them was headed nothing at all, which left the two of them
reading as a pair with a picture wedged between rather than as three questions
about one object. It gets "What it holds", the name its fold button already used,
and the fold control moves into that header like the other two.

The object the tab is on is set as a title where it is named above the panes,
which is the one place in the window where it is the subject rather than a
mention of an object among others: everything under it, three panes or a list
and the note, is about that object. At body size it read as one more label in a
bar of them. ObjectIdentity takes the style for its first line rather than
picking one, so the same three lines stay the same everywhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pyricau
pyricau force-pushed the add-ability-to-add-save-notes branch from 717f65b to b33947c Compare August 10, 2026 15:09
The row above the panes was one undifferentiated strip: two arrows, the
title, and the button that starts a note at the far end of it. A rule
after the arrows makes it two things instead — how the tab got here, and
what it is on — which is what says the note button belongs to the object
named beside it rather than to the window.

Which is also the answer to where that button goes. Before the title
puts the action ahead of its subject and moves the title depending on
whether a note exists; hugging the end of the title moves the button on
every click, the title being the object the tab is on; under the title
reads best but is a row of window spent on every tab. The end of the row
keeps the reading order and one position, and costs nothing.

It is drawn at the size of every other button in the bars now, rather
than a step smaller: the one thing on screen that says notes exist was
the smallest text in a row of them.
Where a note appears is where the button that starts one belongs: under
the title it will be about, reading in the order it happens — this
object, then write about it. At the far end of the row it kept one
position, but it was as far from the title as the window is wide, and it
read as the window's button rather than as this object's.

What that costs is a line of every tab nobody has written about, so it is
drawn small and with no padding of its own, which also lines its label up
with the title above it.

The tooltip on it is one sentence now. Markdown, class names, addresses
and shark:// links are what the box can do, and that paragraph is in the
way of deciding whether to open the box; it is the placeholder inside it
instead, which is on screen exactly while it is worth reading.

And the wording throughout says what was always true of the code: a note
is the object's, not the tab's. Two tabs on one object are one note and
show each other's writing as it is typed, which is now a test rather than
only a consequence of Place.noteKey and one PlaceNotes per place.
Three of the note keys are lists rather than objects, one is a group of
them, and the tab a window opens with is the whole heap dump — so "a note
per object" reads as if the object list and the leaks had none, which is
the opposite of what noteKey does. The code has always been per Place;
this is the wording catching up, with "location" as the word the docs use
since Place is ours rather than anybody else's.

Also says how to throw a note away, which the app has always been able to
do and nothing wrote down: open it, delete the text, save. An empty note
is no note, so the file goes and the mark comes off the tab.
@pyricau
pyricau merged commit 6adb188 into main Aug 10, 2026
16 checks passed
@pyricau
pyricau deleted the add-ability-to-add-save-notes branch August 10, 2026 18:05
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