Trace the many-phase gap to local-redraw deferral, not the placement pin
The experiment's `many` loss against e44dea3 came from `redraw` refusing any widget whose given frame is not its offer and marking its parent instead, which under the frame/extent protocol chains to the root because a span gives its children its own placement across itself as their frame. Record the trace, the measured bound with the pin off, the local re-ask branch `wip/local-reask` and its two remaining depth-6 counterexamples, and point readers at this section before the earlier explanations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
12b7364998
commit
cb4557f1bc
1 file changed
+78
-30
+78
-30
@@ -18,14 +18,14 @@ present when checked on 2026-09-15.
|
||||
|
||||
The separate frame/extent experiment now addresses the dominant size/resize
|
||||
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` -- 4.5x on seed 13 at depth 8
|
||||
after the 2026-09-17 work -- and what is left of that is the measure/place
|
||||
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.
|
||||
Its current head is `34cafb6` on `wip/region-and-placement` in
|
||||
`/home/bob/repos/iris-layout-experiment`. See **Frame and extent: retained
|
||||
prototype** for the mechanism. The app's framework pin is unchanged. **It is
|
||||
not ready to replace #18 yet**: against `e44dea3` it wins on `size` and
|
||||
`scroll` and loses on `many` and `resize`. The cause of the `many` loss was
|
||||
traced on 2026-09-17 and is not what the earlier sections say; read **Where
|
||||
the `many` gap actually comes from** before anything else in this document
|
||||
about performance, and take the earlier sections' explanations as history.
|
||||
|
||||
**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,
|
||||
@@ -762,31 +762,79 @@ core tests, the oracle at 100 seeds, the shrinker at 400 trees of depth 5,
|
||||
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
|
||||
### Where the `many` gap actually comes from (2026-09-17, traced)
|
||||
|
||||
**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 two sections above blame the placement pin and then the extent contract.
|
||||
Neither is the cause. Disabling the pin (unsound, a bound) still redraws
|
||||
**487** distinct widgets a frame at seed 13 against `e44dea3`'s 159, and a
|
||||
per-widget trace of one `many` frame shows what does it: **local redraws
|
||||
defer to the parent, and the deferrals chain to the root.**
|
||||
|
||||
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.
|
||||
`redraw` refuses a dirty widget whose given box is not as long as its offer
|
||||
and marks its parent instead. Under this protocol that is nearly every
|
||||
widget under a self-sized container. `Span` hands its children its own
|
||||
placement across itself as their *frame* (`UiRegion::from_axis(axis,
|
||||
UiSpan::FULL, *own.axis(!axis))`), and that placement is `FULL` while the
|
||||
span is being measured and its answer once it is placed, so a child's offer
|
||||
frame across the span is the whole window and its given frame is the span's
|
||||
height or width. The trace has 43 deferrals in one frame, in chains such as
|
||||
424 → 425 → 437 → 449 → 483 → 487 → 491 with `given=(52, 40)
|
||||
offered=(1920, 40)` at every step. Each container reached that way runs its
|
||||
body at both placements, redrawing its subtree at two geometries.
|
||||
`e44dea3` defers under the same rule but its given and offer differ only by
|
||||
the widget's own alignment slack, so its chains stop after a level or two:
|
||||
19 deferrals, 159 distinct widgets.
|
||||
|
||||
**`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.
|
||||
**Re-asking locally at the offer** is the fix that follows, on branch
|
||||
`wip/local-reask` (one commit over `34cafb6`, pushed). `ActiveData` keeps
|
||||
`offer_region` beside `given_region`; `redraw` draws the widget once in the
|
||||
offer's frame at the offer's lengths and placement, and again at the given
|
||||
box where the two differ. Uninstrumented, 200 frames, seed and depth as
|
||||
before, against `e44dea3`:
|
||||
|
||||
| phase | `e44dea3` | `34cafb6` | `wip/local-reask` |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `many`, seed 1, depth 8 | 0.263 ms / 157 draws / 95 distinct | 0.549 / 263 / 108 | 0.499 / 239 / 93 |
|
||||
| `many`, seed 13, depth 8 | 0.896 / 524 / 159 | 4.727 / 1904 / 508 | 1.282 / 647 / 294 |
|
||||
| `size`, seed 1 | 0.016 / 16 draws | | 0.010 / 3 |
|
||||
| `size`, seed 13 | 0.094 | | 0.003 |
|
||||
| `resize`, seed 1 | 0.019 / 13 draws | 27 draws | 0.028 / 22 |
|
||||
| `scroll`, `repaint` | 1 us | | 1 us |
|
||||
|
||||
With the pin also disabled on that branch (bound), draws fall a further 15%
|
||||
and distinct widgets do not move. **What is left is the offer-then-place
|
||||
double pass**: a dirty container is drawn at its offer placement, where its
|
||||
children get the offer geometry, and again at its placed one, and any child
|
||||
whose `Holds` is a point (`Branch`, `Scroll`, wrapping text at a width it
|
||||
was not measured at) is drawn on both passes. That pass exists because the
|
||||
frame a span gives its children across itself is derived from the span's
|
||||
own answer, so it cannot be the same on both asks.
|
||||
|
||||
**The branch is not sound.** The suite, the debug oracle, 100 seeds and the
|
||||
shrinker at 400 trees of depth 5 pass; the oracle at 1000 seeds of depth 6
|
||||
diverges on two. Both reduce to a self-sized container whose answer changes
|
||||
under a local redraw:
|
||||
|
||||
- seed 532, `reorder`: `Pad{0} > Stack[ Span{RIGHT}[ Wrapped, Wrapped,
|
||||
Rect[x:178px, y:123px] ] ]`, rotating the span's children. Warm places
|
||||
a text at 947..1115 x 536..659, cold at 986..1076 x 483..712.
|
||||
- seed 398, `every-size`: `Span{LEFT}[ Rect, Rect, Stack[ Rect,
|
||||
Branch{ probe: Wrapped, wide: Rect, narrow: Span{DOWN}[ Rect[x:129px,
|
||||
y:leftover] ] } ] ]` with a size rule on the stack. Warm places a widget
|
||||
at 11..183, cold at 6.5..187.5 on x.
|
||||
|
||||
Reduce them with `SHRINK_SEED=<seed> SHRINK_DEPTH=6 SHRINK_CASE=<case>` on
|
||||
that branch before building on it.
|
||||
|
||||
**The cross-axis frame is the protocol question, not the pin.** Bryan's
|
||||
rule that a widget's frame does not change between the measuring and the
|
||||
placing ask is broken across every span, because the only sensible
|
||||
reference for a child's fraction across a span is the span's own box, and
|
||||
that box is the span's answer. `e44dea3` has the same double pass and pays
|
||||
for it by deferring to the parent; what it does not have is a frame that
|
||||
changes under a widget. Whether to keep frame and placement apart at all
|
||||
turns on this and on nested-span semantics; see the 2026-09-17 assessment
|
||||
handed to Bryan.
|
||||
|
||||
## How layout is decided
|
||||
|
||||
|
||||
Reference in new issue
Block a user