Take every whole-message parse off the composing thread, and count the rest
Bryan's report after the last change read no worse in the numbers -- medians unchanged, worst stall halved -- but scrolling felt bumpier while messages loaded, and instrumenting the split paths found the feeling's likely source: work the counters never saw. The live reply's block split (BlockedMarkdown) recomputed on the composing thread at every delta -- a whole-message parse of the growing text, 815 of them and 2.9 seconds inside one streamed reply, a few milliseconds per delta on the thread that draws, plus a cache entry per partial text that nothing reads again. It now works the way parsedMarkdown already did one file over: first split inline so the row has its height, every later one off-thread, drawing one split behind, cached nowhere. Emulator, same streamed fixture: anim p90 7.3 -> 3.4ms, p99 9.3 -> 4.7ms. The settle moment had the same shape: nothing warms live deltas, so the just-finished reply's split parse ran inside the flatten, uncounted, in a frame. The flatten now splits a reply only when ParsedReplies.splitReady says warm() has made its parses; the session screen warms the one cold row off-thread and re-flattens (warmedTick), so the whole-to-blocks swap always composes against ready parses. Readiness is an explicit mark set by warm() rather than a peek into the blocks cache, because a message with memory notes is warmed as its parts -- inferred readiness left it unsplittable forever and re-warmed on every fold. Also: user slices shrink to ~1000 chars (about one viewport, so a slice composing mid-fling costs a few milliseconds, not sixty), and the split and flatten paths are all timed -- "units flattened", "markdown split into blocks", "message cut into parts", "user message cut into slices", "blocks split while streaming" -- so the next "it feels bumpier" report names its cause instead of hiding it in anim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
333d2de92b
commit
356dee65f1
5 files changed
+113
-8
No files matched your search
@@ -291,12 +291,40 @@ class ParsedReplies {
|
||||
|
||||
private val chunks = ConcurrentHashMap<String, List<String>>()
|
||||
|
||||
fun blocksOf(text: String): List<String> = blocks.computeIfAbsent(text) { markdownBlocks(it) }
|
||||
private val ready = ConcurrentHashMap.newKeySet<String>()
|
||||
|
||||
fun blocksOf(text: String): List<String> =
|
||||
blocks.computeIfAbsent(text) {
|
||||
DebugStats.timed("markdown split into blocks") { markdownBlocks(it) }
|
||||
}
|
||||
|
||||
/** How a long user message divides into slices; cached for the same reason as [blocksOf]. */
|
||||
fun chunksOf(text: String): List<String> = chunks.computeIfAbsent(text) { userChunks(it) }
|
||||
fun chunksOf(text: String): List<String> =
|
||||
chunks.computeIfAbsent(text) {
|
||||
DebugStats.timed("user message cut into slices") { userChunks(it) }
|
||||
}
|
||||
|
||||
fun partsOf(text: String): List<MessagePart> = parts.computeIfAbsent(text) { messageParts(it) }
|
||||
/**
|
||||
* Whether [warm] has made everything drawing [text] as blocks will look up.
|
||||
*
|
||||
* What the flatten asks before drawing a reply that way. Splitting 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 [blocksOf]'s cache, because a message with memory notes
|
||||
* is warmed as its *parts*: nothing ever splits its full text, and inferring readiness from the
|
||||
* cache left exactly that message unsplittable forever, re-warmed on every fold.
|
||||
*/
|
||||
fun splitReady(text: String): Boolean = text in ready
|
||||
|
||||
/** The other half of [splitReady]; [warm] calls it once a message's parses exist. */
|
||||
fun markSplitReady(text: String) {
|
||||
ready.add(text)
|
||||
}
|
||||
|
||||
fun partsOf(text: String): List<MessagePart> =
|
||||
parts.computeIfAbsent(text) {
|
||||
DebugStats.timed("message cut into parts") { messageParts(it) }
|
||||
}
|
||||
|
||||
/** The parse of [text] -- the one made ahead, or one made now. */
|
||||
fun of(text: String): State =
|
||||
@@ -326,5 +354,6 @@ class ParsedReplies {
|
||||
blocks.clear()
|
||||
parts.clear()
|
||||
chunks.clear()
|
||||
ready.clear()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user