The frame report says what it measured: idle is not stutter, waiting is not late
Iris's phone came back "now THAT is smooth", and reading that run against the bench's own timings found three things the report was getting wrong -- two of them shipped yesterday in the fix for the last three. `missed vsyncs` counted idleness. Every gap between frames was treated as cadence, so the bench's own pauses read as stutter: 276 for sixteen 300ms rests between flings, 2410 for twelve hundred 50ms keystroke gaps, 821 for four hundred 50ms stream gaps -- each within a few percent of the arithmetic. A gap now measures anything only if the frame before it had asked for another one. `late` counted the swapchain wait as cost. A well-paced loop spends each frame blocked in the acquire, so its total sits at exactly one refresh period and every frame lands on the budget boundary -- 0.4ms of work and 5.7ms of waiting is not a late frame. It is judged on `FrameParts::work`. And the refresh rate is the larger of what the platform claims and what the run sustained, because each can only be wrong one way. `Display.getRefreshRate()` answered 60 for a run that drew 3405 frames in 33.1s, since a phone that varies its rate answers with whatever mode it is in when asked. The first attempt at measuring it instead took the fastest tenth of the gaps and reported 88Hz for this repo's 60Hz emulator, whose app manages 54 -- a budget no frame there could meet, invented out of the app's best moments, and caught only by running the corrected report on the emulator before shipping it. A sustained rate is a floor and cannot do that. Both are printed when they disagree. Also corrected in the docs: "103fps on a 120Hz screen" divided the fling phase by its whole duration, rests included. Both runs sustained ~120.3fps through the motion, so the callback ordering was never costing frames -- what changed is the clock, which moves no frame count at all, which is exactly why nothing in a report could show it. `fling_profile.rs` is `frame_profile.rs` and gained a stream run, which says where the frame time now is: folding an arriving event is 0.35ms and applying the diff 0.41ms, while the frame is 3.86ms here and 9.5ms on the phone. 401 events move the item count 652 -> 654, so nearly every one is a delta into the same row -- the cost is re-shaping one growing message, not `fold_event`'s per-event clone, which was the hypothesis and is what measuring it ruled out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42d54eec95
commit
9bf714fa2e
6 files changed
+457
-57
No files matched your search
+57
-1
@@ -472,11 +472,67 @@ the view keeps **one**, anchored by whichever of a touch or a frame
|
||||
arrives first, so a fling is advanced on the clock its velocity was
|
||||
measured on.
|
||||
|
||||
What the CPU side is *not*: `app-rust/tests/fling_profile.rs` (AGENTS.md's
|
||||
What the CPU side is *not*: `app-rust/tests/frame_profile.rs` (AGENTS.md's
|
||||
rig list) puts iris's own per-frame work during a warm fling at p99
|
||||
0.26ms, with only one frame in six laying anything out at all. The
|
||||
multi-millisecond spikes are first-pass only.
|
||||
|
||||
**The result, from Iris's phone the same day: "now THAT is smooth. I
|
||||
couldn't actually see any lag myself."** With three corrections to what
|
||||
the report meant, found by reading that run against the bench's own
|
||||
timings:
|
||||
|
||||
- **The frame rate never was the problem, and the first reading of it was
|
||||
wrong.** "103fps on a 120Hz screen" divided the fling phase's frames by
|
||||
its whole duration, which includes sixteen deliberate 300ms rests. Both
|
||||
runs sustained ~120.3fps through the motion itself. So the callback
|
||||
ordering was not costing frames -- what changed is the *clock*, which
|
||||
moves no frame count and is the whole point: an uneven sample of an
|
||||
even cadence cannot show up in any frame-time percentile.
|
||||
- **`missed vsyncs` counted idleness.** Every gap was treated as cadence,
|
||||
so the bench's own pauses read as stutter: 276 for sixteen 300ms rests,
|
||||
2410 for twelve hundred 50ms keystroke gaps, 821 for four hundred 50ms
|
||||
stream gaps -- each within a few percent of the arithmetic. A gap now
|
||||
measures anything only if the frame before it had asked for another.
|
||||
- **`late` counted the vsync wait as cost.** A well-paced loop spends
|
||||
each frame blocked in the acquire, so its total sits at exactly one
|
||||
refresh period and every frame lands on the budget boundary. It is
|
||||
judged on `FrameParts::work` -- the total minus the acquire -- now.
|
||||
- **The refresh rate is the larger of what the platform claims and what
|
||||
the run sustained**, because each can only be wrong one way.
|
||||
`Display.getRefreshRate()` answered 60 for a run that drew 3405 frames
|
||||
in 33.1s, since a phone that varies its rate answers with whatever mode
|
||||
it is in when asked. And `FrameReport::sustained_frame_hz` is a *floor*:
|
||||
an app that cannot keep up says nothing about the panel. The first
|
||||
version of it took the fastest tenth of the gaps rather than the
|
||||
sustained rate and reported **88Hz for this repo's 60Hz emulator**,
|
||||
whose app manages 51 -- a budget no frame there could meet, invented
|
||||
out of the app's best moments, and caught only by running the corrected
|
||||
report on the emulator before shipping it. The two are printed together
|
||||
whenever they disagree.
|
||||
|
||||
The signature of the fixed loop, from that run: `build p50 0.4ms,
|
||||
acquire p50 5.7ms, submit p50 1.7ms` -- four tenths of a millisecond of
|
||||
work and the rest of the refresh period spent waiting its turn.
|
||||
|
||||
### Streaming is where the frame time is now (2026-09-09)
|
||||
|
||||
Measured after the fling was fixed, and it is not where it looks.
|
||||
`frame_profile.rs`'s stream run: folding an arriving event is 0.35ms and
|
||||
applying the diff to the widget tree is 0.41ms, while the frame that
|
||||
follows is 3.86ms on this desktop and 9.5ms of `build` on Iris's phone --
|
||||
over a 120Hz budget on its own. 401 streamed events move the item count
|
||||
from 652 to 654, so nearly every one is a *delta into the same row*: the
|
||||
cost is re-laying out and re-shaping one growing markdown message on
|
||||
every delta.
|
||||
|
||||
`fold_event`'s `items.to_vec()` per event was the hypothesis -- it is the
|
||||
exact shape of the Compose lesson in AGENTS.md's "Things that have
|
||||
bitten" -- and measuring it is what ruled it out. **Not yet designed**:
|
||||
making a row's text append incrementally rather than reshape touches how
|
||||
`TranscriptRow` holds its shaped text, which is load-bearing enough to
|
||||
raise before building.
|
||||
|
||||
### The Android release profile is `opt-level = 3`, not `"s"` (2026-09-09)
|
||||
|
||||
The table above was measured in bytes only. `"s"` costs the loop
|
||||
|
||||
Reference in new issue
Block a user