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
@@ -343,7 +343,23 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
||||
// What is actually drawn: the transcript with runs of adjacent tool
|
||||
// calls folded into one row each, flattened into the list's units.
|
||||
val rows = remember(items) { groupToolRuns(items) }
|
||||
val units = remember(rows, expandedNotes) { transcriptUnits(rows, replies, expandedNotes) }
|
||||
// Bumped when a cold reply's parses become ready, so the flatten runs again and can split
|
||||
// it; see [unwarmedReplies].
|
||||
var warmedTick by remember { mutableIntStateOf(0) }
|
||||
val units =
|
||||
remember(rows, expandedNotes, warmedTick) { transcriptUnits(rows, replies, expandedNotes) }
|
||||
// The reply that just finished streaming is the one row whose parses nobody has made: pages
|
||||
// warm before their fold lands, but nothing warms live deltas. Off the composing thread,
|
||||
// then the tick re-flattens -- so settling never costs a whole-message parse in a frame.
|
||||
// Re-launched per fold and almost always finds nothing; during streaming the last row is
|
||||
// unsettled and not wanted.
|
||||
LaunchedEffect(rows) {
|
||||
val cold = unwarmedReplies(rows, replies)
|
||||
if (cold.isNotEmpty()) {
|
||||
warm(replies, cold)
|
||||
warmedTick++
|
||||
}
|
||||
}
|
||||
// The same list, readable from effects launched before this composition: an effect's closure
|
||||
// keeps the values of the composition that launched it, and both the anchor saver and the
|
||||
// restore need the units as they are *now*.
|
||||
|
||||
Reference in new issue
Block a user