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:
iris committed 2026-09-08 22:22:06 -04:00
1 parent 4fdabc39d0
commit 1318e149f5
8 files changed
+241 -97

No files matched your search

+15
View File
@@ -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