Files
ai-app/app/stream-bench.sh
T
irisandClaude Opus 5 ab6a797941 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>
2026-09-03 18:50:29 -04:00

96 lines
3.7 KiB
Bash
Executable File

#!/bin/sh
# Streams a message into the sandbox session on screen and prints the app's
# render report for the time it took -- the standard measurement for "is a
# reply that is still arriving cheap", the way transcript-bench.sh is for
# scrolling one that has settled.
#
# ./stream-bench.sh FILE restart the app, open the first
# session, stream FILE into it, report
# ./stream-bench.sh -k FILE keep whatever session is open now
#
# The session is the first one the sandbox lists (ui-sandbox.sh spawn makes
# one), and the file is echoed back a word at a time, which is the shape a
# real reply arrives in. The numbers to read are `markdown reparsed while
# streaming` -- how many times, and how long each -- and the worst
# `record: one block`; both are proportional to how much of the reply the
# tail reparse has to cover, which is what the live parse exists to bound.
set -eu
cd "$(dirname "$0")"
. ./android-env.sh >/dev/null 2>&1
keep=""
while getopts k flag; do
case $flag in
k) keep=1 ;;
*) exit 2 ;;
esac
done
shift $((OPTIND - 1))
file=${1:?usage: stream-bench.sh [-k] FILE}
sid=$(./ui-sandbox.sh api /sessions | python3 -c 'import json,sys; print(json.load(sys.stdin)[0]["id"])')
if [ -z "$keep" ]; then
adb shell am force-stop com.example.aiapp
adb shell am start -n com.example.aiapp/.MainActivity >/dev/null
sleep 5
ui-trace record -d 3000 --do 'tap 500 545' -o /tmp/bench-open.txt >/dev/null 2>&1
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 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 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
sleep 1
ui-trace record -d 2000 --do 'tap 723 205' -o /tmp/bench-report.txt >/dev/null 2>&1
sleep 1
adb logcat -d -s ai-app:I | sed -n '/ai-app render report/,$p' | sed 's/^.*ai-app : //'