From 46d3a6fd418ec87765fb6121d9cbba1bccebfe19 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 5 Sep 2026 22:14:35 -0400 Subject: [PATCH] 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 --- AGENTS.md | 7 +++ docs/IRIS.md | 47 +++++++++++++++ docs/RUST.md | 161 ++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 199 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6798462..d6a2552 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -261,6 +261,13 @@ Each exists because something was invisible without it. framework, from `atrace` text output with no trace processor needed. It is how the cost of a layout node per link was attributed to the framework rather than guessed at. +- **`iris/android-app/build-apk.sh [debug|release] [--abi ...] [--features + ...]`** builds iris-android-app's cdylib (`cargo ndk`) and its APK + (Gradle) in one step and verifies the result (`aapt2`/`apksigner`), and + **`iris/android-app/run-bench.sh [--apk PATH]`** installs it on this + checkout's own emulator, taps "Run benchmark" by label, and prints the + report -- written so the P0 build/install/tap/read-report cycle stops + being retyped by hand each time (docs/RUST.md's P0 box). ### Driving the UI diff --git a/docs/IRIS.md b/docs/IRIS.md index cb1a2af..4610707 100644 --- a/docs/IRIS.md +++ b/docs/IRIS.md @@ -437,3 +437,50 @@ with a number instead of a guess (RUST.md's I5 box). blocked handing the frame to the driver," not a confirmed GPU-completion time. Enough to separate "iris is slow building the frame" from "iris is slow handing it off," not enough to claim an exact GPU budget. + +## 2026-09-05: `List::replace_back`/`List::clear`, and `TranscriptScreen::apply` + +Fixes the "every client refolds and rebuilds the whole widget tree per +streamed event" cost RUST.md's P0 box measured (20 events/second against a +~3,200-row transcript). Two small additions to `iris::widget::List` +(`iris/src/widget/list.rs`), plus one new method on `transcript-ui`'s +`TranscriptScreen`. + +- **`List::replace_back(row: ListRow) -> Option`**: swaps the + *last* row's widget for a new one without moving it — same slot index, + so an anchor already pinned there (in particular a list flush with its + own end) stays pinned, and a `List` scrolled elsewhere is untouched. + `None` if the list is empty. `RowKey` may differ between the old and new + row; only `heights`/`extents` care, and both are invalidated for the + evicted key the same way `pop_back` already does. +- **`List::clear()`**: drops every loaded row and resets to `List::new`'s + state (`more_before`/`more_after` untouched — a caller that wants those + cleared too calls `set_more_before(None)`/`set_more_after(None)` itself). + The fallback path for a change that touches more than the tail. +- **`transcript_ui::TranscriptScreen::apply(&self, rsc, old: &[TranscriptItem], new: &[TranscriptItem])`**: + the incremental alternative to rebuilding the whole screen from + `transcript_ui::build_tree` on every folded event. Diffs the two + `group_tool_runs` outputs and picks the cheapest update: nothing changed + (no-op), a pure append (`push_row`, unchanged cost), or — the common + streaming case, a delta into a still-open assistant message — a rebuild + of just the one changed row via `List::replace_back`, with any further + new rows appended after it. A row changing *before* the tail (only + `group_tool_runs` retroactively grouping tool calls into a run does + this) falls back to `List::clear` plus a full rebuild, counted in + `TranscriptScreen::take_rebuilds()`. `bench_client.rs`, `transcript_client.rs` + and `desktop-app/app.rs` all call this now instead of rebuilding on every + event; only the opening page (and `apply`'s own fallback) still calls + `build_tree`. +- **`TextEditCtx::set_with_spans(text, spans)`**: `set()` plus a fresh + `Vec` in one call, needed because a streamed row's markdown + re-renders to both a new string and a new span list on every delta and + the two have to land together — a stale span list drawn against new + text can point past its end. `set()` itself is unchanged (still clears + spans to none, as before). + +Measured on this checkout's emulator (`iris/android-app/run-bench.sh`, +release, x86_64, `force-gles`): worst-frame and p99 during the streaming +phase dropped from 369.3ms/284.5ms (full rebuild per event, prior pass) to +~101–130ms/~76–103ms across three runs (this fix) — see RUST.md's P0 box +for the full numbers and the comparison's caveats (different AVD +instances, not a controlled A/B on identical hardware state). diff --git a/docs/RUST.md b/docs/RUST.md index 5f96ad8..0f93e26 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -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` 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`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 101–130ms and `p99` from + 284.5ms to 76–103ms 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,