Record retained layout optimization results

This commit is contained in:
iris-ai committed 2026-09-14 15:58:10 -04:00
1 parent 38f6f9ac07
commit cb1cfff0bf
1 file changed
+53 -28
+53 -28
View File
@@ -9,8 +9,8 @@ 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 `cdec293`, thirteen commits, 50
tests passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised
worktree `/home/bob/repos/iris-pr18`, head `84f589e`, fifteen commits,
workspace 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.
**The one thing waiting on the owner.** `tabs` at 1920x1200 is no longer
@@ -137,30 +137,51 @@ not in the repository, because the counters it reads are patched into
**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:
`a640c6c` and `84f589e` remove most of the CPU work without weakening the
retained-layout rules. Against the same seed-1/depth-7 load, widget draws are
now 2,117 cold, 1 for a leaf repaint, 11 for a scroll, and 3,139 for a resize
(from 2,755, 1,313, 1,313, and 5,512 respectively). Workspace tests pass; the
five reference renders, resize render, and image-tab replay are byte-identical
to the prior #18 head.
The generated tree now includes `Aligned` with every meaningful per-axis
alignment. That exposed two retained-layout ordering bugs which `84f589e`
fixes. The regular cold-layout equivalence suite passes. The ignored 100-seed
sweep passed every transition through seed 59 and now gets past the former
scroll failure at seed 52; at seed 60 it reaches the already-documented
iterative wrapping-text defect, differing by 0.00003 px after `SwapForThree`.
The branches are invariants rather than widget exceptions:
- A dirty widget draws locally first only while its retained box has the same
pixel size. If its returned `Size` is unchanged, no size reader can observe
the repaint and no ancestor draws. If the size changed, the dependent path
lays out. A changed pixel box takes the conservative path first, which is the
condition the discarded `/tmp/escalate.patch` missed on resize.
- Dirty widgets settle deepest-first. A changed size queues only its immediate
reader; propagation stops as soon as a reader's own answer stays unchanged.
During an output resize, the resize condition stays live until these updates
finish, so an `Aligned` ancestor chooses the new child box before a
pixel-dependent descendant draws in it.
- A span measures an unknown child in the part of its axis still available,
rather than giving every child the whole container and immediately taking
most of it away. This is the archive's faster shape, recreated on the current
types; it often makes the measurement box the final box without caching an
answer under different constraints.
A proposed `Scroll` shortcut that reused its direct child's retained size was
discarded. “Direct child is clean” is not strong enough while a dirty
descendant's structural change is still propagating; seed 52 demonstrated the
stale-size failure. Re-measuring the scroll subtree costs 11 draws rather than
1, but remains two orders of magnitude below the old 1,313 and follows the
actual dependency invariant.
The remaining costs are:
- **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.
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. This is now the largest CPU layout cost.
- **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
@@ -169,8 +190,8 @@ Three separate costs, in the order they are worth fixing:
## The random trees
`iris::random` grows a seeded tree -- spans in every direction holding two to
four children, stacks, scrolls on either axis, padding with each of its four
sides its own number,
four children, stacks, scrolls on either axis, alignment 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
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
@@ -211,7 +232,7 @@ cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
```
49 tests pass on #18's head. `--workspace` matters: `rig-input` is a crate of
50 tests pass on #18's head. `--workspace` matters: `rig-input` is a crate of
its own.
Render checks are the last pass, not the iteration loop -- the owner asked for
@@ -329,7 +350,11 @@ Then, roughly in dependency order:
centred content, which otherwise has to say `Redraw` because only its own
draw knows where the middle was. Size is the harder half: a declared size
beside the one `draw` returns is two sources of truth for one thing, so
settle what each means before building it.
settle what each means before building it. This is independent of #18's
retained-update fix now that `Aligned` is in its generated coverage; land
#18 first, then design built-in alignment and size together as the next
structural slice rather than mixing that representation change into this
performance correction.
- **`OnResize::Translate`, which still does nothing.** The chain removed half
its obstacle: a placed widget's slot holds the box it was offered while its
drawing is a set of fractions of that box, so the two are no longer one