Draw a peer message a block at a time, and parse it off the drawing thread

A message from another agent was the one markdown in the app still rendered
whole: one parse and one display list for the entire thing. Every settled
reply has been cut into blocks since the transcript was made lazy, and
`warm` has been making those parses ahead on a background thread -- but it
filtered for assistant replies alone, so the longest message a transcript
holds was also the only one parsed on the thread that draws.

Measured on the emulator against a 43KB peer message, opening it: 177ms in
`markdown parsed while composing`, against none afterwards and 156 blocks
already ready. What is left is the card being a single list item, so all 156
blocks are still measured, placed and recorded at once -- 118ms of placement
in that same frame.

The `when` in `warm` is now the rule rather than a filter: every row that
draws markdown belongs in it.

Blocks are spaced by the transcript's own BLOCK_SPACING rather than the
renderer's internal padding, which moves a heading about 6px (2.3dp) closer
to the paragraph above it. The message's total height is unchanged, and it
now matches every reply in the transcript.

While here: FrameStats was remembered per session screen and DebugStats is a
global emptied only by the copy button, so the two halves of a render report
covered different stretches of time -- and `drawAccounting` divides one by
the other. A report copied after visiting two sessions claimed 36.8 seconds
of placement inside a 13.5 second window, and clamped "everything else" to
0.00ms (0%), which reads as a screen whose entire cost is this app's code.
One FrameStats for the app, so both halves mean "since this was last copied".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-01 15:20:24 -04:00
1 parent 6d12388063
commit 80aaf286c2
4 files changed
+56 -33

No files matched your search

@@ -1049,7 +1049,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
// One poll for this machine's limits, read by the two things that show them: the bar under
// the header, and the colour of the button that opens the dialog.
val usage = rememberSessionUsage(settings, summary.setup)
val frames = rememberFrameStats()
recordFrames()
var usageOpen by remember { mutableStateOf(false) }
var settingsOpen by remember { mutableStateOf(false) }
@@ -1150,9 +1150,9 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
" ${expandedTools.size} tool calls and" +
" ${expandedGroups.size} groups open",
),
frames = frames.lines(context.refreshHz()),
frames = FrameStats.lines(context.refreshHz()),
accounting =
frames.drawPhase().let { (nanos, count) ->
FrameStats.drawPhase().let { (nanos, count) ->
drawAccounting(nanos, count)
},
crash = lastCrash(context),
@@ -1170,7 +1170,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
// Emptied by the copy, so pressing it twice measures two separate
// stretches
// of scrolling rather than one and then the same one again.
frames.reset()
FrameStats.reset()
DebugStats.reset()
Toast.makeText(context, "Copied render report", Toast.LENGTH_SHORT)
.show()