Record both halves of the text fix, and what dirtying many widgets shows
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60bf2568f8
commit
592cc808ca
1 file changed
+58
-37
@@ -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:
|
||||
|
||||
|
||||
Reference in new issue
Block a user