Record what the shaping store cost and what it bought
Both gestures, since two widths in turn flatters a store and a drag does not, and the memory beside the time: the question that decided the shape of it was whether it is a fixed cost or one per text widget. Bring LAYOUT.md's §1 and §2 to what shipped. A widget returns its size rather than recording it, a slot carries a box rather than a translation, and the per-axis answer is `OnResize` rather than `is_size_independent` -- and the constraint the position chain replaced was an agent's, which the text now says. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2acfcf51aa
commit
d50e791130
2 files changed
+88
-68
No files matched your search
@@ -9,8 +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 `b1b3eca`, eighteen commits,
|
||||
workspace tests passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised
|
||||
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
|
||||
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
|
||||
@@ -159,16 +161,14 @@ above were measured on. Earlier statements here that the retained layout was
|
||||
"about 1.6x the old 3.14 ms resize" compared a 260-widget depth-8 tree against
|
||||
that 1061-widget depth-7 one, and are withdrawn.
|
||||
|
||||
`tests/fair.rs` in `/home/bob/repos/iris-perf2` is the load that does carry it:
|
||||
one hand-written tree -- 40 rows of `Dir::RIGHT` span holding a 40px rect and a
|
||||
`Dir::DOWN` span of one wrapping and one non-wrapping text -- in source that
|
||||
compiles unchanged on `43ce8c7` and on #18. 500 resizes alternating 900 and 892
|
||||
wide at 1200 tall, `perf stat -e instructions:u` on the test binary directly:
|
||||
|
||||
| revision | instructions/frame | median frame | the first paragraph's right edge |
|
||||
| --- | --- | --- | --- |
|
||||
| before #16 (`43ce8c7`) | 60.4M | 5.49 ms | **914.2 px, past the 900 px output** |
|
||||
| #18 head (`b1b3eca`) | 133.4M | 11.8 ms | 881.4 px |
|
||||
`tests/revision_cost.rs` is the load that does carry it: one hand-written tree
|
||||
-- 40 rows of `Dir::RIGHT` span holding a 40px rect and a `Dir::DOWN` span of
|
||||
one wrapping and one short non-wrapping text, each its own random words -- in
|
||||
source that compiles unchanged on `43ce8c7` and on #18. Drop it into an old
|
||||
worktree and run it there; take the number from `perf stat -e instructions:u`
|
||||
on the test binary rather than the clock. It also prints where the layout put
|
||||
the paragraphs, which is the other half of the comparison, and `text_memory`
|
||||
in the same file reports what the tree holds.
|
||||
|
||||
**The old code is faster because it skips the question, not because it answers
|
||||
it more cheaply.** Two shortcuts pay for the 2.2x, and both are wrong:
|
||||
@@ -182,8 +182,8 @@ it more cheaply.** Two shortcuts pay for the 2.2x, and both are wrong:
|
||||
rather than the part left for it, which its own comment records as a
|
||||
deliberate choice ("tempting to subtract the abs & rel from the ctx outer,
|
||||
but that would create inconsistent sizing"). In the fixture above that shapes
|
||||
the paragraph at 900 rather than the 860 left beside the rect, and it draws
|
||||
14 px past the edge of the output.
|
||||
a paragraph at 900 rather than the 860 left beside the rect, and it draws
|
||||
29 px past the edge of the output.
|
||||
|
||||
So the amplification #18 pays is the cost of asking in the right box. That does
|
||||
not excuse the size of it, and most of it turns out not to be layout at all.
|
||||
@@ -320,38 +320,55 @@ 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.
|
||||
|
||||
Two capacity changes, prototyped in `/home/bob/repos/iris-perf2` and measured
|
||||
on the fair fixture above:
|
||||
`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.
|
||||
|
||||
| | instructions/frame | median frame |
|
||||
| --- | --- | --- |
|
||||
| #18 head | 133.4M | 11.8 ms |
|
||||
| + `TextBuffer` keeps 8 shaped layouts | 30.8M | 3.09 ms |
|
||||
| + `TextView` keeps 8 placed results | 18.2M | 1.85 ms |
|
||||
On `tests/revision_cost.rs`, 500 resize frames, every paragraph its own random
|
||||
words:
|
||||
|
||||
That is **3.3x fewer instructions than the pre-#16 code**, on the tree where the
|
||||
pre-#16 code puts the paragraph 14 px outside the output. On the random tree at
|
||||
depth 8 the same two changes take a resize from 55.1M to 18.7M instructions per
|
||||
frame (5.28 to 1.80 ms) and a cold frame from 13.4 to 11.5 ms. Widget draws do
|
||||
not move at all -- 449 either way -- so this removes work per draw and leaves
|
||||
every retained-layout rule alone.
|
||||
| | 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 |
|
||||
|
||||
Neither is 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; these hold a
|
||||
*drawing* keyed on the width that produced it, and the widget still draws. The
|
||||
existing single-entry caches already make exactly this assumption; only their
|
||||
capacity changes. Workspace tests and the ignored 100-seed sweep pass with both
|
||||
in. Still to do before either lands: headless reference renders, an eviction
|
||||
rule better than a FIFO cap (a transcript holds thousands of texts, and eight
|
||||
layouts each is real memory -- entries older than the current frame are what
|
||||
wants dropping), and a decision on whether this belongs in #18 or a slice of
|
||||
its own.
|
||||
**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.
|
||||
|
||||
The remaining costs after those are:
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
The remaining costs after this are:
|
||||
|
||||
- **Writing a glyph instance per glyph per draw.** With shaping and placement
|
||||
memoized, `Painter::glyphs`, `Layers::write` and `InstanceList::push` are
|
||||
most of what is left. A cold frame writes 48,050 instances for a tree that
|
||||
kept, `Painter::glyphs`, `Layers::write` and `InstanceList::push` are most of
|
||||
what is left. A cold frame writes 48,050 instances for a tree that
|
||||
holds 10,872. What reaches the GPU is 10,872, since a layer uploads whole and
|
||||
`set_instance` cancels a dirty mark when the bytes are unchanged -- so this is
|
||||
CPU cost only, but a one-leaf repaint still uploads 5,863 instances where the
|
||||
@@ -359,7 +376,7 @@ The remaining costs after those are:
|
||||
- **A container measures a child by drawing it in a box it will not keep.**
|
||||
The remaining-region trial removes many mismatches, but an unknown child's
|
||||
measured length can still make its final box differ, and nested containers
|
||||
compound those redraws. The general form of the text caches is a widget
|
||||
compound those redraws. The general form of the shaping store is a widget
|
||||
answering from a box it has already drawn in, which -- unlike a size answer --
|
||||
is realized by definition; the cost is retaining more than one drawing's
|
||||
primitives, which is why text, where only the shaped layout has to be kept,
|
||||
|
||||
Reference in new issue
Block a user