e65c961e1eea46d584a4e2509e1c58de300f5b45
The piece of a lazy list this had not rebuilt was the render node per item. Without one, a row's glyphs are recorded into its parent's display list, and that list is re-recorded every frame the parent is invalidated -- which, while the list is scrolling, is every frame. With one, the row is recorded once and afterwards moved by a transform, and the render thread culls the ones off screen itself. The draw-phase culling it replaces could not have won, and the two are mutually exclusive rather than complementary: `onScreen` and `RowWindow` read the scroll position from inside every row's and every block's drawing, and reading a scroll position during draw invalidates the drawing it is in. So the machinery for deciding which rows need not be drawn was re-recording all of them, every frame, to decide it. Keeping both would have bought the cost of the first and none of the second. Measured on the emulator over a thirty-swipe scroll: **one** row display list recorded across 481 frames, against 1,544 recordings for the same gesture before, and the frame's draw phase down from 4.2ms to 2.9ms at the median. Two things this also corrects. `LAYOUT_MEASURE_DURATION` is the *View* hierarchy's pass, and Compose is one view -- `AndroidComposeView. dispatchDraw` calls `measureAndLayout()` before it records, so all of Compose's own measurement, text shaping above all, is reported inside DRAW_DURATION. Every "layout is 0.0ms, so nothing is being re-measured" reading in this file's history was reading a bucket that never contained it. And the retain window is now load-bearing for a second reason: a retained display list costs memory, so bounding what is retained bounds that too. Found by the ai-app-2-6d session reading the render reports against this code; the diagnosis and the ordering are theirs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Languages
Rust
54%
Kotlin
43.6%
Shell
2.4%