Warm a fence's highlighting like a parse, and page the restore by rows

Two things the measurements for the previous commit turned up.

Highlighting a fence cost 174ms for a two-hundred-line Kotlin block, and
the lazy list charged it again every time that block scrolled back into
composition -- six times in one bench run, with the scroll's draw phase at
1.29ms per frame. So it is warmed and cached where parses already are:
`highlight` is a plain function taking no colour from the theme, `warm`
fills `ParsedReplies.highlighted` from `fences(parse)` off the drawing
thread, and `fenceContent` extracts the code here rather than through the
library's composable, so the string warmed is the string drawn.

And the anchor restore asked for a span counted in events, which goes
negative when the anchor's row is the oldest half-row and was coerced to
one -- a request per delta, six hundred round trips walking one reply back
a word at a time with the spinner up. It asks for a page of rows now.

Clean pairs, fresh sessions each side, same gestures. Fence scroll
(transcript-bench.sh): draw phase 0.74ms per frame before highlighting
existed, 0.77ms after, no lexing in the window either side. Streaming
forty linked items (stream-bench.sh): 2412ms of reparsing before, 674ms
after; mean 5.0ms to 1.4ms, worst 8.9ms to 7.9ms. Bullet glyphs, fence
colours, the image links and the reference link checked on the emulator;
lint clean on AGP 9.4.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-03 18:50:29 -04:00
1 parent 6892dc7caf
commit ab6a797941
6 files changed
+344 -148

No files matched your search

+41 -5
View File
@@ -38,17 +38,53 @@ if [ -z "$keep" ]; then
sleep 3
fi
# Pinned to the newest end before anything is sent. The transcript never
# moves under a reader who is further back (see TranscriptList), so a reply
# streaming into a session parked at an older row arrives entirely
# off-screen: nothing recomposes, nothing draws, and the report comes back
# with two recompositions in it and no streaming counters at all. That
# reads exactly like a build where the work vanished. The control is the
# app's own "Jump to latest", which is only there while the newest message
# is off screen -- so when it is absent the list is already where it needs
# to be.
jump=$(ui-trace record -d 1200 -o /tmp/bench-jump.txt >/dev/null 2>&1
ui-trace show /tmp/bench-jump.txt -m 'Jump to latest' --field box |
grep -o '[0-9]*,[0-9]*\.\.[0-9]*,[0-9]*' | tail -1)
if [ -n "$jump" ]; then
x=$(echo "$jump" | awk -F'[,.]' '{print int(($1 + $4) / 2)}')
y=$(echo "$jump" | awk -F'[,.]' '{print int(($2 + $5) / 2)}')
ui-trace record -d 1500 --do "tap $x $y" -o /tmp/bench-tolatest.txt >/dev/null 2>&1
fi
# The first tap resets the report's window; see transcript-bench.sh.
ui-trace record -d 2000 --do 'tap 723 205' -o /tmp/bench-reset.txt >/dev/null 2>&1
adb logcat -c
./ui-sandbox.sh send "$sid" "@$file" >/dev/null
# Until the echo has finished: its status goes back to idle.
# Until the reply has finished, measured by the transcript rather than by
# the status. A session is idle at both ends of a turn, and polling for
# "idle" answers on the first poll -- before the turn has even started --
# so the report then covers the moment between the send and the first
# delta, and prints a window with nothing in it. The event count only
# grows, so "it stopped growing" is the one signal that cannot be true
# before the work begins.
events() {
./ui-sandbox.sh api "/sessions/$sid/transcript?limit=1" |
python3 -c 'import json,sys; d=json.load(sys.stdin); print(d[-1]["seq"] if d else 0)'
}
last=""
still=0
i=0
while [ "$i" -lt 120 ]; do
status=$(./ui-sandbox.sh api "/sessions/$sid" |
python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')
[ "$status" = idle ] && break
while [ "$i" -lt 180 ]; do
now=$(events)
if [ "$now" = "$last" ]; then
still=$((still + 1))
[ "$still" -ge 2 ] && break
else
still=0
fi
last=$now
sleep 2
i=$((i + 1))
done