Incremental text: parley cannot, the app already does it, and the 9.5ms is the fixture
Iris asked to look into incremental text rendering, hoping parley supported it. It does not, by design: a `Layout` re-linebreaks and re-aligns freely but "if the text content or the styles applied to that content change then a new `Layout` must be created", its LRU cache holds harfrust's per-font shaper data rather than shaped runs, and its own `PlainEditor` rebuilds the whole layout from the whole buffer on every keystroke. The app already does what incremental layout would buy: `RowBlocks:: apply_delta` keeps one `TextEdit` per markdown block and re-shapes only the one a delta landed in. Re-splitting the markdown to find it is 18µs at 18,000 characters; comparing the blocks is 470ns. What is left is one `TextBuffer::shape` of that block, linear in its length at ~0.23ms per 1,000 characters here -- and the bench fixture's streamed message is 14,888 characters in a *single* block, a run-on paragraph with no blank line in it, so every delta reshapes all of it. That is 3.5ms of the measured 3.86ms frame. Real replies are not that: across 7,706 top-level blocks from 3,675 real assistant messages on this machine (lengths only, no content copied anywhere), p50 147 characters, p90 449, p99 836, largest 1,580, nothing above 4,000; code fences p50 126, largest 589. At those sizes a reshape is 48µs to 372µs here, roughly 0.12-0.93ms on the phone -- inside a 120Hz budget with no incremental anything. So the recommendation is not to build it, and to give the fixture's streamed message the paragraph structure a real reply has instead. Three runs added to `frame_profile.rs` so none of this is re-derived: what reshaping a growing message costs (including at the sizes real replies reach), where a delta's cost is, and what the fixture actually streams. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9bf714fa2e
commit
43a3a345e4
3 files changed
+219
-9
No files matched your search
+48
-4
@@ -528,10 +528,54 @@ every delta.
|
||||
|
||||
`fold_event`'s `items.to_vec()` per event was the hypothesis -- it is the
|
||||
exact shape of the Compose lesson in AGENTS.md's "Things that have
|
||||
bitten" -- and measuring it is what ruled it out. **Not yet designed**:
|
||||
making a row's text append incrementally rather than reshape touches how
|
||||
`TranscriptRow` holds its shaped text, which is load-bearing enough to
|
||||
raise before building.
|
||||
bitten" -- and measuring it is what ruled it out.
|
||||
|
||||
### Incremental text: parley cannot, and it turns out not to matter (2026-09-09)
|
||||
|
||||
Iris asked to investigate incremental text rendering and hoped parley
|
||||
supported it. **It does not, by design.** The crate's own docs: a
|
||||
`Layout` "supports re-linebreaking and re-aligning many times... but if
|
||||
the text content or the styles applied to that content change then a new
|
||||
`Layout` must be created". Its `LruCache` caches harfrust's per-font
|
||||
shaper data, instance and plan -- not shaped runs -- and its own
|
||||
`PlainEditor::update_layout` rebuilds the whole layout from the whole
|
||||
buffer on **every keystroke**. So there is nothing to adopt, and adding
|
||||
it would be upstream work in parley.
|
||||
|
||||
**And the app already does the thing incremental layout would buy.**
|
||||
`RowBlocks::apply_delta` keeps one `TextEdit` per top-level markdown
|
||||
block and re-shapes only the block a delta landed in; re-splitting the
|
||||
markdown to find that block is 18µs at 18,000 characters and comparing
|
||||
the blocks is 470ns. Neither is the cost.
|
||||
|
||||
**The 9.5ms is a bench-fixture artifact.** Measured with
|
||||
`frame_profile.rs`:
|
||||
|
||||
- Re-shaping a block is linear in its length -- ~0.23ms per 1,000
|
||||
characters on this desktop, so a message grown to 17,600 characters
|
||||
costs 4.1ms on its *last* delta and 842ms of shaping over the whole
|
||||
reply.
|
||||
- The fixture's streamed message is **14,888 characters in one block** --
|
||||
a synthetic run-on paragraph with no blank line in it, so every delta
|
||||
reshapes all of it. That is the whole of the frame: 3.5ms of the
|
||||
measured 3.86ms.
|
||||
- Real replies are not like that. Over **7,706 top-level blocks from
|
||||
3,675 real assistant messages** on this machine (block lengths only;
|
||||
no content left the machine): p50 147 characters, p90 449, p99 836,
|
||||
largest 1,580, and **nothing above 4,000**. Code fences are smaller
|
||||
still -- 170 of them, p50 126, largest 589.
|
||||
- At those sizes a live reshape is 48µs (p50), 208µs (p99) and 372µs
|
||||
(the largest block ever seen), or roughly 0.12-0.93ms on the phone.
|
||||
Comfortably inside a 120Hz budget, with no incremental anything.
|
||||
|
||||
**So: do not build incremental text.** What is worth doing instead is
|
||||
giving the fixture's streamed message the paragraph structure a real
|
||||
reply has, so the stream phase measures something that happens. Both
|
||||
apps read the same fixture, so the Compose/iris comparison stays sound,
|
||||
but numbers from before and after the change are not comparable to each
|
||||
other. Whether to keep a pathological block as a *labelled* stress case
|
||||
alongside it is Iris's call -- the danger of the current one is only that
|
||||
its number reads as "streaming costs 9.5ms" when nothing does.
|
||||
|
||||
### The Android release profile is `opt-level = 3`, not `"s"` (2026-09-09)
|
||||
|
||||
|
||||
Reference in new issue
Block a user