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:
1 parent
167862ca1b
commit
e1030d69f6
14 files changed
+871
-82
No files matched your search
+12
-9
@@ -191,14 +191,17 @@ does not repeat it again by hand.
|
||||
|
||||
## What is not started at all
|
||||
|
||||
- **The markdown *block* model beyond syntax spans** -- `highlight/markdown.rs`
|
||||
colours a `.md` file or fence for the highlighter, but does not build the
|
||||
block tree (headings, lists, tables, fences as distinct nodes) that a
|
||||
renderer walks to lay out prose versus code versus a table.
|
||||
`CodeFence.kt`'s use of `org.intellij.markdown` for that full CommonMark
|
||||
AST is Compose rendering plumbing, not something to port as-is; a Rust
|
||||
UI layer will want its own block parser or a crate for it, decided
|
||||
alongside the framework choice in RUST.md.
|
||||
- **A full markdown AST.** `markdown_blocks` (2026-09-06) splits a message
|
||||
into its *top-level* blocks -- heading, paragraph, fence, list, table,
|
||||
quote -- with each block's own source, which is what a renderer needs to
|
||||
lay out prose versus code and what lets a streamed delta re-lay out one
|
||||
block instead of the message (docs/RUST.md's Task B). What it
|
||||
deliberately does **not** build is the tree below that: nested list
|
||||
items, table cells, inline spans. Inline styling is still the renderer's
|
||||
own job per block (`iris/transcript-ui/src/markdown.rs`), and nothing
|
||||
has needed the rest yet. `CodeFence.kt`'s use of `org.intellij.markdown`
|
||||
for a full CommonMark AST is Compose rendering plumbing, not something
|
||||
to port as-is.
|
||||
- **`TranscriptUnits.kt`** (see above) -- deliberately out of scope, since
|
||||
it flattens a row into bounded units for a *specific* lazy-list
|
||||
framework's composition cost, which is a fact about that framework
|
||||
@@ -209,5 +212,5 @@ does not repeat it again by hand.
|
||||
`./run-tests.sh` from the repo root now runs `event-model`, `client-core`
|
||||
and `server` in that order (each `cargo test`, forwarding arguments the
|
||||
same way it always has). From `client-core/` directly: `cargo test`
|
||||
(109 tests), `cargo clippy --all-targets`, `cargo fmt` -- all clean as of
|
||||
(119 tests), `cargo clippy --all-targets`, `cargo fmt` -- all clean as of
|
||||
this writing (2026-09-06).
|
||||
@@ -8,6 +8,36 @@ capability that moved. Small and trivial changes do not go here.
|
||||
An entry gives the date, what changed, why, and a short before/after where
|
||||
it helps judge the change without the session that made it. Newest first.
|
||||
|
||||
## 2026-09-06: a transcript row is a column of blocks, and a block is the selection unit
|
||||
|
||||
`transcript-ui`'s row builder used to make **one** `TextEdit` per message.
|
||||
It makes one per top-level markdown block now -- heading, paragraph,
|
||||
fenced code, list, table -- in a `Span::down`, because a streamed delta
|
||||
into a single buffer re-shaped the whole message through parley on every
|
||||
event. `client_core::markdown_blocks::split_blocks` does the splitting;
|
||||
`row::RowBlocks::apply_delta` updates the block a delta lands in and
|
||||
leaves the rest of the message's layout alone.
|
||||
|
||||
**The change to judge, since it is what a reader feels**:
|
||||
`Selection` is keyed by `SelKey = (RowKey, u32)` -- a row and a block --
|
||||
so **a block, not a row, is the unit a selection steps in**. A drag still
|
||||
runs from a reply into the tool output beneath it and copies as one
|
||||
thing; what changed is that the row under the finger is filled in block by
|
||||
block rather than all at once, which is if anything closer to what the
|
||||
old shortcut in `Selection`'s module doc was apologising for. `register`
|
||||
takes a `SelKey`; `unregister` still takes a `RowKey` and now drops every
|
||||
block of it (dropping only the first is how a freed widget gets left in
|
||||
the map -- the shape docs/REVIEW-2026-09-06.md's finding 1 called out).
|
||||
|
||||
`Selection::locate(ui, render, pos_window)` is new: which block is under a
|
||||
window position, with that block's own local position and size. The
|
||||
list-level handler uses it for the pointer-captured half of a drag,
|
||||
instead of computing a row-local position from `List::extent`.
|
||||
|
||||
`row::build_row` returns `(RowKey, StrongWidget, Option<RowBlocks>)` --
|
||||
the third is the per-block state a caller keeps only for the row a reply
|
||||
is streaming into, and is `None` for a tool run, which never streams.
|
||||
|
||||
## 2026-09-06: a reported `Size` may not carry `dp`; `Len::fold_dp`
|
||||
|
||||
**New: `Len::fold_dp(density) -> Len`** -- the same fold `apply_rest` does
|
||||
|
||||
+17
-2
@@ -643,8 +643,23 @@ do not duplicate it there.
|
||||
|
||||
## From the phone, bench v2 (2026-09-06): streaming re-lays out the whole message
|
||||
|
||||
- [ ] **Streaming a delta into a long message costs a full text layout of
|
||||
that message.** Iris's phone report (`docs/bench/iris-phone-v2-2026-09-06.md`):
|
||||
- [x] **Streaming a delta into a long message costs a full text layout of
|
||||
that message.** **Done 2026-09-06** -- a row is a column of one
|
||||
`TextEdit` per markdown block (`client_core::markdown_blocks`,
|
||||
`row::RowBlocks::apply_delta`), so a delta re-shapes the last block and
|
||||
keeps every earlier block's layout. A block is the selection unit now
|
||||
(`Selection`'s `SelKey`); selection across blocks and rows still works,
|
||||
checked on the emulator with a real long-press drag. Pass condition met
|
||||
in `a_delta_into_a_long_reply_redraws_the_same_widgets_as_a_short_one`:
|
||||
a delta into a 100-paragraph reply redraws the same widget count as one
|
||||
into a one-paragraph reply (30 either way). 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. docs/RUST.md's Task B box has the detail and the two dead
|
||||
ends. **The phone is the measurement that decides it** -- these are
|
||||
emulator numbers and only the ratio transfers.
|
||||
|
||||
The original entry, for the record: Iris's phone report (`docs/bench/iris-phone-v2-2026-09-06.md`):
|
||||
the stream phase is the one place iris is behind Compose (p50 18.2 ms vs
|
||||
13.4 ms; p99 level at ~43 ms). `TranscriptScreen::apply` replaces only
|
||||
the last row, but that row is the growing message, and replacing it
|
||||
|
||||
+66
-5
@@ -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
|
||||
|
||||
Reference in new issue
Block a user