Record PR 19 performance sweep and request invalidation fix

This commit is contained in:
iris-ai committed 2026-09-20 17:32:55 -04:00
1 parent 4c7072e62f
commit 048738bb9d
3 files changed
+137 -5

No files matched your search

+92
View File
@@ -6,6 +6,98 @@ nothing here is rediscovered. Each entry says who found it and when.
it (settled design, the measurement method) belongs in `docs/LAYOUT.md`, and
the current plan is in `docs/HANDOFF.md`.
## Performance sweep of #19 (2026-09-20)
`0e838e9`, by Codex.
The 5.89% text-resize increase at `8780b40` was real, but was not extra
shaping: `revision_cost` at both `de1eb7e` and `8780b40` drew 168 widgets,
rendered seven texts and broke seven layouts per resize frame. Sampling the
latter put most CPU work in relocating retained primitives. This is a CPU
layout/recording fixture, not a GPU text-rendering benchmark.
**Discarded discovery invalidated things it did not decide.** Both
`Painter::size_request` and `measured_request` kept dependencies and pinned
the rel base even when they discarded the discovered answer and used the
ordinary measured size. Repainting one unchanged paragraph consequently
redrew its enclosing span and remapped the other 39 paragraphs. Discovery now
keeps those dependencies only when its answer is used; the measured path
already records the size it reads. Used hints still register request readers:
adding a cap to a previously unbounded hinted share must reach its allocator.
The same edit is checked for an unhinted share too. `Widgets::size_rules` now
borrows its rules instead of cloning both axes at every lookup; callers that
need an owned snapshot clone explicitly.
All instruction measurements below are medians of nine executable runs with
`perf stat -e instructions:u`, release builds without diagnostics. Every set
kept all nine readings within 2% of its median. Totals include initialization
and the cold frame. Resize uses 40 rows and 1,000 frames; the other phases use
40 paragraphs in a scroll and 2,000 frames. Edit alternates a short suffix,
rather than growing the workload every frame. `SWEEP=1` cycles 256 widths;
the default toggles two widths. Billion retired instructions:
| Workload | Before deferred requests (`de1eb7e`) | Incoming (`8780b40`) | After this fix | After vs before deferred |
| --- | ---: | ---: | ---: | ---: |
| Text resize, two widths | 9.340 | 9.890 | 9.432 | +0.99% |
| Text resize, width sweep | 7.044 | 7.478 | 7.131 | +1.23% |
| Unchanged paragraph repaint | 0.376 | 3.010 | 0.370 | -1.58% |
| Paragraph edit | 3.485 | 3.607 | 3.492 | +0.22% |
| Scroll | 0.428 | 0.430 | 0.428 | +0.10% |
| Idle | 0.306 | 0.306 | 0.306 | +0.04% |
The repaint regression was eightfold in these process totals; diagnostics
now show one text draw and no span draw. The remaining roughly 1% resize
cost is discovery around content that still needs measurement. It is not
necessary to weaken wrapping, overflow or bounds correctness to recover the
regression. Differences below about half a percent should not be ranked:
rebuilding changes instruction counts too.
The full PR is substantially cheaper than its upstream base `ca2b4b2` in
these fixtures: that base takes 124.072B instructions for the two-width resize,
13.516B for repaint, 14.155B for edit and 13.517B for scroll. Its geometry also
differs (paragraphs run past the 900-pixel output), so this is a whole-version
comparison, not equal-work evidence for a single optimization. The fixture
was identical apart from spelling the fixed width `Len::abs(40.0)` there and
omitting unavailable diagnostic calls.
**The generated trees are not all faster.** At depth 8, 1,000 frames, seeds
1 and 13, the incoming branch's bounded resize workloads take 0.561B and
4.297B instructions; the fix takes 0.539B and 4.016B. The pre-deferred branch
uses 0.359B and 1.898B, but does different work: seed 13 draws 67 widgets per
resize there and 170 with deferred requests, and even the active cold tree
changes. Those are not equal-work comparisons. Removing intrinsic bounds
with `IRIS_UNBOUNDED=1` isolates the ordinary path: seed 1 resize is 0.359B
before vs 0.363B after, and seed 13 is 1.897B vs 1.296B. Unbounded seed 13's
multi-widget repaint still rises from 0.980B to 1.208B. A consumed request's
conservative invalidation remains a cost; eliminating it would require
checking whether the request changed before invalidating its allocator.
This sweep does not establish a universal speedup or make that cost inevitable.
**Storage and limits.** The forced-redraw plain and clamped allocation rigs
still allocate zero times after warm-up. A single 2,000-row stress run
(10,001 widgets, all retained) settles at 133,400 KiB RSS before deferred
requests, 141,912 KiB incoming and 138,820 KiB after this fix. These process
RSS snapshots show the retained bookkeeping cost, not a per-widget allocation
size or a leak. The spare glyph store still copies text when keeping a prior
width; its 128-entry bound does not bound bytes. The allocator visits expression
nodes at each crossing; many distinct caps can require quadratic work. Neither
case should be mistaken for the small fixed-width text-update workload above.
**GPU work is a separate tradeoff.** The first attempt detected RADV on the
RX 7900 XT but could not allocate its 64 MiB device buffer (`No space left on
device`). After allocation became available, both rigs ran successfully.
Across three timestamp runs of `chain_cost` (200,000 two-pixel rectangles),
depth 1 took 4878 µs, depth 4 took 7679 µs, depth 16 took 145150 µs, and
depth 64 took 430443 µs. The first run's depth-2 result was anomalous; the two
repeats agreed on approximately 78 µs for depths 14. Chain depth counts opted-in
movable regions, not every widget. Deep chains have a real GPU cost, but 200,000
small quads through 64 movable ancestors is a stress case, not a measurement
of a normal paragraph or a phone. The CPU recording-only `draw_cost` rig also
passed; its wall times under concurrent load are not used as regression evidence.
No renderer or shader behavior changed in this performance fix.
Verification is recorded in `HANDOFF.md`. The app's Iris pin is unchanged.
## One ask, the root's included (2026-09-20)
`0d03267`. `root_layout` read the root's declared lengths against the window