iris: a transcript row is a column of markdown blocks, so a streamed delta costs one block

A row was one TextEdit holding the whole message, so every delta
re-shaped every paragraph of a long reply through parley -- the one phase
where iris trails Compose on the phone (p50 18.2ms vs 13.4ms, bench v2).

- client-core/src/markdown_blocks.rs: split a message into its top-level
  blocks with their source, through the same pulldown-cmark the renderer
  parses with so the two cannot disagree about where a block starts, plus
  common_prefix. Appending markdown can rewrite an earlier block (a
  trailing --- turns the paragraph above into a heading), so the fast
  path compares the prefix it keeps rather than assuming it -- with the
  test that says so.
- transcript-ui: a row is a Span of one TextEdit per block;
  RowBlocks::apply_delta replaces the block a delta lands in;
  TranscriptScreen keeps the tail row's blocks, seeded in build_tree as
  well as push_row (a screen opened onto a streaming reply took the
  rebuild path for its first delta otherwise, with nothing to say so).
- A block is the selection unit: Selection is keyed by (RowKey, u32),
  which is reading order at both levels, and the pointer-captured half of
  a drag resolves the block under the finger from its drawn box
  (Selection::locate) instead of from the row's extent.

Pass condition: a_delta_into_a_long_reply_redraws_the_same_widgets_as_a_short_one
drives a real UiRenderState and asserts the draw count for a delta into a
100-paragraph (3,000+ char) reply equals the count for a one-paragraph
one. 30 either way; it read 630 against 30 twice on the way there.

Emulator stream phase, same AVD before and after: p50 61.5 -> 54.5ms,
p90 211.7 -> 113.1ms, p99 342.6 -> 137.4ms, worst 403.6 -> 143.0ms, 202
-> 293 frames in the same 21 seconds. Selection across blocks verified
with a real long-press drag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-06 17:33:37 -04:00
1 parent 167862ca1b
commit e1030d69f6
14 files changed
+871 -82

No files matched your search

+66 -5
View File
@@ -107,17 +107,78 @@ Rig fix on the way past: `iris/android-app/run-bench.sh` polled logcat for
and printed a report that had never been run. It polls for the report's
own first line now.
### Bench, before Task B (emulator, 2026-09-06)
### Task B, closed 2026-09-06: a streamed delta costs one markdown block
A transcript row was one `TextEdit` holding the whole message, so every
delta re-shaped every paragraph of a long reply through parley -- the one
phase where iris trailed Compose on Iris's phone. A row is a **column of
one `TextEdit` per top-level markdown block** now, and a delta that lands
in the last block is one `set_with_spans` on that block.
- **`client-core/src/markdown_blocks.rs`** is the split: `split_blocks`
(top-level blocks with their source, via the same `pulldown-cmark` the
renderer parses with, so the two cannot disagree about where a block
starts) and `common_prefix`. Seven tests, including the one that says
the fast path must **compare** rather than assume: appending `---` under
a paragraph turns that paragraph into a heading, so an already
laid-out block is not always still what it was.
- **`iris/transcript-ui/src/row.rs`** builds the column and owns
`RowBlocks::apply_delta`; **`lib.rs`** keeps the *tail* row's blocks
(`TranscriptScreen::tail`) since that is the only row a delta reaches.
- **A block is the selection unit**, not a row: `Selection` is keyed by
`SelKey = (RowKey, u32)`, which compares in reading order at both
levels so every range query in that file is unchanged. The list-level
(pointer-captured) half of a drag resolves the block under the finger
from its drawn box (`Selection::locate`) instead of doing arithmetic
from the row's extent.
**Pass condition, met**: `a_delta_into_a_long_reply_redraws_the_same_widgets_as_a_short_one`
(`transcript-ui/src/lib.rs`) drives a real `UiRenderState` and asserts the
`Widget::draw` count for one delta into a 100-paragraph (3,000+ character)
reply equals the count for the same delta into a one-paragraph reply.
**30 either way.** It is a real test, not a tautology: it read **630
against 30** at three points on the way -- once because `Span`'s measure
pass redrew every child, and once because `build_tree` did not seed
`tail`, so the first delta after opening a screen took the rebuild path
with nothing on screen or in `take_rebuilds()` to say so.
Two things tried and dropped, so the next session does not redo them.
`Painter::measure` (a container asking a clean child for its size instead
of drawing it provisionally) fixed one of the 630s but the test passes
without it once the `tail` seeding is right, so it was removed rather than
kept on speculation. And the emulator's own numbers say the remaining
cost is not in the block split.
**Verified on the emulator** beyond the counter: the transcript draws its
blocks with their own spacing (heading, prose, fence), and
`ui-trace record --do "holddrag 300 700 700 1000 700 600"` logs
`iris selection: begin at row (3187, 0)` then `extend to row (3187, 1)`
with the highlight crossing from the heading into the code block -- a
selection that spans blocks, which is what the re-key had to keep.
### Bench, stream phase, before and after Task B (emulator, 2026-09-06)
`iris/android-app/build-apk.sh debug --abi x86_64 --features
"transcript-screen bench force-gles"` + `run-bench.sh`, this checkout's
AVD. Emulator absolutes transfer nothing; the before/after ratio on the
same emulator does.
stream: 202 frames over 21.0s
late: 197 (97.5%)
total p50 61.5ms p90 211.7ms p99 342.6ms
worst 403.6ms
Same AVD, same fixture, same build flags, 20 minutes apart. Emulator
absolutes transfer nothing; the ratio does.
before after
stream: 202 frames over 21.0s stream: 293 frames over 21.0s
late: 197 (97.5%) late: 285 (97.3%)
p50 61.5ms p50 54.5ms (-11%)
p90 211.7ms p90 113.1ms (-47%)
p99 342.6ms p99 137.4ms (-60%)
worst 403.6ms worst 143.0ms (-65%)
The tail is where the whole-message re-layout lived, and it is where the
change shows: 91 more frames delivered in the same 21 seconds. The p50
moves least, which is consistent -- a delta into a *short* message never
cost much. **The phone number is Iris's to take**; nothing here is a
statement about her device.
- [x] **Merge the `DragGesture` work** -- done 2026-09-06 (merge commit