diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index e9661e9..8f5abef 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -422,8 +422,9 @@ back. The original proposal was implemented by Claude as `5fcace1` on `wip/region-and-placement` in `/home/bob/repos/iris-pr18`. It kept the fraction reference stable but failed five layout cases and three draw-count cases. -The first correction is `efb416b`; the exact-recomposition continuation is -`2ed5503`, pushed to origin's `wip/region-and-placement`, in the isolated checkout +The first correction is `efb416b`, exact recomposition is `2ed5503`, and the +current performance/alignment continuation is **`7601aa2`**, pushed to origin's +`wip/region-and-placement`, in the isolated checkout `/home/bob/repos/iris-layout-experiment`; **it is not on PR #18**. The original checkout is unchanged. Keep the experiment's build directory separate: Cargo can accept a different worktree's artifacts as fresh when a target directory @@ -436,7 +437,7 @@ report must not replace the reference against which that report was obtained. Text wraps at the available extent, not the whole fraction reference. **The offer includes availability.** A span can keep the same reference frame -while offering a text less room after an earlier sibling. `known_len` therefore +while offering a text less room after an earlier sibling. `measure_len` therefore accepts the placement as well as the frame. The first measurement's `offer_placement` survives later placing evaluations, and local redraw asks that original question before restoring the assigned slot. Frame-length @@ -484,52 +485,96 @@ at arbitrarily wider widths. New tests compare actual primitive and mask geometry and assert reuse for fixed-frame resizing and widening unwrapped text (including explicit newlines and empty content). +**Measure the offer without placing an intermediate answer.** `Painter::measure_len` +replaces the hint/cache query and its duplicated drawing fallback. It still runs +`Widget::draw` when needed, but leaves that drawing in the offer until the parent +assigns the child's slot. Placing the child's own answer first was wasted work: +the parent immediately replaced that placement. The regression counts three +rather than four evaluations for a numeric-size leaf in a span, checks its actual +primitive bounds, and repeats after resize. There is no second layout body on +`Widget`. `Holds::ANY.through(...)` now returns `ANY` directly: an unrestricted +range needs no inverse division, including for a negative fraction. + +**A chosen slot does not cancel a child's alignment.** Bryan caught this in the +`tabs` image: a fixed 100 px square under a flexible wrapper sat at the slot's +near edge. `ask_box` now aligns a declared frame inside the chosen slot, just as +it does inside an unassigned offer. A regression asserts the actual coordinates +before and after resize. Warm/cold equality could not find this because both +were wrong. The corrected 900 px image still has 88 px between blue and red and +78 px between red and orange: the enclosing `.pad(10)` adds 10 px before the +red slot. Red is centered in that slot, 5 px right of the visible black gap's +center. Do not describe those two centers as the same thing or compensate with +an arbitrary offset. The parked padding API change is still separate. + Ordinary verification passes: formatting, workspace clippy with -`layout-diagnostics`, workspace tests, and the fast generated cases. The -suite contains 98 tests. A `tabs` resize from 1920x1200 -to 900x1200 matches a cold 900x1200 screenshot with zero differing pixels. -With exact equality, the final release oracle passes 2000 seeds at depth 4 -and 1000 seeds at depth 6, the shrinker passes 400 seeds at depth 5, and the -debug oracle passes 120 seeds at depth 4 with assertions enabled. Every scan -runs all fifteen scenarios. The fixed-frame reuse regression was also run -against `efb416b`: it fails there with two child draws instead of one. +`layout-diagnostics`, workspace tests (100 suite tests and 21 core tests), and +the fast generated cases. The corrected `tabs` resize from 1920x1200 to +900x1200 matches a cold image with zero differing pixels. The release oracle +passes 2000 seeds at depth 4 and 1000 at depth 6; the shrinker passes 400 at +depth 5, and the debug oracle passes 120 at depth 4 with assertions enabled. +Every scan runs all fifteen scenarios with exact equality. The measurement +regression fails with four draws instead of three when the optimization is +disabled. -**Do not land this as a performance improvement.** The old stretching and -wrapper reuse tests pass, and one numeric-size test improves from three draws -to two. The continuation reduces widget draws from `efb416b` by 6.9% cold, -9.0% with many dirty widgets, 7.6% for a size change, and 8.7% for resize. -Against the last working `e44dea3`, it still does substantially more nested -layout work (seed 1, depth 8): +**Still not ready to replace PR #18.** The prototype does substantially more +nested layout work than `e44dea3`, even after removing intermediate placements +(seed 1, depth 8): -| phase | widget draws before | prototype | primitive writes before | prototype | +| phase | widget draws at e44dea3 | current | primitive writes at e44dea3 | current | | --- | ---: | ---: | ---: | ---: | -| cold | 369 | 498 | 9635 | 9704 | +| cold | 369 | 484 | 9635 | 9179 | | repaint | 1 | 1 | 1 | 1 | -| many | 157 | 282 | 4721 | 6398 | -| size | 16 | 254 | 106 | 5725 | +| many | 157 | 274 | 4721 | 5873 | +| size | 16 | 247 | 106 | 5200 | | scroll | 2 | 2 | 0 | 0 | -| resize | 13 | 300 | 0 | 6255 | +| resize | 13 | 292 | 0 | 5730 | -These are instrumented work counts, not elapsed-time speedups (50 frames per -phase except cold, which is one; 578 generated widgets, 191 finally active). -Uninstrumented release binaries, run directly with `perf stat -r 3 -e -instructions:u`, also improve over `efb416b`: 2000 size-change frames fall -from 14.3923B to 13.8914B instructions (3.5%); 2000 resize frames fall from -16.9756B to 16.1331B (5.0%). These whole-process totals include fixture setup; -reported run-to-run variation is at most 0.24%. The many-dirty workload falls -from 16.7962B to 16.0030B instructions (4.7%, 2000 frames). Repaint and scroll -keep their draw counts but incur small CPU increases: 0.8849B to 0.8913B -(+0.7%) and 2.4332B to 2.4479B (+0.6%) over 100,000 frames each. These are -CPU-work measurements, not phone frame times. Keeping measurement validity separate from final drawing validity -and deferring to an already dirty parent were also tried; neither improved -this rig's draw counts, so those changes were removed. +These are instrumented work counts, not speedups (50 frames per phase except +cold, which is one; 578 generated widgets, 191 finally active). Against +`2ed5503`, the changes remove 7 widget draws and 525 primitive writes per size +frame, and 8 draws and 525 writes per resize frame. -Raw placement reads in containers and repeated offer/placement evaluations -remain conservative. Retaining local coordinates also costs memory per -primitive. The experiment demonstrates selective reuse with one `Widget::draw` -body, not a finished performance replacement. Optimize the container protocol -under the same correctness tests before adopting it. The parked `Inset`/`Outset` -API changes are not included in this experiment. +**Measure cycles as well as instructions** (Bryan, 2026-09-17). The following +are whole-process medians from seven alternating before/after runs of direct, +uninstrumented release test executables under `perf stat -e +cycles:u,instructions:u`. The baseline is `39f7b08`: `2ed5503` with the same +centering fix, so both sides draw the corrected layout. CPU-heavy scans were stopped +during measurement. Counts include fixture setup; size, resize and many run +5000 frames, repaint and scroll run 1,000,000. These are VM CPU measurements, +not phone frame times. + +| phase | cycles before → after (billions) | instructions before → after (billions) | +| --- | ---: | ---: | +| size | 12.8008 → 11.0344 (-13.8%) | 34.3660 → 29.9450 (-12.9%) | +| resize | 14.9368 → 13.0376 (-12.7%) | 39.8487 → 35.0032 (-12.2%) | +| many | 14.8875 → 12.9579 (-13.0%) | 39.6021 → 34.7499 (-12.3%) | +| repaint | 2.7416 → 2.6626 (-2.9%) | 6.0266 → 6.0197 (-0.1%) | +| scroll | 9.7848 → 8.4829 (-13.3%) | 21.7203 → 20.9527 (-3.5%) | + +The size/resize/many cycle samples have nonoverlapping before/after ranges, +with each range less than 1.3% of its median. Additional depth-6 fixtures, +10,000 frames and seven alternating pairs each: seed 3 improves cycles by +7.0% for size and 8.4% for resize; seed 13 improves resize by 9.2%, while its +size samples overlap and establish no cycle improvement. + +**The remaining gap has not been established as an unavoidable correctness +cost.** A temporary failure trace of one frame on seed 1, depth 8 counted +39/108 failed reuse attempts in size, 65/151 in resize, and 45/109 in many +where only the raw placement snapshot differed: both numeric frame and extent +ranges still held. Containers read that snapshot to construct child positions, +so discarding the check would be wrong; a way to retain those child positions +relative to the extent is the next protocol question. There is also necessary +width-dependent work: the hot text at widget 199 visits 68 px and 89.53613 px +and reports different heights (316.80078 and 228.80078). It is evaluated +repeatedly at those widths, so that fact alone does not justify all the repeats. + +Keeping measurement validity separate from final drawing validity and deferring +to an already dirty parent were previously tried without improving draw counts. +Measuring first in local redraw and then restoring its slot saved only one draw +in `many` and added another request on simple repaints; that trial was removed. +Retaining local coordinates still costs memory per primitive. Optimize under +the exact oracle before adopting the prototype; the `Inset`/`Outset` changes +remain parked. ## How layout is decided diff --git a/docs/PLAN.md b/docs/PLAN.md index 6d78b38..f1811c6 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -876,7 +876,10 @@ so reuse composes positions in the same order as a fresh draw instead of recovering fractions by division. The experimental oracle requires exact pixel-region equality. `Widget::draw` remains the only layout body. The prototype has significant container-update costs and is not yet adopted into -the pinned framework. +the pinned framework. A container's length query can evaluate the offered extent +without first placing the child's reported size; the parent's subsequent draw +assigns the final slot using the same `Widget::draw` body. A parent-selected slot +still honors a fixed child's alignment within that slot. **A `Span` leaves its children in the offered orthogonal region unless explicitly compacted** (2026-09-12). It still reports the widest orthogonal