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
|
The separate frame/extent experiment now addresses the dominant size/resize
|
||||||
redraw cascade through dependency tracking and invalidation, not glyph emission.
|
redraw cascade through dependency tracking and invalidation, not glyph emission.
|
||||||
Its current head is `0e107f0` in `/home/bob/repos/iris-layout-experiment`.
|
Its current head is `34cafb6` on `wip/region-and-placement` in
|
||||||
See **Frame and extent: retained prototype** for the mechanism, measured costs,
|
`/home/bob/repos/iris-layout-experiment`. See **Frame and extent: retained
|
||||||
and the remaining cold/many-update work. The app's framework pin is unchanged.
|
prototype** for the mechanism. The app's framework pin is unchanged. **It is
|
||||||
**It is not ready to replace #18 yet**: measured against `e44dea3` it wins on
|
not ready to replace #18 yet**: against `e44dea3` it wins on `size` and
|
||||||
`size` and `scroll` and loses badly on `many` -- 4.5x on seed 13 at depth 8
|
`scroll` and loses on `many` and `resize`. The cause of the `many` loss was
|
||||||
after the 2026-09-17 work -- and what is left of that is the measure/place
|
traced on 2026-09-17 and is not what the earlier sections say; read **Where
|
||||||
double draw rather than a conservative read. See **Measured against #18's
|
the `many` gap actually comes from** before anything else in this document
|
||||||
head, and the placement pin behind the gap** and the two sections after it.
|
about performance, and take the earlier sections' explanations as history.
|
||||||
|
|
||||||
**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,
|
||||||
@@ -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
|
five reference renders plus `tabs` resized to 900x1200 and `random` to
|
||||||
1280x800, all byte-identical on Venus.
|
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
|
The two sections above blame the placement pin and then the extent contract.
|
||||||
now splits a failed reuse three ways. At seed 13, depth 8, `many`, of 1,203
|
Neither is the cause. Disabling the pin (unsound, a bound) still redraws
|
||||||
refusals a frame: **819 fail an extent range**, 379 a frame range and 310
|
**487** distinct widgets a frame at seed 13 against `e44dea3`'s 159, and a
|
||||||
the placement pin (they overlap). Seed 1: 55, 20 and 37 of 98.
|
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,
|
`redraw` refuses a dirty widget whose given box is not as long as its offer
|
||||||
hi: 1916 }`. `Painter::px_len` narrows the extent to exactly the length
|
and marks its parent instead. Under this protocol that is nearly every
|
||||||
read, and the measuring ask and the placing ask are two different extents:
|
widget under a self-sized container. `Span` hands its children its own
|
||||||
a widget that reads its pixel width while being measured cannot have that
|
placement across itself as their *frame* (`UiRegion::from_axis(axis,
|
||||||
drawing reused where it is finally placed. Wrapping text and `Scroll` both
|
UiSpan::FULL, *own.axis(!axis))`), and that placement is `FULL` while the
|
||||||
do. That is the measure/place double draw, and it is why the experiment
|
span is being measured and its answer once it is placed, so a child's offer
|
||||||
draws 508 distinct widgets of 583 active where `e44dea3` draws 159.
|
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
|
**Re-asking locally at the offer** is the fix that follows, on branch
|
||||||
`painter.placement()` with a non-recording read -- unsound, a bound only --
|
`wip/local-reask` (one commit over `34cafb6`, pushed). `ActiveData` keeps
|
||||||
takes seed 13's `many` from 4.73 ms to 3.03 ms; doing the same to `Scroll`
|
`offer_region` beside `given_region`; `redraw` draws the widget once in the
|
||||||
moves nothing. A span cannot use an extent ask as it stands: it hands each
|
offer's frame at the offer's lengths and placement, and again at the given
|
||||||
child the *frame* along the axis (so `rel(0.5)` is half the row) and a slot
|
box where the two differ. Uninstrumented, 200 frames, seed and depth as
|
||||||
cut from its own *extent*, and the slot is accumulated from lengths that are
|
before, against `e44dea3`:
|
||||||
fractions of the frame. Expressing those slots in extent coordinates needs
|
|
||||||
the frame-to-extent ratio, which is the inverse division this branch
|
| phase | `e44dea3` | `34cafb6` | `wip/local-reask` |
|
||||||
removed. **Ask Bryan before designing that**; it is the shape of the
|
| --- | ---: | ---: | ---: |
|
||||||
protocol, not a cleanup.
|
| `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
|
## How layout is decided
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user