docs: record the streaming-rebuild fix, its numbers, and the new scripts

RUST.md's P0 box gets the fix, the before/after streaming-phase numbers
(with their caveats), the build-apk.sh/run-bench.sh scripts, and what the
dropout-fix pass's three remaining verifications are blocked on (the
sandbox ai-server currently fails to build, unrelated to this change).
IRIS.md gets the List::replace_back/clear and TranscriptScreen::apply
API entries. AGENTS.md's rigs section gets one sentence on each script.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-05 22:14:35 -04:00
1 parent 5655fa8093
commit 46d3a6fd41
3 files changed
+199 -16

No files matched your search

+145 -16
View File
@@ -36,6 +36,20 @@ session spending an afternoon on them again.
## Where things stand (2026-09-05)
- **Streaming no longer costs a full rebuild** (P0's box, "Streaming no
longer costs a full rebuild" subsection): `iris::widget::List::
replace_back`/`clear` plus `transcript_ui::TranscriptScreen::apply`
replace the "refold + rebuild the whole ~3,200-row tree per event" path
in all three clients. Worst/p99 frame time in the streaming phase
dropped roughly 3x on this checkout's emulator (see the box for the
exact numbers and their caveats). Blocked and not done this same pass:
the three `iris-scroll.sh` runs, the host-GPU `FrameReport` retake, and
the `EMU_GPU=software` cold boot the dropout-fix pass left open --
`app/ui-sandbox.sh`'s `ai-server` currently fails to build
(`event_model::Event::LimitReached` missing, unrelated to this pass's
diff). Two new scripts, `iris/android-app/build-apk.sh` and
`iris/android-app/run-bench.sh`, now do the build/install/tap/read-report
cycle that used to be typed out by hand each time.
- **The intermittent touch-scroll dropout is root-caused and fixed,
2026-09-05.** Not the previously-suspected coalesced first
`ACTION_MOVE` (ruled out) -- a gesture's `ACTION_DOWN` can land on a
@@ -1515,20 +1529,23 @@ accepted.
the bare REST fetch (no live stream yet) looked perfect on its own,
and only *resuming* a stream after it exposed the seam.
**Deliberately left simple, and why** (`app.rs`'s module doc has the
full account): every incoming SSE event refolds the session's whole
item list and rebuilds the entire right-hand widget tree from
scratch, rather than reaching for `TranscriptScreen::push_row`'s
incremental append -- `push_row` can only add a new row, and a
streaming reply is exactly a row whose text keeps changing after it
first appears. Fine at the size a desktop session's conversation
is; wrong for a long, fast-streaming one, and the real fix needs
`transcript-ui` to expose updating a row already on screen, which it
does not yet. The composer's in-progress text is saved and restored
across a rebuild so a reply streaming in while the reader is typing
a followup doesn't erase it. No history paging (I3's job, reused
as-is if this becomes permanent) and no scroll-position preservation
across a rebuild -- both named rather than silently missing.
**Deliberately left simple at the time, later fixed** (`app.rs`'s
module doc has the full account): every incoming SSE event used to
refold the session's whole item list and rebuild the entire
right-hand widget tree from scratch on every event, rather than
reaching for `TranscriptScreen::push_row`'s incremental append --
`push_row` could only add a new row, and a streaming reply is
exactly a row whose text keeps changing after it first appears.
Fine at the size a desktop session's conversation is; wrong for a
long, fast-streaming one -- fixed below (this same box's "Streaming
no longer costs a full rebuild" entry) by giving `transcript-ui` a
`TranscriptScreen::apply` that updates a row already on screen
instead of rebuilding every row around it. `rebuild_transcript`
still runs the whole tree once, for a freshly loaded/selected
session and for `apply`'s own rare full-rebuild fallback. No history
paging (I3's job, reused as-is if this becomes permanent) and no
scroll-position preservation across a rebuild -- both still named
rather than silently missing, and neither depends on the fix below.
Background network I/O runs on plain `std::thread`s reporting back
through winit's `EventLoopProxy<AppEvent>` rather than iris's own
`Tasks`/`task_on`, because `Tasks` only requests a redraw once after
@@ -3581,8 +3598,12 @@ device.
400 fixture events replay at 20/s through `fold_event` -- the same
fold path a live SSE frame takes in `transcript_client.rs`'s own
`apply_event` -- each one triggering `rebuild_transcript`'s full
`transcript_ui::build_tree` rebuild, same tradeoff as
`TranscriptClient`/`desktop-app`. A battery sampler runs
`transcript_ui::build_tree` rebuild at the time this box was
written, same tradeoff as `TranscriptClient`/`desktop-app`. **Fixed
2026-09-05, later the same day**: all three now call
`TranscriptScreen::apply` instead -- see this same section's
"Streaming no longer costs a full rebuild" entry below for the
before/after numbers. A battery sampler runs
concurrently on its own `tokio::spawn`d task (not through
`ctx.update`, since a JNI battery read needs no widget-tree access),
attaching whichever thread it runs on via a stored `JavaVM` --
@@ -3715,6 +3736,114 @@ device.
branch was using (`iris/core/src/render/mod.rs`,
`iris/src/android/render.rs`, `iris/src/default/render.rs`).
**Streaming no longer costs a full rebuild, 2026-09-05.** The gap
named above and in E4/I5 (`push_row` can only append; a streaming
reply is a row that keeps *changing* after it appears) is closed:
`iris::widget::List` gained `replace_back` (swap the last row's
widget in place, same slot, so a pinned-to-newest list stays pinned
and an off-screen replace moves nothing on screen -- two new unit
tests, `replacing_the_last_row_stays_pinned_to_the_bottom` and
`replacing_the_last_row_out_of_view_does_not_move_visible_rows` in
`iris/src/widget/list.rs`) and `clear` (drop every row, the fallback
path). `transcript_ui::TranscriptScreen::apply(rsc, old_items,
new_items)` diffs `group_tool_runs(old)`/`group_tool_runs(new)`
(pure bookkeeping, no widget built doing it) and picks the cheapest
update: unchanged (no-op), pure append (`push_row`, same as before),
the common streaming case -- only the last row's content changed --
rebuilds just that one row and swaps it in with `replace_back`, or
(rare: `group_tool_runs` regrouping a row before the tail) a full
`List::clear` rebuild, counted by `TranscriptScreen::take_rebuilds()`.
Seven new unit tests in `transcript-ui/src/lib.rs`'s `diff_tests`
cover all three cases directly against synthetic `Vec<FoldedRow>`s
(no widget/Rsc needed for the decision itself). `bench_client.rs`,
`transcript_client.rs` and `desktop-app/app.rs` all call `apply` now
instead of rebuilding per event; `IRIS.md`'s 2026-09-05 entry has
the full API account. `TextEditCtx` also gained `set_with_spans`
(`iris/src/widget/text/edit.rs`) -- `set()` plus a fresh span list
in one call, since a streamed row's re-rendered markdown needs both
to land together.
**Two new scripts, `iris/android-app/build-apk.sh` and
`iris/android-app/run-bench.sh`**, written this pass after repeating
the ANDROID_HOME/NDK-export/`cargo ndk`/Gradle-release/keystore/
apksigner incantation by hand one too many times. `build-apk.sh
[debug|release] [--abi arm64-v8a|x86_64] [--features "..."]` builds
the cdylib and the APK and verifies it (badging, and signing for a
release build); `run-bench.sh [--apk PATH]` installs on this
checkout's own emulator (`emu serial`), taps "Run benchmark" by
label (no coordinates), polls logcat for the report line, and prints
it. Used for everything below and for the redelivery at the end of
this box.
**Numbers, this checkout's AVD (`ai-app-2`), release, x86_64,
`force-gles`, via `run-bench.sh` -- three separate runs, same warm
AVD (not a fresh cold boot each time):**
frames=690 janky%=78.26 p50=26.9ms p90=60.3ms p99=103.4ms worst=130.1ms cpu_p50=7.4ms gpu_wait_p50=15.7ms
frames=691 janky%=84.95 p50=28.3ms p90=60.9ms p99=95.2ms worst=120.7ms cpu_p50=5.4ms gpu_wait_p50=18.2ms
frames=691 janky%=58.32 p50=18.9ms p90=40.3ms p99=75.6ms worst=101.4ms cpu_p50=3.5ms gpu_wait_p50=12.5ms
Against this same box's earlier iris-half reading (full rebuild per
event, a *different*, freshly-booted x86_64 AVD, host GPU):
`frames=372 janky%=56.99 p50=19.5ms p90=219.5ms p99=284.5ms
worst=369.3ms cpu_p50=0.4ms gpu_wait_p50=13.9ms`. The tail is what
moved: `worst` dropped from 369.3ms to 101130ms and `p99` from
284.5ms to 76103ms across all three post-fix runs, consistent with
removing the periodic full-tree-rebuild stall during the
20-events/second streaming phase. `p50`/`cpu_p50` are *not* a clean
comparison -- these three runs share one already-warm AVD instance
rather than each getting its own fresh cold boot the way the earlier
reading did, and `cpu_p50` in particular is noisy run to run (3.5 to
7.4ms here) in a way a controlled A/B would need to separate from
the code change itself. **What a future pass should do for a clean
number**: two fresh cold boots of the same AVD, one per build,
`run-bench.sh` on each, nothing else running.
**The three remaining I5 verifications the dropout-root-cause pass
left open (this box's "Touch-scroll dropout root-caused" subsection)
-- attempted this pass, blocked, not silently dropped.**
`app/iris-scroll.sh` needs `dev.iris.android.demo`'s plain
`transcript-screen` (non-`bench`) debug build, which needs a live
sandbox server (`app/ui-sandbox.sh`) to bake in at build time
(`build.rs`'s `AI_APP_TRANSCRIPT_HOST`/etc, skipped only under
`bench`). `./ui-sandbox.sh start` fails to build **the Rust
`ai-server` itself**, unrelated to anything in this box's diff:
`error[E0599]: no variant named 'LimitReached' found for enum
'event_model::Event'` in `server/src/session/mod.rs:2633`,
`server/src/session/claude/translate.rs:307`, and
`server/src/session/echo.rs:389` -- `event_model` and `server` have
drifted out of sync on this branch, most likely from concurrent
work elsewhere on `rustify` (this pass touched nothing under
`server/` or `event-model/`, confirmed by `git status`). Since
fixing that is a separate, unrelated repair (and risks colliding
with whatever pass is mid-edit there), this pass did not attempt
it. Consequently, not done this pass: the three `iris-scroll.sh`
runs, the host-GPU `FrameReport` table row retake, and the
`EMU_GPU=software` + `force-gles` cold-boot `FrameReport` (the
backend-isolation question) -- none of the three need the broken
server directly, but the first two need the same debug build the
server outage blocks, and by the time that was found there was not
enough of this pass left to justify a fresh `EMU_GPU=software` cold
boot (several minutes) for the third alone without also covering the
other two on the same session. A future pass: fix or wait out the
`server`/`event_model` drift, rebuild `dev.iris.android.demo` with
plain `transcript-screen` via `./build-apk.sh debug --abi x86_64
--features "transcript-screen force-gles"`, then run
`app/iris-scroll.sh` three times and retake the host-GPU row, and
separately cold-boot with `EMU_GPU=software` for the third.
**Redelivered, 2026-09-05.** `./build-apk.sh release --abi
arm64-v8a` (arm64-only jniLibs; an earlier step in this same pass
had left an x86_64 slice in there from the emulator testing above,
removed before this build so the delivered APK matches P0's
original arm64-only shape) -- `aapt2 dump badging` confirms
`native-code: 'arm64-v8a'` and the same
`dev.iris.android.demo.bench` id, `apksigner verify` the same
`CN=ai-app` cert as before. Copied over
`~/host/bench/iris-bench-arm64.apk`; `~/host/bench/README.md` gained
a one-line build-date/commit note so Iris can tell which build she
has.
- [ ] **P1 — session screen parity.** History paging backward (with the
page-boundary healing `client-core` does not have yet, below),
`TranscriptSource`-backed cache/server stitching, jump-to-latest,