# Residual scroll jank at 120 Hz — `perf` instrumentation Status (2026-10-29): the two *known* jank sources are fixed (GPU-resident thumbnails, §8.7 of HANDOFF.md; thread priorities). A small amount of residual jank remains on the phone (Mali G715) — suspected Slint event-loop bug, not yet proven. This doc describes the instrumentation used to hunt it. ## The `perf` cargo feature (off by default) `cargo apk2 build --release --features perf --target x86_64-linux-android` With `perf` off every instrumentation site is behind `#[cfg(feature = "perf")]` — the code is eliminated, zero runtime overhead. With `perf` on, three families of lines are logged (tag `gallery`) and mirrored to `/sdcard/Download/.gallery_perf.log` (the logcat ring buffer rotates too fast to be useful at ~120 lines/s). Only lines starting with `FR `, `STG `, `DECODE ` or `gallery: [W` are mirrored; logcat gets everything as before. ## Line formats - `FR d=<µs> gap=` — once per presented frame, from the rendering notifier on the UI thread. `t` = ms since process start (monotonic), `d` = app-side render cost (BeforeRendering→AfterRendering, i.e. everything Slint does in its render pass), `gap` = ms since the previous frame (120 Hz → 8.33 ms; a hitch = gap >> 8.33). - `STG wake=<µs> timers=<µs> events=<µs> n= poll=<µs> lastrender=<µs>` — cost of the event-loop *stages* of the iteration just completed, from timestamps instrumented into the vendored `i-slint-backend-android-activity` (`vendor/i-slint-backend-android-activity-1.18.1/stg.rs`, `stg::snapshot()`): `wake` = time from the previous iteration's poll-end to the first Choreographer event (waker/wakeup latency), `timers` = `update_timers_and_animations`, `events` = `process_event`, `n` = number of polled events, `poll` = whole `poll_events`, `lastrender` = whole `do_render` of the previous frame (draw + wgpu surface tail). - `DECODE ` — per-thumbnail-job cost on the worker threads; correlates decode bursts against FR gap spikes. ## How to read a capture 1. `adb pull /sdcard/Download/.gallery_perf.log`. 2. Find hitched frames: `gap > ~20 ms` on `FR` lines. 3. For each hitch look at the same-frame `STG` line: - big `wake` → the Choreographer waker was starved/delayed (the fixed nice-+10 bug had exactly this signature); - big `lastrender` → the previous frame's render tail (wgpu submit / surface present) ran long; - big `timers`/`events` → Slint-side event processing (our tick is budgeted; a spike here that correlates with `DECODE` bursts is cross-thread pressure). 4. If none of the stages explains the gap, the delay is *inside* wgpu/Vulkan or in Slint's surface handling (the suspected-bug class). The `STG` timestamps come from the pinned vendor crate; re-apply `vendor/i-slint-backend-android-activity-1.18.1` (including `stg.rs`) when bumping Slint.