Files
ai-app/docs/bench/iris-phone-v2-2026-09-06.md
T
irisandClaude Fable 5.1 fb6b459c2c iris: Scroll pans on a finger drag; a vertical drag in a focused field scrolls rather than selects
IRIS_TODO.md's "the composer has no touch-drag scroll". `Scroll::drag`
takes its pan from the same `sense::DragGesture` `List` is driven by --
arbitration, DRAG_SLOP, velocity and pointer capture all stay in sense.rs
and only what a committed pan *means* is decided per caller -- and
`WidgetLike::scrollable()` registers it beside the wheel handler it already
registered, so every scroll area pans on a finger with nothing added at the
call site. No fling: `Scroll` has no per-frame tick to animate one and the
areas it wraps are at most a screenful. `Scroll::amt()` exposes the pan
position.

`attr.rs`'s `on_press` treated an already-focused field as the plain
click_or_drag case, so every Pressing frame extended a selection. It now
applies the same DRAG_SLOP rule its unfocused branch already did: a press
past the slop vertically abandons its pending selection for the rest of the
gesture, so the scroll area around the field wins it. That is Android
EditText's own behaviour and it is what lets a swipe up over the composer
scroll instead of dragging a highlight through what you typed.

Also fixed, found doing it: `ActiveData::mask` stored the mask a widget
*set* rather than the one it was drawn *under*, and `redraw` feeds that
field back in as the inherited mask -- so a targeted redraw of any `Masked`
handed it its own mask and aborted on `set_mask`'s nested-mask assert. A
real abort on the emulator, `assertion failed: self.mask == MaskIdx::NONE`.

And the per-frame orphan guard from 76b1f99 is now a count comparison
(O(active widgets)); the O(primitives) walk only runs to build the failure
message, because running it per frame made a debug build on the emulator too
slow to finish a bench run at all.

Tests: four in scroll.rs (pan past the slop, a tap inside it, a horizontal
drag, the end clamp), `a_finger_drag_over_a_scroll_area_pans_it` in
sense_tests.rs driving the whole registration/dispatch/capture path (fails
with "got 0" without the new registration), and
`redrawing_a_masked_widget_does_not_nest_its_own_mask` in layout_tests.rs
(aborts on the pre-fix code).

The composer itself is deliberately still not `.scrollable()`: `Scroll`
measures against the window rather than its own offered box, so inside the
`MaxSize` capping it at six lines it pans the field out of the bar --
measured, reverted and written down in RUST.md and DECISIONS.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 16:45:56 -04:00

2.8 KiB

iris bench v2 report from Iris's phone, 2026-09-06

Build 2e3f4ad (bench v2, fling physics, keyboard-wipe fix, dp unit), run by Iris on her Pixel 9 Pro XL, verbatim. The display was at 120 Hz (8.3 ms budget) where compose-phone-v2-2026-09-06.md ran at 60 Hz, so compare the millisecond percentiles, not late.

Side by side (Compose 60 Hz / iris 120 Hz, p50 / p90 / p99 ms): fling 5.5/8.7/11.6 vs 3.8/6.9/12.6; stream 13.4/31.7/42.5 vs 18.2/35.8/43.1; type 7.3/13.2/16.5 vs 7.2/9.2/11.2; keyboard: iris could not show the IME (phase invalid). Process CPU 69.6 s over 125 s vs 40.6 s over 150 s; peak RSS 577 MB vs 379 MB; battery current mean 571 mA vs 452 mA.

Iris's observations on the same run: "the scrolling is not similar at all. It does not fling for me yet [with a finger], and the test also seems to give it a constant velocity and abruptly stop it at some point. Also unsure what's going on in that image with the compaction" -- her screenshot shows the Compacted: 180000 -> 20000 tokens. row drawn twice overlapping, and once more below the composer bar: primitives of a replaced/removed row surviving in the GPU buffers, the same shape as the header drawn twice after a keyboard resize.

Root-caused and fixed 2026-09-06 (commit 76b1f99): the diagnosis in that sentence was right and the location was not -- UiRenderState:: draw_inner read the needs_redraw mark without consuming it and skipped the branch that frees a redrawn widget's old primitives. docs/RUST.md's "Stale primitives, the phone's half" box has the full account, the guard (orphaned_primitives, debug_asserted every frame) and the emulator run that exercises it.

iris bench report
per phase:
  fling: 1783 frames over 53.2s
    late: 104 (5.8%)
      total  p50 3.8ms  p90 6.9ms  p99 12.6ms
    worst 29.1ms
  stream: 401 frames over 21.3s
    late: 306 (76.3%)
      total  p50 18.2ms  p90 35.8ms  p99 43.1ms
    worst 43.8ms
  type: 1202 frames over 65.7s
    late: 309 (25.7%)
      total  p50 7.2ms  p90 9.2ms  p99 11.2ms
    worst 15.3ms
  keyboard: 9 frames over 9.7s
    late: 9 (100.0%)
      total  p50 12.0ms  p90 12.9ms  p99 12.9ms
    worst 12.9ms

frames:
  3395 frames over 149.9s at 120Hz (8.3ms budget)
  late: 728 (21.4%)
  total  p50 5.0ms  p90 10.9ms  p99 36.6ms
  worst 43.8ms
  cpu_p50 2.0ms  gpu_wait_p50 2.6ms

bench:
  fling: 8 flings out + 8 back at 12000px/s, travel start=idx=651/off=1217px outward=idx=651/off=101536px end=idx=651/off=1022px
  scroll: 6 cycles (24 swipes, legacy tween), streamed 400/400 fixture events
  type: 600 characters inserted then deleted, one per 50ms
  keyboard: could not be shown (5 attempts, 0 confirmed visible)
  process CPU time over this run: 40603ms
  peak RSS: 379156kB
  battery current: mean -452353µA over 149 samples (min -1753125, max -204687)