0a50f9b832
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 0a50f9b832 |
120Hz: fix the ALooper wake-fd race (root cause of slot drops) + phase-slip-robust referee
ROOT CAUSE of the 8-12% slot drops (uniform dec=1/0/1/0: locked, no wake seen, dirty, no render): AOSP Looper::pollInner (system/core/libutils/ Looper.cpp) drains the wake fd in the SAME epoll batch (up to 16 fds) as any ready input fd, and pollOnce returns the input's ident — the POLL_WAKE result is a local that gets dropped. ALooper_wake's byte is consumed WITHOUT PollEvent::Wake ever reaching Slint, unrecoverable. With motion events arriving 1-4 ms after the edge (always for a real 120-240 Hz finger; ~1 in 5 frames for 20 Hz adb input) the wake is lost and the locked decision (saw_wake && slot-guard) drops the dirty frame for that slot. Fix (vendor backend): a dedicated eventfd registered with the MAIN looper under ALOOPER_POLL_CALLBACK. The Looper invokes the C trampoline INSIDE pollInner (before pollOnce returns, regardless of which ident is handed back), so delivery can never be swallowed by a co-batched input response. - frame_callback: note the edge, write the eventfd, then ALooper_wake (secondary). The fd is leaked: it belongs to the main looper for the process lifetime. - trampoline: DRAINS the eventfd (level-triggered — without the drain the counter stays >0, the fd stays readable and the loop busy-spins on POLL_CALLBACK at ~10 kHz: 1812 spin iterations measured) and publishes the edge into delivered_edge. - main loop: saw_wake |= (delivered_edge newer than last seen). - DIRTY BACKSTOP: the 6 ms poll cap was never a backstop — timeout iterations have saw_wake=false so the locked decision never rendered (the 20-40 ms silences). Now: dirty && last render >= 2 slots old renders regardless of wake state. Measured on the phone (Pixel 9 Pro, 120 Hz, sltest, identical capture): buildC (before): 127 drops + 12 stalls in 1142 frames buildE (after): 8 drops + 0 stalls in 1266 frames Phase (p10-p90): 0.72-1.00 ms in all windows. Remaining 8/1266 (0.63%) are slip-and-recover pairs where the UI thread is DESCHEDULED 6-8 ms while sleeping in epoll (wakes arrive on time, cb-lat 0.5 ms) — EAS scheduling variance, no app-side contention in sltest. Referee fixes found while judging this data (tools/jank_eval.py): - NEAREST-OBSERVED-EDGE slot grid: the 120 Hz panel clock phase-slips after idle blanking (re-locks at arbitrary phase), invalidating any grid anchored at ftns[0]. An entire drag window read late=230/ dropped=24 while per-cycle tracing showed presents at edge+2.0 ms. slot_of_ftn assigns each present to the nearest observed hardware edge in a time-continuous slot domain (slip-immune: a constant delta shifts all slots, differences preserved; stall-accurate: ftn times are CLOCK_MONOTONIC). - tools/jank_capture.py: capture is INVALID unless the screen is Awake AND the keyguard is down (a run on a sleeping phone silently injects no input: 49 IT lines, 1 present). |
|||
| b97b6515d5 |
120Hz: Choreographer chain self-sustains on (animating||dirty); EW gates the wake; MISS v2 forensics; ctxsw discriminator + present CPU split
vsync.rs: - set_state(animating, dirty) replaces set_animating. The Choreographer callback chain re-posts while animating||dirty: gating on animating alone let the chain die during flings (pointer inactivity >200ms) — with EW disabled the loop then froze (measured: 30 frames in 4s, then silence). The dirty mirror is pushed every main-loop iteration (<=8ms stale); the 6ms poll backstop bounds chain-break damage. - EW_ON now gates the WAKE itself: the early waker raced the Choreographer (fires at edge+0.52ms measured) and its independent phase caused the -4..+4ms render-phase wander. Choreographer is the single wake source. - MISS lines carry forensics: sw/sn (wake slot vs last rendered slot), eage (ms since last observed vsync edge), lastage (ms since last render) — distinguishes chain-stall / loop-stuck / guard-reject / no-wake-iteration without a second run. stg.rs: - ctxsw_sample() reads /proc/self/task/<pid>/stat for voluntary/involuntary context switches, CONDITIONAL on iteration >5ms (per-iteration procfs reads were a measured confounder: 20-50us each, pushed phase across the slot boundary, 10x worse drops). vsw/nvol on the IT line. - precpu: CPU time inside Queue::present on the PRES line (pre_us >> precpu = blocked on binder/fence/SF; pre_us ~= precpu = CPU-bound). wgpu_30_surface.rs: EW_ON moved here (vkprobe) so vsync.rs can gate the wake without a new crate dependency. lib.rs: poll backstop uses the dirty state; 6ms cap during active phases. |
|||
| c72bfa481d |
120Hz hunt: EW wake probe + poll/render CPU-time splits; sltest baseline
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. |
|||
| 7c2d0f354e |
120Hz: early waker thread + render freshness guard (vsync-edge pacing)
The Choreographer vsync callback reaches the UI thread only 2.5-6 ms AFTER the vsync edge (measured on the phone). With a 2.5-5 ms Skia render plus present, that left no phase margin inside the 8.3 ms slot: 51% of frames landed one slot late (judder) and ~4% two slots late (hitches), on every build tested so far. Attempted fixes, all measured and retired (see commit history): - render-ahead hold (present previous frame at next render start): the held present lands after the NEXT wake's 3.2-6 ms Choreographer latency -> 52% one-slot late. - present-at-end + present GATE (sleep on the UI thread to quantize the present to the vsync grid): the gate sleep (2.5-4 ms) added to the slot budget (wake 5.3 + core 3 + gate 3 + present 1 = 12.3 ms > 8.3 ms) -> cadence collapsed to 60 Hz. Working fix (this commit): - vsync.rs: an 'early waker' thread learns the vsync grid from the callback's frame_time_nanos (vkprobe::note_vsync_edge) and wakes the main looper ~0.8 ms BEFORE each edge while animating. Render start moves from edge+2.5..6 to edge-0.8..+0.5, so the present lands at a natural ~edge+3..5 ms phase with no UI-thread sleep. - lib.rs: a render freshness guard (min gap = period - 1.5 ms) makes the FIRST wake of each slot (early or the late Choreographer fallback) render and rejects the second (double render = 60 Hz collapse). Threshold sits between late-after-early (5.8 ms, reject) and early-after-late (7.5 ms, accept). - lib.rs: locked renders no longer consume pending_redraw (it carried the motion invalidations; consuming it starved the follow-up unlocked iteration -> 60 Hz collapse). - stg.rs: IT lines now log the render decision (locked/saw_wake/ pr_before/do_render); vkprobe logs gate sleep (GATE_US, now 0). Measured (phone, 8580-file grid, slow 4 s drags): in-burst cadence: 8.29-8.44 ms median, p90 8.6-10.2 ms (was 8.36 median / 10.2 p90 with 3.7-20% two-slot gaps) in-burst >16.7 ms gaps: 0-2 per burst (was 3-13) present-to-present: 27% exact one slot, 33% one-slot-late steady state (imvisible), 3% f-jumps (Skia core 2.5-5 ms variance). Residual judder lever: Skia core cost variance (next step). Test hygiene reminders (HANDOFF 8.10): verify ResumedActivity, keep the task clean (sz=1), record the perf-log byte offset before each run (the file accumulates across process launches). |
|||
| 2f4768f354 |
Fix 120Hz scroll jank end-to-end: render-ahead present + full-cache eviction off the UI thread
Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping). |
|||
| 93b27d724b |
Root-cause and fix the Mali scroll jank; strip jank-hunt instrumentation
The 19-26 ms mid-scroll stalls (Mali-only, fixed content offsets) were the Skia renderer's final synchronous GrDirectContext::flush() blocking the UI thread on Mali driver back-pressure — proven by per-stage timestamps in the vendored renderer (stall frames: flush 19-24 ms, walk 1-3 ms, 1 binding re-eval, image imports 0.1 ms each). - vendor/i-slint-renderer-skia (pinned 1.18.1, [patch.crates-io]): remove that one flush. Redundant on the wgpu backend: the surface already calls flush_and_submit before queue.present, same Vulkan queue, in-order. Phone: 0 stalls across 4 runs (max 3.6-7.4 ms vs the 8.3 ms budget), display verified clean. - docs/BUGS/slint-skia-final-flush-jank.md: full evidence, prior art (slint-ui/slint#12030 closest), open question for a future upstream filing (not filed yet). - Profiling also retired the O(n)-walk theory: Slint 1.18 prunes the item walk to ~133-152 items; the windowed Flickable built on that theory was already reverted (ListView is smooth with the fix). - Strip all jank-hunt instrumentation: FR/SCR/TICK/APLYD/STG logging, stg markers, .gallery_* file knobs, jsamp thread sampler, DbgScroll programmatic scroll, nblog perf-file mirroring. Also fixes the 'launches into unstoppable fast scroll' bug: a leftover .gallery_cmd was re-read on every launch (file never deleted) and overrode user swipes — the hook is gone. - Keep: nblog (logd backpressure is real), zero-dimension decode guards, keep-alive (default-off). - HANDOFF: replace stale windowed-Flickable section with the actual root cause/fix; 15 unit tests pass. Verified on phone (aarch64, 8580 photos) and emulator (x86_64). |