Commit Graph

4 Commits

Author SHA1 Message Date
01c15795ff 2ms poll cap during active scroll + record_check.py (display-side referee)
2ms cap (vendor backend): the residual 8/1266 phone slip-frames all
correlated with polls that ran 5.5-10.7ms despite the 6ms cap
(ALooper_pollOnce(6) returns 2-5ms LATE on this device — the delay is
inside kernel/epoll; vsw=nvol=0 on every slip iteration, so the UI
thread was never descheduled mid-iteration). A 2ms cap bounds the
damage of a lost/late wake: the next poll picks up the pending edge
within 2ms → render start edge+2-3ms → present inside the slot.
Emulator A/B (2 vs 6ms): no difference (5-8 drops/capture both ways,
vs 47-147 for the pre-evfd build) — the evfd wake-fd fix dominates;
the shorter cap is kept.

tools/record_check.py: the DISPLAY-side referee. jank_capture judges
the app's self-report; this records the actual display output with
`adb shell screenrecord` (30 Mbps) during scripted (or hand) drags and
measures inter-frame gaps in the mp4:
  * median refresh period is computed from ACTIVE-WINDOW gaps only —
    rest periods emit idle frames at irregular 20-100ms intervals that
    would pollute the reference (found during validation)
  * active windows come from real wall-clock drag times (1.5s pad)
  * verdict: CLEAN (0 gaps >1.8x med), SUSPECT (1.25-1.8x), JANK
  * selftest: 4 cases incl. drop-in/outside-active-window + real
    ffmpeg 120fps mp4 through the ffprobe pipeline
Validated end-to-end on the emulator (record → drags → pull → analyze).
When the phone is free: one `record_check.py record --app wow.gmp.sltest
--drags 4,2,4` run gives the visual-certain answer the user asked for —
did the glass actually show every frame?
2026-10-30 16:40:09 -04:00
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).
2026-10-30 16:08:48 -04:00
a68c8996c6 perf tooling: jank_capture.py (end-to-end capture+report) + stress-test fixes in jank_eval
jank_capture.py: one command owns the whole pipeline so provenance can
never be wrong (the failure mode this replaces: append-only logs spanning
processes, hand byte-offsets, tail-guessing, wrong-build data):
  1. record build identity (versionName + apk sha256 + tag)
  2. force-stop + DELETE it.log  (file can then contain exactly ONE process)
  3. cold launch, verify ResumedActivity (before and after)
  4. scripted drags ('4,2,4' = 4s drag, 2s settle, 4s drag)
  5. pull the complete log
  6. report: PROVENANCE + jank_eval verdict + MISS forensics
  verdicts: PASS(0) / FAIL(1) / SUSPECT(fitted grid) / INVALID(multi-process)
MISS forensics: groups consecutive misses into episodes, classifies each
  (CHAIN-STALL / LOOP-STUCK / GUARD-REJECT / NO-WAKE-ITER) with a +-33 ms
  all-thread context window (IT poll/ev/rd/dec/pc/rc/vsw/nvol, VSYNC
  latency, PRES, FR, SLOWEV, DECODE, OTHR, INVK).
  self-test: synthetic 120Hz log with a 30 ms stuck-poll episode.

jank_eval.py fixes found under emulator stress:
  - auto-windows active threshold was circular: 0.75x the refresh rate
    meant a JANKY app (low present rate) is never detected as active.
    Now 0.4x rate / 250 ms buckets. (Emulated 60 Hz scroll at 30-40 Hz
    present was invisible to the old threshold.)
  - BUCK 500->250 ms required the density multiplier *2 -> *4 (forgot,
    caught by stress: only the fastest spans were windowed).
  - fallback period (no VSYNC lines) was a telescoping mean (spans idle
    gaps: 391 presents over 23 s fit '59.6 ms'). Now median-of-deltas
    snapped to standard rates. A fitted grid cannot prove drops/doubles:
    verdict on it excludes them (stalls+lates+spread remain) and the
    verdict is marked SUSPECT (exit 3) instead of PASS/FAIL.
  - run_file now returns (ok, suspect); small-log early exit fixed.

Stress matrix on emulator (x86_64, 60 Hz SF) — all pass:
  capture pipeline x3 (standard 4,2,4 / fast 1,1,1 / slow 6,2,6)
  pause/resume mid-drag (teardown -> occluded -> resume burst)
  determinism (identical report twice), multi-process -> INVALID,
  explicit --windows, --period-ms override, no-VSYNC -> SUSPECT path,
  small log -> exit 1, synthetic perfect 120 Hz -> PASS exit 0.
2026-10-30 14:45:52 -04:00
915e04c2c3 120Hz: jank_eval referee + slot-based wake guard + MISS ledger
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.
2026-10-30 13:29:50 -04:00