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

@@ -466,32 +466,46 @@ private fun updateTool(
private val parsingThreads = Dispatchers.Default.limitedParallelism(2)
/**
* Parses the replies among [rows], off whatever thread is drawing.
* Parses the markdown among [rows], off whatever thread is drawing.
*
* Called where a page of transcript is folded rather than where a row is composed, which is the
* whole point: the work happens seconds before the reader reaches the rows it was done for. See
* [ParsedReplies].
*
* What is warmed mirrors what the rows draw, unit by unit -- prose split into its blocks, a memory
* note whole -- because a string warmed under a key no row ever looks up is a miss that nothing
* reports; see [transcriptUnits], which is the flatten this has to agree with. It reads the same
* [ParsedReplies.partsOf] and [ParsedReplies.blocksOf] caches the flatten does, so a message is
* scanned once however many pages hand it back through here, while the whole loaded transcript
* crosses this on every page.
* note whole, a peer message split the same way prose is -- because a string warmed under a key no
* row ever looks up is a miss that nothing reports; see [transcriptUnits], which is the flatten
* this has to agree with. It reads the same [ParsedReplies.partsOf] and [ParsedReplies.blocksOf]
* caches the flatten does, so a message is scanned once however many pages hand it back through
* here, while the whole loaded transcript crosses this on every page.
*
* Every kind of row that draws markdown belongs in the `when` below. That is the rule the peer
* message was missing: this used to filter for assistant replies alone, so the one row type nobody
* had thought about paid its whole parse in the frame it appeared in, with no counter saying which
* row it was.
*/
suspend fun warm(replies: ParsedReplies, rows: List<TranscriptItem>) {
withContext(parsingThreads) {
val texts =
rows
.filterIsInstance<TranscriptItem.AssistantMsg>()
.flatMap { replies.partsOf(it.text) }
.flatMap { part ->
when (part) {
is MessagePart.Prose -> replies.blocksOf(part.text)
// Drawn as one MarkdownText, so its whole text is the key looked up.
is MessagePart.Remembered -> listOf(part.text)
val texts = rows.flatMap { row ->
when (row) {
is TranscriptItem.AssistantMsg ->
replies.partsOf(row.text).flatMap { part ->
when (part) {
is MessagePart.Prose -> replies.blocksOf(part.text)
// Drawn as one MarkdownText, so its whole text is the key
// looked up.
is MessagePart.Remembered -> listOf(part.text)
}
}
}
// A message from another agent is markdown too, and it is the longest thing
// in a transcript often enough that leaving it out was the whole of why one
// cost a fifth of a second to open: it was the only markdown in the app
// parsed on the thread that draws. Its blocks, not its text, because
// [PeerMessageRow] draws it a block at a time.
is TranscriptItem.PeerNote -> replies.blocksOf(row.text)
else -> emptyList()
}
}
if (texts.isNotEmpty()) replies.warm(texts)
}
}