Record what the layout actually costs, and what of it is necessary

The random tree at depth 7 measured against the two revisions before it: the
GPU pass is a tenth of a millisecond everywhere, and every difference is the
CPU laying out. A repaint of one leaf costs 1313 draws and 6.5 ms where the
pre-#16 code cost one draw, because `redraw` escalates to the top size reader
before knowing whether the size changed at all. Drawing first and escalating
only on a different size takes that to one draw and takes a resize below the
pre-#16 number, but misplaces four widgets in the resize case, so it is the
owner's to call.

Also records the measure-by-drawing multiplier -- 1.3x the drawn widgets at
depth 4, 5.2x at depth 8 -- and that scrolling is now part of the random
trees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-14 14:36:11 -04:00
1 parent ba275fc966
commit 38f6f9ac07
1 file changed
+55 -3
+55 -3
View File
@@ -9,8 +9,8 @@ Canonical `main` is **`ca2b4b2`** (#17, the headless rig). Sixteen slices are
in. in.
**#18 `split/18-position-chain`** is open and finished apart from one decision: **#18 `split/18-position-chain`** is open and finished apart from one decision:
worktree `/home/bob/repos/iris-pr18`, head `4178dfb`, twelve commits, 49 tests worktree `/home/bob/repos/iris-pr18`, head `cdec293`, thirteen commits, 50
passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised tests passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised
to boxes. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean. 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 **The one thing waiting on the owner.** `tabs` at 1920x1200 is no longer
@@ -115,10 +115,62 @@ A chain is irrelevant at an example's couple of hundred primitives; a
transcript's glyphs are tens of thousands, which is the regime `chain_cost` transcript's glyphs are tens of thousands, which is the regime `chain_cost`
measures. measures.
### How much of that work is necessary
Measured 2026-09-14 on a random tree at seed 1, depth 7 -- 1061 widgets, 839
of them drawn, 10,872 primitive instances -- against the same rig at `43ce8c7`
(the last commit before #16 sized a widget by drawing it) and at `f942385`
(#16 itself). The rig is a `Harness` load counting widget draws, text shapes
and instance writes per frame, plus the GPU pass through timestamp queries;
it lives in the scratch worktrees `/home/bob/repos/iris-size-{old,new}` and is
not in the repository, because the counters it reads are patched into
`iris-core`.
| per frame | before #16 | #16 | #18 head |
| --- | --- | --- | --- |
| cold layout | 19.3 ms, 839 draws | 30.9 ms, 3317 | 23.6 ms, 2755 |
| repaint one leaf | 0.000 ms, 1 draw | 8.5 ms, 1683 | 6.5 ms, 1313 |
| scroll one scroller | 0.001 ms, 1 draw | 8.9 ms, 1683 | 6.5 ms, 1313 |
| resize the output | 3.2 ms, 839 draws | 31.8 ms, 6940 | 25.2 ms, 5512 |
| GPU pass | 0.129 ms | 0.114 ms | 0.115 ms |
**The GPU is not the subject.** The pass is a tenth of a millisecond at every
revision and every phase; all of this is the CPU laying out.
Three separate costs, in the order they are worth fixing:
- **A repaint escalates to the top size reader and redraws its whole
subtree.** `redraw` asks `mark_readers` first, so a widget whose drawing
changed but whose *size* did not still redraws everything from the highest
widget that ever read it. At depth 8 a one-rectangle repaint costs 4825
draws and 24 ms, which is more than building the tree from nothing. The
measured alternative is to draw the widget in its own box first, compare
the `Size` it returns with the one it had, and escalate only when it
differs: repaint falls to 1 draw and 0.000 ms, a scroll to 11 draws, and a
resize to 532 draws and 2.3 ms -- below the pre-#16 numbers on every line.
**It is not correct as written**: the resize case in `tests/generated.rs`
fails at seed 1 with four widgets misplaced, because drawing the widget
clears the mark that was the only thing stopping a later parent draw from
reusing it into a different box. The patch is kept at
`/tmp/escalate.patch`. This is the owner's call, since it changes when a
widget is drawn.
- **A container measures a child by drawing it in a box it will not keep.**
`Span::draw` places each child without an exact `size_hint` at
`UiRegion::FULL` to read its length, then places it again at the box it
worked out, and the second place redraws whatever the first drew. It
compounds with nesting: draws per drawn widget are 1.3x at depth 4, 3.3x at
7 and 5.2x at 8. Wrapping text is shaped at the trial width and then again
at the real one -- 332 shapes for 152 text widgets on a cold frame.
- **Redundant draws rewrite primitives.** 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, so this is CPU cost only -- but a one-leaf
repaint still uploads 5,863 instances where the pre-#16 code uploaded 106.
## The random trees ## The random trees
`iris::random` grows a seeded tree -- spans in every direction holding two to `iris::random` grows a seeded tree -- spans in every direction holding two to
four children, stacks, padding with each of its four sides its own number, four children, stacks, scrolls on either axis, padding with each of its four
sides its own number,
rects with varying opacity, text both wrapping and overflowing, a declared size rects with varying opacity, text both wrapping and overflowing, a declared size
over half of it, stopping at a depth. `examples/random.rs` draws one over half of it, stopping at a depth. `examples/random.rs` draws one
(`IRIS_SEED`, `IRIS_DEPTH`). `tests/generated.rs` grows each seed twice -- once (`IRIS_SEED`, `IRIS_DEPTH`). `tests/generated.rs` grows each seed twice -- once