iris: redrawing one widget cost O(its own primitives squared)
Iris's report was that expanding a tool card holding a long,
horizontally-scrolling edit lags on her phone. The cause is not text
layout: shaping and rasterising a 51,200-glyph block is 20ms, and the
frame that drew it took 1.37 seconds.
A widget redrawn in place frees every primitive it owned and writes
fresh ones. Freeing compacts each layer's draw order with swap_remove,
so ~N primitives are renumbered, and finding the handle to renumber was
a linear scan of everything that widget drew -- O(N^2) in the widget's
own primitive count. A paragraph never notices; one text widget holding
a whole old_string and new_string is every glyph in the card.
The arena now records, per slot, where that slot's handle sits in its
owner's ActiveData::primitives, written at the one place a handle is
taken (Painter::own), and apply_free indexes straight to it.
50,000 glyphs, redrawn: before 636ms after 2.4ms
per glyph: before 12.7us after 0.043us, flat in N
benches/message_list.rs gains scenario (g) for it, reporting per-glyph
because flat is the pass condition and a total hides it. That file had
also stopped running entirely: scenarios (a) and (e) built a LazySpan
with no mask around it, which the span now asserts against, so the
benchmark panicked on its second line. Fixed here too.
Also, on Iris's instruction: the copied report no longer inlines a tail
of the app log. Dev Updater's Runtime tab reads the same ring through
devlog's provider, so it was the same lines twice; the diagnostics pane
still names the provider's authority to read them from.
This commit is contained in:
1 parent
4fdabc39d0
commit
1318e149f5
8 files changed
+241
-97
No files matched your search
+44
-1
@@ -12,7 +12,50 @@ things still stay out.
|
||||
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-08 (newest): one `ScrollController`, a `Scrollable` trait, and `Pin`
|
||||
## 2026-09-08 (newest): redrawing one widget cost O(its own primitives squared)
|
||||
|
||||
Your report -- expanding a tool card with a long horizontally-scrolling
|
||||
edit in it lags -- is a framework defect, not a text-layout one, and the
|
||||
size of it is not close: **a 51,200-glyph block took 1.37 seconds to
|
||||
redraw, of which shaping and rasterising the text was 20ms.** It is 29ms
|
||||
now, and the cost is linear in the glyph count rather than quadratic.
|
||||
|
||||
What happened. A widget redrawn in place frees every primitive it owned
|
||||
and writes fresh ones. Freeing compacts each layer's draw order with
|
||||
`swap_remove`, and every primitive that gets swapped into a hole has to be
|
||||
told its new position -- so a widget with N primitives generates ~N
|
||||
renumberings. Finding the handle to renumber was a **linear scan of
|
||||
everything that widget drew**, which made the pass N^2. For a paragraph
|
||||
that is nothing. For one text widget holding a whole `old_string` and
|
||||
`new_string`, N is every glyph in the card.
|
||||
|
||||
The fix is a back-pointer: the primitive arena now records, per slot,
|
||||
where that slot's handle sits in its owner's `ActiveData::primitives`
|
||||
(`Primitives::handle_index`), written at the one place a handle is taken
|
||||
(`Painter::own`). `apply_free` indexes straight to it.
|
||||
|
||||
50,000 glyphs, redrawn: before 636ms/redraw after 2.4ms/redraw
|
||||
per glyph: before 12.7us after 0.043us (flat in N)
|
||||
|
||||
`benches/message_list.rs` grew scenario **(g)** for it, and the number to
|
||||
read is per-glyph: flat as N grows is the pass condition, and a total
|
||||
hides it. Two things about that file: it also caught the quadratic in
|
||||
`--phone`-sized text, and it had stopped running at all -- scenarios (a)
|
||||
and (e) built a `LazySpan` with no mask around it, which the span now
|
||||
asserts against, so the whole benchmark panicked on its second line.
|
||||
Fixed in the same change.
|
||||
|
||||
**What this does not fix, and what I would do next.** An open card still
|
||||
shapes, rasterises and submits *every* glyph of its input and output, not
|
||||
the screenful you can see -- iris does not cull within a widget, and a
|
||||
tool card is the one place that bites, because an `Edit`'s `old_string`
|
||||
and `new_string` go onto the card whole. The output half is already capped
|
||||
(`OUTPUT_LINES`/`OUTPUT_BYTES` in `tool.rs`, 80 lines or 4 KiB behind a
|
||||
"Show all"); the *input* half has no cap at all, and that is the
|
||||
asymmetry to close. Wrapping the block, which you asked for, changes the
|
||||
shape but not this cost: the same glyphs are laid out either way.
|
||||
|
||||
## 2026-09-08: one `ScrollController`, a `Scrollable` trait, and `Pin`
|
||||
|
||||
Your three points on `docs/SCROLL.md`, in one change. The shape is the one
|
||||
you proposed: **a controller both scrolling widgets contain**, rather than
|
||||
|
||||
@@ -944,6 +944,21 @@ diagnosis and what building it actually costs.
|
||||
the shaped-mask work (`.masked_by`, 38bf630) is the likeliest, since the
|
||||
old chain was `.masked()` *inside* the padding. Left ticked with the
|
||||
original symptom recorded rather than deleted, in case it comes back.
|
||||
- [ ] **An open tool card lays out every glyph of its input, however
|
||||
long.** iris does not cull within a widget -- a `Text` shapes,
|
||||
rasterises and submits the whole string whether or not the box it sits
|
||||
in can show it -- and a tool card is where that bites, because an
|
||||
`Edit`'s `old_string` and `new_string` go onto the card whole. The
|
||||
*output* half is already capped at 80 lines or 4 KiB behind a "Show
|
||||
all" (`tool.rs`'s `OUTPUT_LINES`/`OUTPUT_BYTES`); the input half has no
|
||||
cap at all, which is the asymmetry to close, and the cheaper fix of the
|
||||
two. Culling inside a `Text` is the other, and is a real design
|
||||
question: the shaped layout knows where each glyph is, so a viewport
|
||||
test is possible, but nothing else in iris cares where the screen is.
|
||||
Found 2026-09-08 chasing Iris's "expanding the edit card lags" report,
|
||||
whose actual cause was the quadratic `apply_free` (fixed; docs/IRIS.md).
|
||||
Wrapping the block does not change this cost -- the same glyphs are laid
|
||||
out either way.
|
||||
- [ ] **No overflow ellipsis.** `TextAttrs` can wrap or not wrap; there is
|
||||
no "one line, ellipsised" the way `maxLines = 1` + `TextOverflow.
|
||||
Ellipsis` gives Compose. A tool card's summary is clipped instead, so
|
||||
|
||||
Reference in new issue
Block a user