tools/jank_eval.py: strict PASS/FAIL jank evaluator (the referee).
- slot grid from VSYNC ftn edges (hardware truth); anchor fallback only
- PASS iff per active window: no dropped slot, no same-slot double, no
present past the SF latch, no >20ms present gap, phase spread <=0.5ms
- self-test: 7 synthetic log files through the same main() path, all
classified correctly; validated on the emulator (60 Hz emu display
detected, active windows found)
- re-judges slt4 honestly: my 'clean' drag-1 window is FAIL (30.6ms +
21ms stalls, 20 past-latch presents, 1.1ms spread) — the user saw it
lib.rs/stg.rs: slot-based wake guard replaces the interval freshness
guard (0.2ms false-reject margin). A wake renders iff its edge-grid slot
is strictly after the last rendered slot; a gap of >=2 slots emits
MISS {mon, slots, nowake} — the missing evidence for stall slots
(wakes lost vs loop blocked vs guard).
sltest: log drain batch-flush (64 lines) — per-line FUSE flush stalled
the drain thread for minutes (file frozen, process alive).
gallery: MISS lines added to the perf-log allowlist.
The 'callback arrives 3-6 ms late' model that invalidated render-ahead in
earlier sessions was a mislabeled STG field: the Choreographer callback
actually fires at edge+0.5 ms (measured from the VSYNC ftn/mon pair). With
the wake ON the edge, the held frame presented at render START lands at
edge+~1.5 ms (constant phase, 4 ms of deadline margin), and render spikes
(5-10 ms, the measured cause of the residual 1% drops + 1.5 ms phase
spread) only stretch the PREPARE phase of the frame held for the NEXT
slot — spikes stop delaying visible frames.
- lib.rs: RENDER_AHEAD_ACTIVE = locked again (was false)
- lib.rs/stg.rs: IT lines gain rcpu (CPU us inside do_render) — spike
classifier: rcpu≈render ⇒ Skia/driver CPU work; rcpu≪render ⇒ blocked
on GPU/driver
- hypotheses-120hz.md: model correction + sltest baseline + results
Emulator behavior verified: scroll, settle (transition flush), mid-drag
pause/resume (no black window), no AlreadyAcquired panics, held-frame
presents at 0.6-2.1 ms. Phone cadence measurement pending (phone out for
user use ~2h).
Measurements only, no behavior change:
- vsync.rs: EW lines (every early-waker landing, EW_ON flag, off by default)
- lib.rs/stg.rs: IT lines gain dec (locked/saw_wake/pr_before/do_render) and
pcpu (CPU us inside poll_events — 2% = asleep in epoll, rules out CPU
spin/deschedule as the wake latency)
Findings (docs/BUGS/hypotheses-120hz.md, sltest baseline):
- Choreographer callback fires at edge+0.52ms (p90 0.65) — the 'late
Choreographer' model from earlier sessions was a mislabeled STG field
(poll block time, not callback latency). The pipeline actually works at
median: present at edge+~4ms, inside the 5.5ms deadline.
- sltest zero-jank definition measured: 98.8% of frames land in the next
display slot; residual = 0.5-0.7% drops + 0.7-0.9% doubles + 1.4ms
phase spread (p10-p90) = the visible micro-judder.
- Residual cost is VARIANCE, not median: per-frame render 3.5ms median
(SKD 1.07 + CLR 0.8 + PRE 1.0 + SKF/ORD 0.65) with spikes to 10.7ms
(SKD max 5.2, CLR max 3.2, PRE max 2.5). Spikes push presents past the
deadline (drops) and shake the phase (judder).
Next: classify spikes (CPU work vs GPU/driver block) via render-window
CPU splits, then kill the top source.