Record the extent-child and ordered-walk landings, and where the gap really is

The placement pin was not most of it. Deleting the read prices no dependency
at all; expressing it properly is `e6ba570`, worth 19% of seed 13's `many`
and 20% of `resize`. What refuses most reuses is the extent contract -- 819
of 1,203 refusals a frame -- and that is the measuring ask and the placing
ask being two different extents, which `Span` is the remaining reader of.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-17 19:33:33 -04:00
1 parent 384b6a1150
commit 12b7364998
1 file changed
+79 -24
+79 -24
View File
@@ -22,9 +22,10 @@ Its current head is `0e107f0` in `/home/bob/repos/iris-layout-experiment`.
See **Frame and extent: retained prototype** for the mechanism, measured costs, See **Frame and extent: retained prototype** for the mechanism, measured costs,
and the remaining cold/many-update work. The app's framework pin is unchanged. and the remaining cold/many-update work. The app's framework pin is unchanged.
**It is not ready to replace #18 yet**: measured against `e44dea3` it wins on **It is not ready to replace #18 yet**: measured against `e44dea3` it wins on
`size` and `scroll` and loses badly on `many` -- 6x on seed 13 at depth 8 -- `size` and `scroll` and loses badly on `many` -- 4.5x on seed 13 at depth 8
and the cause is one conservative dependency rather than the semantics. See after the 2026-09-17 work -- and what is left of that is the measure/place
**Measured against #18's head, and the placement pin behind the gap**. double draw rather than a conservative read. See **Measured against #18's
head, and the placement pin behind the gap** and the two sections after it.
**Widen one fuzzer axis at a time, and record which.** Seeds **1121** and **Widen one fuzzer axis at a time, and record which.** Seeds **1121** and
**1839** at **depth 4** failed on `ea6dbae` and on every commit before it, **1839** at **depth 4** failed on `ea6dbae` and on every commit before it,
@@ -707,31 +708,85 @@ the dependency:
| seed 13, depth 8 | 1.088 / 524 | 6.351 / 2380 | 0.147 / 84 | | seed 13, depth 8 | 1.088 / 524 | 6.351 / 2380 | 0.147 / 84 |
`cold` at seed 1 falls from 484 draws to 280 and 12.5 ms to 11.1 ms with it `cold` at seed 1 falls from 484 draws to 280 and 12.5 ms to 11.1 ms with it
gone, and `resize` from 48 us to 12 us. So the pin is not a cost the frame and gone, and `resize` from 48 us to 12 us. Read it as an upper bound on what
extent semantics require: it is `Pad` and `Stack` having no way to say "inside `Pad` and `Stack` having no way to say "inside my extent" was costing, not as
my extent" other than reading the raw box. what removing the read properly would buy -- see below for the difference.
**That is what `c44bd19` was, and the 1.8% that got it reverted was measured **That is what `c44bd19` was, and the 1.8% that got it reverted was measured
before the dependency split.** Extending `widget_within` to accept before the dependency split.** It is now landed as `e6ba570`, below, and the
`DrawRegion::Extent` was priced against a head where one accumulated range bound above turned out to overstate it: deleting the read also deletes
still forced the redraws, so the saving had nowhere to show. Re-price it invalidation that the extent semantics genuinely require, so it prices "no
against `0e107f0` before the experiment replaces #18; the enlarged child dependency at all" rather than "the dependency expressed properly".
record it costs is 16 bytes against a phase that is presently 6x #18's on
seed 13.
**A smaller one, unrelated and free.** `redraw_updates` picks the deepest ### Landed on 2026-09-17: extent children, and an ordered walk
dirty widget by scanning the whole `needs_redraw` set and calling `depth` on
every member, once per pop, and `depth` is a hash lookup. Depth reads per
`many` frame at seed 1, depth 8: 131 at `IRIS_DIRTY=8`, 1,314 at 33, and
14,611 at 145, for 21, 62 and 182 pops. It is about 3% of a 33-dirty frame
(`depth` plus the hashbrown fold in `perf report`) and grows as the square of
the update set. Ordering the pops rather than re-scanning needs no retained
state.
Verified green at `0e107f0` on 2026-09-17 before any of the above: Three commits on `wip/region-and-placement`, pushed, on top of `0e107f0`.
`cargo fmt --all --check`, `cargo clippy --workspace --all-targets -D
warnings`, and `cargo test --workspace` (106 suite, 20 core, 11 generated, **`e6ba570`, a child gets a part of the container's extent.**
4 harness) all clean. `widget_within` takes a `DrawRegion`, and `DrawRegion::Extent(part)` gives
the child a part of the extent without reading it. What is retained is the
part rather than the box it resolved to, so moving the extent re-places the
child through the same rule: `inherited_children` became `extent_children`,
carrying `Inherit` for the wrapper case `Painter::widget` already had and
`Within(part)` for the new one. `Pad` and `Stack` use it and no longer read
`placement()`. The dependency that goes up is a range on the container's
*extent*, since only the part's length reaches the child. A declared length
is unchanged -- it is a length of the frame wherever its box came from.
What still pins the placement is a report with a fraction in it, and that
pin is on the answer rather than the drawing; the test from the first
attempt fails without it.
**`3bf2293` and `34cafb6`, the walk takes the deepest mark from a
`BTreeSet` keyed by depth** rather than `max_by_key` over the whole set.
Every mark made while the walk runs goes through `mark`, which queues
itself; the set is still what says the walk is done, so a mark that arrived
another way cannot be left for the next frame. Depth reads per `many` frame
at seed 1 depth 8: 131/1,314/14,611 at 9/34/145 marks become 57/160/436.
What is drawn does not change at any load measured. Ties between equal
depths now break by widget id, which makes the walk deterministic.
Cycles, medians of seven alternating runs, `perf stat -e cycles:u`, against
both the branch #18 would merge and the experiment as it stood:
| phase | `e44dea3` | `0e107f0` | head | vs `0e107f0` |
| --- | ---: | ---: | ---: | ---: |
| `many`, seed 1 | 1.273 B | 2.543 B | 2.502 B | -1.6% |
| `many`, seed 13 | 1.323 B | 7.308 B | 5.931 B | -18.8% |
| `resize` | 0.269 B | 0.485 B | 0.389 B | -19.9% |
| `size` | 0.261 B | 0.205 B | 0.204 B | -0.2% |
| `scroll` | 1.730 B | 0.912 B | 0.939 B | +2.9% |
Verified at each commit: fmt, clippy with `-D warnings`, 109 suite and 20
core tests, the oracle at 100 seeds, the shrinker at 400 trees of depth 5,
1000 seeds at depth 6 and 2000 at depth 4 over all fifteen cases, and the
five reference renders plus `tabs` resized to 900x1200 and `random` to
1280x800, all byte-identical on Venus.
### What is left of the gap, and it is not the placement pin
**The extent contract is what refuses most reuses.** The diagnostics rig
now splits a failed reuse three ways. At seed 13, depth 8, `many`, of 1,203
refusals a frame: **819 fail an extent range**, 379 a frame range and 310
the placement pin (they overlap). Seed 1: 55, 20 and 37 of 98.
The point ranges are the tell -- `Holds { lo: 20, hi: 20 }`, `{ lo: 1916,
hi: 1916 }`. `Painter::px_len` narrows the extent to exactly the length
read, and the measuring ask and the placing ask are two different extents:
a widget that reads its pixel width while being measured cannot have that
drawing reused where it is finally placed. Wrapping text and `Scroll` both
do. That is the measure/place double draw, and it is why the experiment
draws 508 distinct widgets of 583 active where `e44dea3` draws 159.
**`Span` is the one raw placement read left that costs anything.** Replacing
`painter.placement()` with a non-recording read -- unsound, a bound only --
takes seed 13's `many` from 4.73 ms to 3.03 ms; doing the same to `Scroll`
moves nothing. A span cannot use an extent ask as it stands: it hands each
child the *frame* along the axis (so `rel(0.5)` is half the row) and a slot
cut from its own *extent*, and the slot is accumulated from lengths that are
fractions of the frame. Expressing those slots in extent coordinates needs
the frame-to-extent ratio, which is the inverse division this branch
removed. **Ask Bryan before designing that**; it is the shape of the
protocol, not a cleanup.
## How layout is decided ## How layout is decided