gallery/docs/BUGS/hypotheses-120hz.md
Greg Pomerantz 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.
2026-10-30 11:59:39 -04:00

2.2 KiB

120 Hz zero-jank campaign — hypothesis log

Target: sltest (minimal 100-row grid, no textures) on the phone's Mali G715. Definition of ZERO JANK (measured from PRES mon= + VSYNC ftn= lines):

  1. no display-slot skip: for consecutive presents, slot_index(n+1) == slot_index(n)+1 (100%)
  2. no same-slot double: two presents never land in the same display slot
  3. phase stable: present phase within its slot varies < 0.5 ms Test: 2x input swipe 640 1900 640 900 4000 (slow drags) + fling, ~10 s. A "burst" = consecutive presents with gaps < 20 ms; evaluate inside bursts only.

Hypotheses (order = priority)

  • H6 [sltest confound]: sltest FileLogger is blocking (flush per line) on the render path (PRES/STG/IT at ~240 lines/s). FIX: non-blocking drain-thread logger. TEST: rebuild, re-measure baseline.
  • H1: early waker misses edges (thread descheduled / nanosleep jitter) → render starts late (edge+5 late wake wins) → present phase bimodal. TEST: render-start phase histogram vs edges; early-wake hit rate.
  • H2: Skia render core variance → present phase jitter. TEST: VK SKD distribution in sltest (trivial scene, expect <1.5 ms).
  • H3: wgpu/Mali submit+present latency variance (CLR/SKF/ORD/PRE). TEST: VK PRE / CLR / SKF distributions in sltest.
  • H4: event-loop overhead (timers, change tracker, spin iterations) eats the slot budget. TEST: IT poll/timers/ev/fe distributions; iters/s during scroll.
  • H5: CPU scheduling of the UI thread (deschedule mid-slot, core migration, frequency). TEST: wall vs CPU split for the render window (vkprobe thread_cpu_ns).
  • H7 [method confound]: adb input swipe motion is bursty (~20-60 Hz) → grid movement itself is non-uniform regardless of render cadence. TEST: pointer-location overlay + user finger, later.

Rules

  • One hypothesis per build; keep the change minimal; measure before/after with the identical test; record numbers here.
  • A hypothesis is RULED OUT only when the measurement it predicts is absent (not "the fix didn't help").
  • Phone hygiene before each run: clean task (sz=1), ResumedActivity, dismiss 16KB dialog (sltest is debuggable), record log byte offsets.

Results

(append below)