Thin the app's comments
The same pass the server had, on the Kotlin side: comments restating what the code says are gone, and the ones recording a measurement, a constraint or an incident are kept but cut to a few lines each. 6540 comment lines to 5674, and 920 lines off the app. Two doc comments had drifted onto the item above the one they describe -- `contextAfter`'s onto `sessionWorking` in Events.kt, and `UsageMonitor`'s equivalent on the server was fixed in the previous commit. Each is back on its own item, which is the only non-comment line this diff moves. The comments are reflowed to the column limit at their own indentation: several were written wide, and ktfmt re-wrapped them into lines holding a single orphan word. `/tmp` script, not kept -- ktfmt is idempotent over the result, which is the check. Left alone deliberately: this codebase's remaining comment density is high because the comments carry things the code cannot say -- what a null means, what a number was measured against, which bug a guard exists for. Of the 238 one-line doc comments in the app, five were pure restatement of the name and were removed; the rest each say something the signature does not. ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest pass; cargo test (127), clippy --all-targets and fmt still clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
79682f03a7
commit
edc39c7371
68 files changed
+2077
-2997
No files matched your search
@@ -69,13 +69,10 @@ import org.intellij.markdown.flavours.gfm.GFMTokenTypes
|
||||
*
|
||||
* [live] is the reply still arriving, and two things are different for it. Its parse is incremental
|
||||
* -- see [LiveParse] -- so a delta costs a parse of the block it landed in rather than of the whole
|
||||
* message. And its pieces get a layer each: when drawing is invalidated, only the piece that
|
||||
* changed is re-recorded instead of the whole reply, which is worth a great deal while every delta
|
||||
* invalidates the message and a finished one can be twenty-five screens tall. It is worth nothing
|
||||
* once the message stops changing -- measured on a Pixel 9 Pro XL, whole rows were re-recorded 65
|
||||
* times in fifty seconds of reading -- and it is not free: each layer is a layout node and a
|
||||
* display list held for the life of the row, and live node count is what the per-frame cost of the
|
||||
* transcript scales with.
|
||||
* message. And its pieces get a layer each, so only the piece that changed is re-recorded. That is
|
||||
* worth a great deal while every delta invalidates the message and worth nothing once it stops
|
||||
* changing -- and it is not free: each layer is a layout node and a display list held for the life
|
||||
* of the row, and live node count is what the transcript's per-frame cost scales with.
|
||||
*/
|
||||
@Composable
|
||||
fun MarkdownText(
|
||||
@@ -92,8 +89,8 @@ fun MarkdownText(
|
||||
var previousSegment: Segment? = null
|
||||
segments.forEachIndexed { at, segment ->
|
||||
val nextContinues = segments.getOrNull(at + 1)?.continues == true
|
||||
// Only the tail is still being written; a frozen segment is finished text that
|
||||
// happens to sit in a live reply, and it takes its colours now. See [MarkdownRoot].
|
||||
// Only the tail is still being written; a frozen segment is finished text that happens
|
||||
// to sit in a live reply, and it takes its colours now.
|
||||
MarkdownRoot(segment.parse, replies, streaming = live && at == segments.lastIndex) {
|
||||
segment.pieces.forEachIndexed { index, piece ->
|
||||
val gap =
|
||||
@@ -103,9 +100,9 @@ fun MarkdownText(
|
||||
if (segment.continues) 0.dp else BLOCK_SPACING
|
||||
else -> gapBefore(previous, piece)
|
||||
}
|
||||
// Keyed by where the piece starts in the message rather than by its position
|
||||
// in this column, so a delta landing in the last block leaves every other
|
||||
// piece's composition alone -- and a block keeps its key when it freezes.
|
||||
// Keyed by where the piece starts in the message rather than by its position in
|
||||
// this column, so a delta landing in the last block leaves every other piece's
|
||||
// composition alone -- and a block keeps its key when it freezes.
|
||||
key(segment.start, piece) {
|
||||
MarkdownPiece(
|
||||
segment.parse,
|
||||
@@ -137,8 +134,7 @@ fun MarkdownText(
|
||||
* A stretch of a message with a parse of its own: the whole of a settled message, or one block, the
|
||||
* finished items of one list, or the unfinished tail of a live one. [start] is where [text] begins
|
||||
* in the message. [continues] says the first piece is an item of the list the segment before it
|
||||
* ended with, so the two draw as one list: no block gap between them, and neither the item above
|
||||
* the seam nor the one below it takes the padding of a list's edge.
|
||||
* ended with, so the two draw as one list.
|
||||
*/
|
||||
private class Segment(
|
||||
val text: String,
|
||||
@@ -154,14 +150,11 @@ private class Segment(
|
||||
*
|
||||
* The first parse has to be inline. The renderer's own asynchronous path draws an empty loading
|
||||
* slot until its result arrives, so a row is measured at nothing before it is measured at its real
|
||||
* height, and the transcript above it collapses and springs back. Seen with five replies on screen
|
||||
* at once, every one of them blank, the whole conversation shrunk to fit a single screen; a moment
|
||||
* later it was all there again. That is the "skipping up and down" this list must never do.
|
||||
* height, and the transcript above it collapses and springs back -- seen with five replies on
|
||||
* screen at once, the whole conversation shrunk to fit a single screen.
|
||||
*
|
||||
* Every parse after the first is off the composing thread, and the row keeps drawing the parse it
|
||||
* already has until the new one lands, so there is never a frame without a height. What is on
|
||||
* screen is always a real prefix of the reply rather than a guess at it; it is simply one parse
|
||||
* behind.
|
||||
* already has until the new one lands, so there is never a frame without a height.
|
||||
*/
|
||||
@Composable
|
||||
private fun liveSegments(text: String): List<Segment> {
|
||||
@@ -187,24 +180,18 @@ private fun liveSegments(text: String): List<Segment> {
|
||||
* Reparsing the whole message per delta was fine for a short reply and not for a long one: a
|
||||
* twenty-five-screen reply parses in tens of milliseconds, hundreds of times, and although that ran
|
||||
* off the composing thread it was every core busy while the frame's own thread waited for one.
|
||||
* Markdown's blocks make the cut safe: a top-level block that another block has started *after* is
|
||||
* finished -- nothing appended later can reach back into it, since a paragraph ends at the blank
|
||||
* line or the block that interrupts it, a fence at its closing fence, a list at the first line that
|
||||
* is neither an item nor indented under one. So every block but the last is [frozen] with the parse
|
||||
* that finished it, and only the tail -- the last block and whatever has arrived since -- is parsed
|
||||
* again.
|
||||
*
|
||||
* A list is cut once more, at its last item, by the same reasoning one level down: an item is
|
||||
* finished once the next item has begun, since a line can only continue the item it is indented
|
||||
* under or start a new one. Without this a reply that is one long list -- forty sources -- parsed
|
||||
* the whole list per delta, and a list streams as forty paragraphs would. The item the cut lands on
|
||||
* has to have begun in earnest: a bare `-` is an empty item now and the first character of a
|
||||
* paragraph line once `-x` arrives, and cutting on it would draw that line as a new item.
|
||||
* Markdown's blocks make the cut safe: a top-level block that another block has started *after* is
|
||||
* finished -- nothing appended later can reach back into it. So every block but the last is
|
||||
* [frozen] with the parse that finished it, and only the tail is parsed again.
|
||||
*
|
||||
* A list is cut once more, at its last item, by the same reasoning one level down. Without this a
|
||||
* reply that is one long list -- forty sources -- parsed the whole list per delta. The item the cut
|
||||
* lands on has to have begun in earnest: a bare `-` is an empty item now and the first character of
|
||||
* a paragraph line once `-x` arrives.
|
||||
*
|
||||
* What the cut gives up is one thing: a reference definition arriving later than a link that uses
|
||||
* it, since the frozen block's parse never sees it. The link draws as its brackets until the reply
|
||||
* settles and is parsed whole by [warm], which is the same moment every other transient of
|
||||
* streaming is put right.
|
||||
* it. The link draws as its brackets until the reply settles and is parsed whole by [warm].
|
||||
*/
|
||||
private class LiveParse(
|
||||
val text: String,
|
||||
@@ -217,8 +204,8 @@ private class LiveParse(
|
||||
get() = frozen + tail
|
||||
|
||||
fun advanceTo(next: String): LiveParse {
|
||||
// Anything but an append to what was frozen -- a message replaced, a stream reset --
|
||||
// starts over.
|
||||
// Anything but an append to what was frozen -- a message replaced, a stream reset -- starts
|
||||
// over.
|
||||
if (!next.regionMatches(0, text, 0, consumed)) return whole(next)
|
||||
val tailText = next.substring(consumed)
|
||||
val parse = parseMarkdown(tailText)
|
||||
@@ -264,8 +251,7 @@ private class LiveParse(
|
||||
|
||||
/**
|
||||
* The piece of the tail still being written: the last item of a list of several, or the first
|
||||
* piece of the last block when there is more than one block. Null when nothing before it is
|
||||
* finished, so the tail stays whole.
|
||||
* piece of the last block when there is more than one. Null when nothing before it is finished.
|
||||
*/
|
||||
private fun openPiece(parse: State.Success, all: List<Piece>): Piece? {
|
||||
val last = all.lastOrNull() ?: return null
|
||||
@@ -315,28 +301,24 @@ fun MarkdownPiece(
|
||||
* The renderer's own environment -- its colours, type scale, dimensions, component table and
|
||||
* reference links -- around whatever draws pieces of [parse].
|
||||
*
|
||||
* The parsing is the library's. Markdown is somebody else's specification, and a hand-written
|
||||
* The parsing is the library's: markdown is somebody else's specification, and a hand-written
|
||||
* parser would get the edge cases wrong one case at a time. So is the environment: the element
|
||||
* composables its dispatch reaches read these locals, and providing them once here is what lets a
|
||||
* piece be drawn anywhere -- in a message's column, or as one item of the transcript list.
|
||||
* Everything below this is the mapping onto the app's palette and type scale.
|
||||
*
|
||||
* The locals are provided directly rather than through the renderer's `Markdown()` composable,
|
||||
* which was the last of its composables on the hot path and was here only to provide them. What
|
||||
* that buys is that nothing between a piece and the screen is the library's but the leaf
|
||||
* composables named in the component table, so a different parser could stand behind [State]
|
||||
* without the renderer's entry point being involved.
|
||||
* which was the last of its composables on the hot path and was here only to provide them. So
|
||||
* nothing between a piece and the screen is the library's but the leaf composables named in the
|
||||
* component table.
|
||||
*
|
||||
* Colours come from the theme rather than from the renderer's defaults, so code, links and rules
|
||||
* are the same Catppuccin values the rest of the app uses. Nothing here picks a colour of its own.
|
||||
* Colours come from the theme rather than the renderer's defaults. Nothing here picks one of its
|
||||
* own.
|
||||
*
|
||||
* [streaming] says this parse is the part of a reply still being written, which only the fences
|
||||
* care about: lexing is proportional to how much code there is, and a fence still arriving is
|
||||
* re-lexed at every delta on the composing thread. Measured streaming a two-hundred-line Kotlin
|
||||
* fence: **13.7 seconds** of lexing across the turn, 211 of them, the worst 177ms -- for colours on
|
||||
* text that was being replaced as fast as they were computed. So a fence still being written is
|
||||
* drawn plain and takes its colours when the block freezes, which is the same bargain [LiveParse]
|
||||
* already makes for a reference link defined at the foot of a message.
|
||||
* care about: lexing is proportional to how much code there is. Measured streaming a two-hundred-
|
||||
* line Kotlin fence: **13.7 seconds** of lexing across the turn, 211 of them, the worst 177ms --
|
||||
* for colours on text being replaced as fast as they were computed. So a fence still being written
|
||||
* is drawn plain and takes its colours when the block freezes.
|
||||
*/
|
||||
@Composable
|
||||
private fun MarkdownRoot(
|
||||
@@ -354,45 +336,40 @@ private fun MarkdownRoot(
|
||||
CompositionLocalProvider(
|
||||
LocalReferenceLinkHandler provides parse.referenceLinkHandler,
|
||||
LocalMarkdownPadding provides markdownPadding(),
|
||||
// Read by the renderer's own text composable, which no paragraph reaches any more, and
|
||||
// by its checkbox. Provided so a path that does reach them draws no image rather than
|
||||
// failing to compose.
|
||||
// Read by the renderer's own text composable, which no paragraph reaches any more, and by
|
||||
// its checkbox. Provided so a path that does reach them draws no image rather than failing
|
||||
// to compose.
|
||||
LocalImageTransformer provides remember { NoOpImageTransformerImpl() },
|
||||
LocalMarkdownAnimations provides markdownAnimations(),
|
||||
LocalMarkdownColors provides
|
||||
markdownColor(
|
||||
text = MaterialTheme.colorScheme.onSurface,
|
||||
dividerColor = MaterialTheme.colorScheme.outlineVariant,
|
||||
// The dark surface every verbatim thing in this app sits on -- see [rawSurface],
|
||||
// and the tool call above this reply, which now matches. `surfaceVariant` was
|
||||
// exactly a card's own fill, so a fenced block inside a tool call had no
|
||||
// background at all and one in a reply read as a step *up* out of the page.
|
||||
// The dark surface every verbatim thing in this app sits on -- and the tool call
|
||||
// above this reply, which now matches. `surfaceVariant` was exactly a card's own
|
||||
// fill, so a fenced block inside a tool call had no background at all.
|
||||
codeBackground = rawSurface,
|
||||
// The same colour. Not drawn by the renderer as a span background but by
|
||||
// [LinkedText] behind the text, so a selection lands on top of it as it does on a
|
||||
// fenced block -- see `appendCodeChip`.
|
||||
// [LinkedText] behind the text, so a selection lands on top of it -- see
|
||||
// `appendCodeChip`.
|
||||
inlineCodeBackground = rawSurface,
|
||||
// The same tint a code block gets, rather than the renderer's 2%-alpha default:
|
||||
// two adjacent tints that differ by a fiftieth read as one flat block on a phone,
|
||||
// so the table would have had a border-less grid and nothing saying where it began.
|
||||
// The same tint a code block gets, rather than the renderer's 2%-alpha default: two
|
||||
// adjacent tints that differ by a fiftieth read as one flat block on a phone.
|
||||
tableBackground = MaterialTheme.colorScheme.surfaceVariant,
|
||||
),
|
||||
LocalMarkdownTypography provides
|
||||
markdownTypography(
|
||||
// A ladder that starts near the body text and descends, because these are headings
|
||||
// inside a chat message rather than the top of a document. The renderer's defaults
|
||||
// are the Material *display* styles -- `#` came out at 57sp and `##` at 45sp, which
|
||||
// is bigger than this app's own screen titles and reads as the reply shouting.
|
||||
//
|
||||
// Every step is a different size, so two levels of nesting never draw the same:
|
||||
// one clear step per level is the whole job of a heading.
|
||||
// are the Material *display* styles -- `#` came out at 57sp, bigger than this app's
|
||||
// own screen titles. Every step is a different size, so two levels of nesting never
|
||||
// draw the same.
|
||||
h1 = MaterialTheme.typography.headlineSmall,
|
||||
h2 = MaterialTheme.typography.titleLarge,
|
||||
h3 = MaterialTheme.typography.titleMedium,
|
||||
h4 = MaterialTheme.typography.titleSmall,
|
||||
h5 = MaterialTheme.typography.labelMedium,
|
||||
h6 = MaterialTheme.typography.labelSmall,
|
||||
// Body text at the size everything else in the transcript uses.
|
||||
text = body,
|
||||
paragraph = body,
|
||||
ordered = body,
|
||||
@@ -400,16 +377,10 @@ private fun MarkdownRoot(
|
||||
list = body,
|
||||
table = body,
|
||||
// Code in a monospace face, in the ordinary text colour. The face and the tinted
|
||||
// background are what say "this is code"; colour is not, and it used to be green
|
||||
// -- the palette's colour for a *literal*. A block of code is not a literal, it
|
||||
// is text that happens to be code, and painting all of it green said the whole
|
||||
// block was one. Where a literal really does appear inside code, the thing that
|
||||
// should colour it is a syntax highlighter looking at the code, which is exactly
|
||||
// what a tool call's input already gets from `catppuccinSyntax`.
|
||||
//
|
||||
// The colour rides on the style here rather than in `markdownColor`, which
|
||||
// stopped carrying `codeText`/`inlineCodeText`/`linkText` when the renderer moved
|
||||
// them onto the typography.
|
||||
// background are what say "this is code"; colour is not, and it used to be green --
|
||||
// the palette's colour for a *literal*. A block of code is not a literal, and
|
||||
// painting all of it green said the whole block was one. Where a literal really
|
||||
// does appear inside code, what should colour it is a syntax highlighter.
|
||||
code =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
@@ -436,31 +407,26 @@ private fun MarkdownRoot(
|
||||
LocalMarkdownDimens provides
|
||||
markdownDimens(
|
||||
// Half the renderer's 16dp. Padding is charged on both sides of every cell, so at
|
||||
// the default a fifth of the narrowest column went on space rather than on words
|
||||
// -- and the narrowest column is where the wrapping below has the least room.
|
||||
// the default a fifth of the narrowest column went on space rather than on words.
|
||||
tableCellPadding = 8.dp,
|
||||
// What a column narrows to before the table starts scrolling sideways instead. It
|
||||
// is the floor, not the width: a table with room to spare spreads across it.
|
||||
//
|
||||
// Down from the renderer's 160dp, and the number is a measurement rather than a
|
||||
// taste. A phone is about 410-450dp wide and a card takes some of that, so 160dp
|
||||
// makes even a three-column table -- the commonest shape there is -- scroll, while
|
||||
// 136dp fits three across the phone this app is read on. Four and up still scroll,
|
||||
// which is the right answer for genuinely too many columns: squeezing six columns
|
||||
// into a phone would give every cell one word per line.
|
||||
//
|
||||
// Narrower would fit more, and stop being readable. This is the widest minimum
|
||||
// that keeps three columns on screen, which is the trade the number is making.
|
||||
// makes even a three-column table scroll, while 136dp fits three across the phone
|
||||
// this app is read on. Four and up still scroll, which is the right answer for
|
||||
// genuinely too many columns. This is the widest minimum that keeps three on
|
||||
// screen.
|
||||
tableCellWidth = 136.dp,
|
||||
),
|
||||
LocalMarkdownComponents provides
|
||||
markdownComponents(
|
||||
// The m3 renderer's own default, restored: supplying `components` at all replaces
|
||||
// the whole set, and this is the only member of it the Material layer overrides.
|
||||
// the whole set, and this is the only member the Material layer overrides.
|
||||
checkbox = { MarkdownCheckBox(it.content, it.node, it.typography.text) },
|
||||
// Everything that draws a run of text, so a link is a span rather than a node --
|
||||
// see [LinkedText]. Setext headings take the same styles as `#` and `##`, which
|
||||
// is the renderer's own pairing.
|
||||
// see [LinkedText]. Setext headings take the same styles as `#` and `##`.
|
||||
text = { LinkedText(it, it.typography.text) },
|
||||
paragraph = { LinkedText(it, it.typography.paragraph) },
|
||||
heading1 = { LinkedHeading(it, it.typography.h1) },
|
||||
@@ -471,8 +437,8 @@ private fun MarkdownRoot(
|
||||
heading6 = { LinkedHeading(it, it.typography.h6) },
|
||||
setextHeading1 = { LinkedHeading(it, it.typography.h1) },
|
||||
setextHeading2 = { LinkedHeading(it, it.typography.h2) },
|
||||
// Lists are ours wherever the renderer's dispatch meets one -- inside a quote --
|
||||
// so they draw like the top-level ones the transcript cuts into items.
|
||||
// Lists are ours wherever the renderer's dispatch meets one -- inside a quote -- so
|
||||
// they draw like the top-level ones the transcript cuts into items.
|
||||
orderedList = { MarkdownList(it.content, it.node, it.listDepth) },
|
||||
unorderedList = { MarkdownList(it.content, it.node, it.listDepth) },
|
||||
table = { LinkedTable(it.content, it.node, it.typography.table) },
|
||||
@@ -491,14 +457,12 @@ private fun MarkdownRoot(
|
||||
/**
|
||||
* A table: its rows, on the renderer's tinted, rounded background, as wide as its columns need.
|
||||
*
|
||||
* Each column has a floor ([markdownDimens]'s `tableCellWidth`), so the table is at least
|
||||
* columns-times-floor wide; narrower than the room it has, it spreads to fill it, and wider, it
|
||||
* scrolls sideways rather than squeezing. The renderer decided that with a `BoxWithConstraints`,
|
||||
* which is a subcomposition; here it is one layout modifier, and the trick is where it sits.
|
||||
* `fillMaxWidth` fixes the minimum width to the room available, the horizontal scroll passes that
|
||||
* minimum through to its content while lifting the maximum to unbounded, and the modifier after it
|
||||
* reads the minimum back as the room and sizes the rows to the larger of that and the floor. The
|
||||
* scroll then has exactly the overflow to scroll, which is none when the table fits.
|
||||
* Each column has a floor, so the table is at least columns-times-floor wide; narrower than the
|
||||
* room it has, it spreads to fill it, and wider, it scrolls sideways rather than squeezing. The
|
||||
* renderer decided that with a `BoxWithConstraints`, which is a subcomposition; here it is one
|
||||
* layout modifier. `fillMaxWidth` fixes the minimum width to the room available, the horizontal
|
||||
* scroll passes that minimum through while lifting the maximum to unbounded, and the modifier after
|
||||
* it reads the minimum back and sizes the rows to the larger of that and the floor.
|
||||
*/
|
||||
@Composable
|
||||
private fun LinkedTable(content: String, node: ASTNode, style: TextStyle) {
|
||||
@@ -539,19 +503,15 @@ private fun LinkedTable(content: String, node: ASTNode, style: TextStyle) {
|
||||
* One row of a table -- the header when [rowIndex] is zero -- with every cell a [LinkedText].
|
||||
*
|
||||
* The renderer's own rows draw each cell at `maxLines = 1` with an ellipsis, which on a phone means
|
||||
* most of a table is simply not readable: anything past about twenty characters ends in "..." with
|
||||
* no way to see the rest, and an elided cell looks like a short one, so a table of measurements
|
||||
* reads as a table of plausible shorter measurements. And they draw a link in a cell as its own
|
||||
* layout node, the cost [LinkedText] exists to avoid.
|
||||
* most of a table is simply not readable: an elided cell looks like a short one, so a table of
|
||||
* measurements reads as a table of plausible shorter measurements. And they draw a link in a cell
|
||||
* as its own layout node, the cost [LinkedText] exists to avoid.
|
||||
*
|
||||
* So: as many lines as the cell needs, cells aligned to the top of the row, because a two-line cell
|
||||
* beside a one-line one centred the short one against the middle of the tall one and lost the line
|
||||
* the reader was reading across. What the wrapping does *not* do is make a wide table fit;
|
||||
* [LinkedTable] scrolls it instead, which is the right answer for too many columns -- wrapping a
|
||||
* six-column table into the width of a phone would give every cell one word per line.
|
||||
* beside a one-line one centred the short one against the middle of the tall one. What the wrapping
|
||||
* does *not* do is make a wide table fit; [LinkedTable] scrolls it instead.
|
||||
*
|
||||
* The semantics are the renderer's: each cell is an item of the table's collection, and a header
|
||||
* cell is a heading.
|
||||
* The semantics are the renderer's: each cell is an item of the table's collection.
|
||||
*/
|
||||
@Composable
|
||||
private fun LinkedTableRow(content: String, row: ASTNode, style: TextStyle, rowIndex: Int) {
|
||||
@@ -587,19 +547,14 @@ private fun LinkedTableRow(content: String, row: ASTNode, style: TextStyle, rowI
|
||||
* Parsing is the expensive half of drawing a reply, and it is expensive in proportion to how much
|
||||
* was written. Measured against a real Claude Code transcript on the emulator, one message took
|
||||
* **51ms** and several took 10-25ms, against 4.6ms for the short synthetic replies this was first
|
||||
* tuned on -- so a page of history landing composed several rows that each stalled the frame they
|
||||
* appeared in. That is the lag when a block loads.
|
||||
* tuned on -- so a page of history landing composed several rows that each stalled the frame.
|
||||
*
|
||||
* Nothing here changes what a row does when it has no answer waiting: it parses inline, on the
|
||||
* composing thread, because a row measured at nothing before it is measured at its real height
|
||||
* collapses the transcript above it. The point is only that by the time the reader scrolls to a
|
||||
* row, the answer is usually already made -- [warm] runs on a background thread as each page of
|
||||
* history arrives, which is seconds before anybody reaches the rows it brought.
|
||||
* Nothing here changes what a row does when it has no answer waiting: it parses inline, because a
|
||||
* row measured at nothing before its real height collapses the transcript above it. The point is
|
||||
* only that by the time the reader scrolls to a row, the answer is usually already made.
|
||||
*
|
||||
* A miss is not stored, and that is what bounds this: the map holds one entry per message a page
|
||||
* warmed and nothing else, so a reply still streaming cannot fill it with hundreds of copies of
|
||||
* itself on the way to being finished. It is dropped with the screen, and emptied by the stream
|
||||
* reset that drops the rows it describes.
|
||||
* warmed, so a reply still streaming cannot fill it with hundreds of copies of itself.
|
||||
*/
|
||||
@Stable
|
||||
class ParsedReplies {
|
||||
@@ -607,14 +562,13 @@ class ParsedReplies {
|
||||
|
||||
/**
|
||||
* How each message divides into pieces, cached beside its parse: [transcriptUnits] asks per
|
||||
* fold, and walking the tree again each time is proportional to the message where a lookup is
|
||||
* proportional to nothing.
|
||||
* fold, and walking the tree again each time is proportional to the message.
|
||||
*/
|
||||
private val pieces = ConcurrentHashMap<String, List<Piece>>()
|
||||
|
||||
/**
|
||||
* How each message divides into prose and memory notes, cached for the same reason as
|
||||
* [piecesOf]: the regex scan behind [messageParts] is proportional to the message.
|
||||
* How each message divides into prose and memory notes, cached for the same reason: the regex
|
||||
* scan behind [messageParts] is proportional to the message.
|
||||
*/
|
||||
private val parts = ConcurrentHashMap<String, List<MessagePart>>()
|
||||
|
||||
@@ -627,7 +581,7 @@ class ParsedReplies {
|
||||
* much code was written -- a two-hundred-line Kotlin fence measured 174ms on the emulator --
|
||||
* and a lazy list drops the composition of a block that scrolls away, so a `remember` inside
|
||||
* the fence paid that again every time the reader came back to it. Six times in one scroll,
|
||||
* measured. [warm] fills this off the drawing thread before the row is reached.
|
||||
* measured.
|
||||
*/
|
||||
private val highlights = ConcurrentHashMap<String, AnnotatedString>()
|
||||
|
||||
@@ -649,11 +603,10 @@ class ParsedReplies {
|
||||
* Whether [warm] has made everything drawing [text] as pieces will look up.
|
||||
*
|
||||
* What the flatten asks before drawing a reply that way. Cutting costs a parse of the whole
|
||||
* message and the flatten runs on the composing thread -- so a reply not marked yet stays
|
||||
* whole, drawing the parse it already has, until the screen has warmed it and re-flattens. An
|
||||
* explicit mark rather than a peek into the parse cache, because a message with memory notes is
|
||||
* warmed as its *parts*: nothing ever parses its full text, and inferring readiness from the
|
||||
* cache left exactly that message unsplittable forever, re-warmed on every fold.
|
||||
* message and the flatten runs on the composing thread, so a reply not marked yet stays whole
|
||||
* until the screen has warmed it. An explicit mark rather than a peek into the parse cache,
|
||||
* because a message with memory notes is warmed as its *parts*: nothing ever parses its full
|
||||
* text, and inferring readiness from the cache left exactly that message unsplittable forever.
|
||||
*/
|
||||
fun splitReady(text: String): Boolean = text in ready
|
||||
|
||||
@@ -668,9 +621,8 @@ class ParsedReplies {
|
||||
}
|
||||
|
||||
/**
|
||||
* [code] coloured for [language] -- the answer made ahead, or one made now.
|
||||
*
|
||||
* The key carries the language, because the same code lexes differently under two of them.
|
||||
* [code] coloured for [language] -- the answer made ahead, or one made now. The key carries the
|
||||
* language, because the same code lexes differently under two of them.
|
||||
*/
|
||||
fun highlighted(code: String, language: Language?): AnnotatedString =
|
||||
if (language == null) AnnotatedString(code)
|
||||
@@ -686,9 +638,9 @@ class ParsedReplies {
|
||||
*
|
||||
* Suspending, and yielding between messages, because "off the composing thread" is not the same
|
||||
* as "free". A page of history arrives as hundreds of parses at once -- 1.5 seconds of them in
|
||||
* a twelve second scroll, measured on a Pixel 9 Pro XL -- and on the default dispatcher that is
|
||||
* every core busy, with the frame's own thread waiting for one. That showed up as 21ms of
|
||||
* `waited` at the 90th percentile: the frame could not start, rather than taking too long.
|
||||
* a twelve second scroll on a Pixel 9 Pro XL -- and on the default dispatcher that is every
|
||||
* core busy, with the frame's own thread waiting for one: 21ms of `waited` at the 90th
|
||||
* percentile.
|
||||
*/
|
||||
suspend fun warm(texts: List<String>) {
|
||||
texts.forEach { text ->
|
||||
@@ -697,9 +649,8 @@ class ParsedReplies {
|
||||
DebugStats.timed("markdown warmed") { parseMarkdown(it) }
|
||||
}
|
||||
// The fences too, and here rather than in a pass of its own: they are found in the
|
||||
// parse this just made, and lexing one is the same kind of cost as parsing the
|
||||
// message it is in -- proportional to what was written, and charged to the frame
|
||||
// that first draws it if nobody paid it earlier.
|
||||
// parse this just made, and lexing one is the same kind of cost as parsing the message
|
||||
// it is in.
|
||||
fences(parse).forEach { (code, language) -> highlighted(code, language) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user