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:
irisandClaude Opus 5 committed 2026-09-09 01:08:30 -04:00
1 parent 42d54eec95
commit 9bf714fa2e
6 files changed
+457 -57

No files matched your search

+21 -14
View File
@@ -389,21 +389,28 @@ Each exists because something was invisible without it.
since the rig lives in iris and the app's examples do not). The emulator is for JNI, the IME, insets, the surface
lifecycle and one verification run before a build goes to the phone --
not for iterating on layout.
- **`app-rust/tests/fling_profile.rs`** is what a fling frame costs on
the CPU, at layer 1 -- `cargo test --release --test fling_profile --
--ignored --nocapture`, from `app-rust/`. It flings the real transcript
screen over the bench fixture eight times, out and back, and prints the
per-frame distribution with a count of how many frames did any layout at
all. `#[ignore]`d and assertion-free, so `run-tests.sh` neither runs it
nor can fail on it; **release or the numbers mean nothing**, since text
shaping dominates. Two things it established on 2026-09-09 that are
worth not re-deriving: only about one fling frame in six lays anything
out (the rest are moved on the GPU through `move_offsets`), and the
- **`app-rust/tests/frame_profile.rs`** is what a frame costs on the CPU,
at layer 1 -- `cargo test --release --test frame_profile -- --ignored
--nocapture`, from `app-rust/`. Two runs: a fling over the bench
fixture eight times out and back, and a reply streaming into it one
event at a time. `#[ignore]`d and assertion-free, so `run-tests.sh`
neither runs it nor can fail on it; **release or the numbers mean
nothing**, since text shaping dominates. It cannot answer anything
about the GPU, the swapchain or the phone's own clock.
What it established on 2026-09-09, worth not re-deriving. A **fling**
is not CPU-bound: only about one frame in six lays anything out (the
rest are moved on the GPU through `move_offsets`), and the
multi-millisecond spikes are all in the *first* pass over a stretch of
transcript -- every later pass over the same rows is p99 0.26ms. So a
warm fling is not CPU-bound in iris, and a phone report showing
otherwise is measuring something else. It cannot answer anything about
the GPU, the swapchain or the phone's own clock.
transcript -- every later pass over the same rows is p99 0.26ms. A
**streamed event** is, and it is not where it looks: folding the event
is 0.35ms and applying the diff to the widget tree is 0.41ms, while the
*frame* is 3.86ms here and 9.5ms on Iris's phone. 401 streamed events
move the item count from 652 to 654, so almost every one is a delta
into the same row -- the cost is re-laying out and re-shaping one
growing message on every delta, not the fold. (The fold was the
hypothesis, from `foldEvent`'s Compose lesson under "Things that have
bitten"; measuring it is what ruled it out.)
- **The emulator is a GLES rig, deliberately** (Iris, 2026-09-08;
docs/RUST.md). Its guest has no hardware Vulkan -- only SwiftShader
in software -- while its GLES *is* the host's real GPU through virgl at