Residual scroll jank at 120 Hz (suspected Slint event-loop bug) needs per-frame + per-stage timing the logcat ring buffer can't hold. New cargo feature `perf` (default OFF — all sites are cfg-gated, zero overhead when off): - FR: per-frame app-side render cost + frame gap, from the rendering notifier (BeforeRendering/AfterRendering). - STG: event-loop stage costs (wake/timers/events/poll/lastrender) from timestamps instrumented into the pinned vendor copy of i-slint-backend-android-activity (stg.rs; Cargo.toml now depends on it directly + optional under `perf`). - DECODE: per-thumbnail-job cost on the workers. - All lines mirrored to /sdcard/Download/.gallery_perf.log (logcat rotates too fast at ~120 lines/s). Format + reading guide: docs/BUGS/scroll-jank-120hz.md.
2.9 KiB
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 <t_ms> d=<µs> gap=<ms>— 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=<events> poll=<µs> lastrender=<µs>— cost of the event-loop stages of the iteration just completed, from timestamps instrumented into the vendoredi-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= wholepoll_events,lastrender= wholedo_renderof the previous frame (draw + wgpu surface tail).DECODE <ms> <hit|miss> <path>— per-thumbnail-job cost on the worker threads; correlates decode bursts against FR gap spikes.
How to read a capture
adb pull /sdcard/Download/.gallery_perf.log.- Find hitched frames:
gap > ~20 msonFRlines. - For each hitch look at the same-frame
STGline:- 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 withDECODEbursts is cross-thread pressure).
- big
- 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.