From 592cc808ca5cbda2cb3f84a951f9364565dade12 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Mon, 14 Sep 2026 19:22:14 -0400 Subject: [PATCH] Record both halves of the text fix, and what dirtying many widgets shows Co-Authored-By: Claude Opus 5 --- docs/IRIS_EXTRACTION_HANDOFF.md | 95 ++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/docs/IRIS_EXTRACTION_HANDOFF.md b/docs/IRIS_EXTRACTION_HANDOFF.md index ea1d4b5..55a5333 100644 --- a/docs/IRIS_EXTRACTION_HANDOFF.md +++ b/docs/IRIS_EXTRACTION_HANDOFF.md @@ -9,10 +9,10 @@ Canonical `main` is **`ca2b4b2`** (#17, the headless rig). Sixteen slices are in. **#18 `split/18-position-chain`** is open and finished apart from one decision: -worktree `/home/bob/repos/iris-pr18`, head `e5f8b6b`, twenty commits, -workspace tests passing, fmt and clippy clean. The last two are the text -shaping store and three retained-layout details that claimed something they -did not do. It is LAYOUT.md §2's position chain, generalised +worktree `/home/bob/repos/iris-pr18`, head `77bb75e`, twenty-two commits, +workspace tests passing, fmt and clippy clean. The last four are the text +shaping work, three retained-layout details that claimed something they did +not do, and the many-widgets-dirty cases. It is LAYOUT.md §2's position chain, generalised to boxes. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean. **The one thing waiting on the owner.** `tabs` at 1920x1200 is no longer @@ -320,49 +320,70 @@ it (`TextView::width`/`tex` and `TextBuffer::layout_key`) each hold exactly one entry and a trial width alternating with a final width evicts the answer that is about to be asked for again. -`e5f8b6b` keeps more than one. A bounded store of shapings on `TextData`, -keyed by the text, the attrs and the width, holds the parley layout and the -glyphs placed from it; `TextBuffer` holds the one it is drawn as, which is -where `TextView::tex` was, so there is one place to invalidate rather than -two. Bounding the store rather than each buffer is the whole point: a -per-buffer cache of eight cost +79% on a tree of 4,000 texts, and lazy -eviction cannot fix that, because the texts holding the memory are exactly the -ones a retained layout has stopped drawing. +`e5f8b6b` and `2525637` are the two halves of the answer. -On `tests/revision_cost.rs`, 500 resize frames, every paragraph its own random -words: +**A new width is a line break, not a shaping.** Only the breaking depends on +the width -- the font selection, the unicode analysis and harfrust under it are +a function of the text and the attrs, and parley re-breaks them in place, which +is what its own editor does on every resize. On the depth-8 tree that is 107 +breaks at 0.119 ms where the shapings they replace were 4.0 ms. This is the +half that holds however far the width moves. -| | instructions/frame | median | p99 | max | the paragraph's right edge | -| --- | --- | --- | --- | --- | --- | -| before #16 (`43ce8c7`) | 56.1M | 4.99 ms | 5.71 ms | 6.47 ms | **929 px, past the 900 px output** | -| #18 head (`b1b3eca`) | 124.2M | 11.76 ms | 13.78 ms | 14.56 ms | 877 px | -| `e5f8b6b` | 17.7M | 1.76 ms | 2.23 ms | 5.39 ms | 877 px | +**A bounded store on `TextData` holds what re-breaking still cannot avoid:** +the glyphs, placed per width, keyed by the text, the attrs and the width, with +`TextBuffer` holding the pair it is drawn as -- which is where `TextView::tex` +was, so there is one place to invalidate rather than two. Bounding the store +rather than each buffer is the point: a per-buffer cache of eight cost +79% on +a tree of 4,000 texts, and lazy eviction cannot fix that, because the texts +holding the memory are exactly the ones a retained layout has stopped drawing. -**Two widths in turn flatters anything that remembers an answer**, so -`SWEEP=1` never repeats a width -- a drag rather than a toggle, and the case -nothing can hit across frames. There the same three are 56.2M, 123.6M and -45.9M instructions per frame: still 2.7x better than #18's head, and a little -better than the old code rather than three times better. Both numbers are the -honest ones, for different gestures. +Instructions per frame over 500 resize frames of `tests/revision_cost.rs`, for +widths that alternate -- the friendly case for anything that remembers an +answer -- and widths that never repeat, which is a drag: -Memory, same rig at 2,000 rows: 108.3 MB on #18's head, 112.7 MB with the -store, and **132.0 MB before #16**, which grows 23 MB over the resizes where -neither of the others grows at all. The store is 128 entries for the whole -`TextData`; 32 is not enough under `SWEEP=1` and 64 is. +| | alternating | never repeating | +| --- | --- | --- | +| before #16 (`43ce8c7`) | 56.1M | 56.2M | +| #18 head (`b1b3eca`) | 124.2M | 123.6M | +| the store alone | 17.7M | 45.9M | +| re-breaking alone | 32.9M | 32.8M | +| both | 20.6M | 24.2M | -Widget draws do not move at all -- 449 either way -- so this removes work per -draw and leaves every retained-layout rule alone. That matters for more than -purity: **text is the load that makes a redundant draw expensive, and so the -thing that shows when the layout is drawing more than it needs to.** The -counters keep saying so -- a text render is counted per ask rather than per -shaping, and glyph placements are counted separately -- so the store cannot -hide the amplification it is paying for. +Neither alone is good at both, which is the reason for having both. The frame +times that go with the last row are 1.88 ms median and 2.54 ms worst +alternating, 2.46 and 3.24 never repeating, against 4.99 and 6.47 before #16 -- +so the worst frame more than halves, and the two gestures are within a +millisecond of each other rather than a factor of two apart. + +Memory on the same rig at 2,000 rows: 108.6 MB on #18's head, 111.6 MB with +both, and **132.0 MB before #16**, which grows 23 MB over the resizes where +neither of the others grows at all. + +Widget draws do not move at all -- 449 either way -- so none of this touches a +retained-layout rule. That matters for more than purity: **text is the load +that makes a redundant draw expensive, and so the thing that shows when the +layout is drawing more than it needs to.** The counters keep saying so -- a +text render is counted per ask rather than per shaping, and breaks and glyph +placements are counted separately -- so neither half can hide the amplification +it is paying for. This is not the frame-local answer cache seed 98 rejected. That one let a parent lay out from a size the child's one drawing might not realize; this holds a *drawing* keyed on what produced it, and the widget still draws. The five reference renders and the resize render are byte-identical, and the -100-seed sweep passes. +sweep passes. + +**A frame that dirties many widgets at once was not being checked, and it is +where the settle order shows.** `77bb75e` adds two generated cases -- every +declared size changing together, and a spread of widgets marked for redraw +together, where nothing changes and so no box may move -- and `IRIS_PHASE=many` +to the rig, with `IRIS_DIRTY` widgets marked per frame. At 130 of 260 widgets, +**choosing which dirty widget to settle next is 24.5% of the frame**: the dirty +set is scanned once per widget settled, and a `HashSet` is walked by capacity +rather than by length. Memoizing the depth walk within one scan does not pay +(it trades parent lookups for memo lookups, 4% more instructions); the fix is +to stop rescanning, which changes the order widgets settle in and is hers to +agree first. The remaining costs after this are: