Price the frame/extent experiment against #18's head, and name the placement pin

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-17 18:02:02 -04:00
1 parent ccc42f34f8
commit 384b6a1150
1 file changed
+95
+95
View File
@@ -21,6 +21,10 @@ redraw cascade through dependency tracking and invalidation, not glyph emission.
Its current head is `0e107f0` in `/home/bob/repos/iris-layout-experiment`.
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.
**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 --
and the cause is one conservative dependency rather than the semantics. See
**Measured against #18's head, and the placement pin behind the gap**.
**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,
@@ -638,6 +642,97 @@ instructions and no cycles; special-casing equal endpoint fractions in
still cost memory per primitive. The prototype remains separate from PR #18;
the `Inset`/`Outset` changes remain parked.
### Measured against #18's head, and the placement pin behind the gap
Checked on 2026-09-17 in `/home/bob/repos/iris-layout-baseline` (`e44dea3`,
its own `target-own` because its `target` is a symlink into
`/home/bob/repos/iris-pr18`) against `/home/bob/repos/iris-layout-experiment`
(`0e107f0`). **The five-phase cycles table above compares `c44bd19` with
`0e107f0` -- two commits inside the experiment -- so it says what the last two
fixes bought, not what the experiment costs against the branch it would
replace.** Against `e44dea3` the picture is different, and it decides whether
this can replace #18.
Nine alternating pairs under `perf stat`, seed 1 at depth 8, uninstrumented
release executables:
| phase | cycles `e44dea3` -> `0e107f0` | instructions |
| --- | ---: | ---: |
| size (2,000) | 0.2618 -> 0.2026 B (-22.6%) | -21.3% |
| scroll (300,000) | 2.5169 -> 1.2796 B (-49.2%) | -49.5% |
| repaint (300,000) | 0.8546 -> 0.9533 B (+11.5%) | +15.0% |
| resize (2,000) | 0.2697 -> 0.4813 B (+78.5%) | +99.0% |
| many (2,000) | 2.3885 -> 4.9504 B (+107.3%) | +98.4% |
Each phase's before and after cycle ranges are disjoint. The work counters agree with the times: `size`
falls from 16 widget draws to 3 and `scroll` from 2 to 1, while `many` rises
from 157 to 274 and `resize` from 13 to 27. `many` is also the phase that
costs anything at all -- median frames at seed 1, depth 8 are 1 us for
repaint and scroll, 9 us for size, 48 us for resize and 667 us for `many`,
so a percentage on `many` is worth two orders of magnitude more than the same
percentage on repaint.
**The `many` regression varies enormously with the tree**, so one fixture
cannot settle it. Median frame, 200 frames, `IRIS_DIRTY` at its default:
| fixture | `e44dea3` | `0e107f0` | draws before -> after |
| --- | ---: | ---: | --- |
| seed 1, depth 8 | 0.318 ms | 0.667 ms | 157 -> 274 |
| seed 2, depth 8 | 0.218 ms | 0.700 ms | 111 -> 349 |
| seed 5, depth 8 | 0.759 ms | 0.702 ms | 337 -> 230 |
| seed 13, depth 8 | 1.088 ms | 6.351 ms | 524 -> 2380 |
| seed 3, depth 6 | 0.173 ms | 0.156 ms | 83 -> 51 |
| seed 13, depth 6 | 0.583 ms | 0.570 ms | 325 -> 268 |
**The cause is the raw placement read, and it is nearly all of it.**
`LayoutHolds::placement` is `Some(..)` for any widget that called
`Painter::placement`, and that pins the exact box the drawing sits in: the
widget is redrawn whenever it moves at all, however wide its frame and extent
ranges are. `Pad` and `Stack` both read it -- `self.padding.region_of(
painter.placement())` and `let placement = painter.placement()` -- so every
`Pad` and `Stack` in a row redraws its whole subtree when an earlier sibling
changes length. A counter for reuse failures where the frame and extent
ranges still hold and only the placement moved says it is 45 of the 109
failures a `many` frame at seed 1 has, 14 of 26 on `resize`, and 73 of 136
on `cold`.
Bounded by deleting the `reads_placement = true` line -- unsound, since
nothing then redraws a widget whose placement really did move, but it prices
the dependency:
| fixture, `many` | `e44dea3` | `0e107f0` | no placement pin |
| --- | ---: | ---: | ---: |
| seed 1, depth 8 | 0.318 ms / 157 draws | 0.667 / 274 | 0.127 / 78 |
| seed 2, depth 8 | 0.218 / 111 | 0.700 / 349 | 0.079 / 46 |
| 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
gone, and `resize` from 48 us to 12 us. So the pin is not a cost the frame and
extent semantics require: it is `Pad` and `Stack` having no way to say "inside
my extent" other than reading the raw box.
**That is what `c44bd19` was, and the 1.8% that got it reverted was measured
before the dependency split.** Extending `widget_within` to accept
`DrawRegion::Extent` was priced against a head where one accumulated range
still forced the redraws, so the saving had nowhere to show. Re-price it
against `0e107f0` before the experiment replaces #18; the enlarged child
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
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:
`cargo fmt --all --check`, `cargo clippy --workspace --all-targets -D
warnings`, and `cargo test --workspace` (106 suite, 20 core, 11 generated,
4 harness) all clean.
## How layout is decided
### Fixed point