HANDOFF §8.10: pop-in root causes (paint-cap drop, scan storm, black window) + test hygiene

This commit is contained in:
Greg Pomerantz 2026-10-30 09:05:08 -04:00
parent b9317e7d23
commit 99d155e859

View File

@ -862,3 +862,75 @@ un-granted: settings card → open Settings → recheck on Resume); test the
passgo-style flow on a fresh install. (M4 viewer is DONE, 2026-10-29 — passgo-style flow on a fresh install. (M4 viewer is DONE, 2026-10-29 —
see the M4 entry in §9 for the verification notes + emulator pinch see the M4 entry in §9 for the verification notes + emulator pinch
injection recipe.) injection recipe.)
### 8.10 Thumbnail pop-in — root causes & fix (2026-10-30, CLOSED, `b9317e7`)
User report: on the warm 8580-file DB the thumbnails "snap in" slowly
after a flick; also requested a test of the COLD launch (fresh scan,
streamed list). Both turned out to be drain-tick / scan-pipeline bugs,
not render bugs.
**Bug A — paint-cap drop (warm + cold).** The drain tick paints at most
`MAX_PAINT_CELLS_PER_TICK` (2) cells/tick; finished decodes beyond the
cap were **silently dropped** (removed from `inflight`, never added to
`painted`). FILL then re-submitted them → the same cell re-decoded
3–6× per viewport; a new viewport's ~21 cells took 500–850 ms to fill
after a flick. The flick bursts themselves were clean 120 Hz — all the
"late frames" were in the settle window.
Fix: excess results hold in `app.ready` (bounded `READY_CAP=48`,
`app.ready_paths` keeps FILL from re-submitting) and paint on a later
tick at zero re-decode cost. Cap is adaptive: 2 during grid motion
(frame budget), 4 when settled.
**Bug B — cold-start scan storm.** Every 512-entry scan batch re-sorted
the WHOLE accumulated list and `set_list` did a full model reset
(`set_vec`) + generation bump + state clears. Sorted prefixes are NOT
stable as the set grows (new entries interleave by mtime), so each
batch orphaned every queued/in-flight decode (arrived marked stale →
dropped → re-submitted): a 17× re-decode storm (max observed dups per
file = 18 = batch count) plus full row churn per batch.
Fix: batches now stream in APPEND order (each 512-group mtime-sorted,
groups concatenated → stable prefix); `set_list` detects appends
(element-wise prefix match) and extends the model in place
(`extend_from_slice` — existing rows/textures untouched) with NO
reset/generation-bump. The final global sort (DB order) is ONE reset at
scan end; the viewport re-fills from the disk cache (~2 ms/cell).
Measured on phone (cold, fresh DB): scan 7637 ms → ~4.3–4.9 s, decode
dups 18 → 2–3, grid renders with thumbnails while streaming.
**Bug C — black window after pause (found while testing A/B).** The OS
can invalidate our surface (system dialog activity, app switch, screen
lock) with NO window event and no pending model change; nothing ever
sets `pending_redraw` again → the window comes back black and stays
black until the next content mutation.
Fix: (1) `request_redraw()` unconditionally in `RenderingSetup`;
(2) drain-tick **window-liveness watchdog**: window visible but
`LAST_PRESENT_US` (updated in `AfterRendering`) stale >2.5 s → one
forced redraw. `now_us`/`NOW_T0` are no longer `perf`-gated (the
watchdog needs them in all builds).
**Also: `debuggable = false`** (was `true` for simpleperf during the
jank hunt). A debuggable app shows the system **16 KB compatibility**
dialog on every cold launch; that dialog is its own activity, it
paused us, and its inconsistent lifecycle (TerminateWindow or not,
depending on timing) was the main black-window trigger in testing.
NOTE: the .so is 4 KB-aligned (fine on this 4 KB-page kernel); on a
16 KB-page kernel a non-debuggable build would be refused — revisit if
targeting such devices.
**Phone verification (cold, 8580 files, fresh DB, exclusive phone):**
clean launch (449 ms to window, no dialog), grid streams in with
thumbnails, two fast flicks render 120 Hz (median 8.3–8.5 ms, 0–2 late
frames over ~220 frames), viewport fully painted after the flicks,
grid intact after an app switch (Termux 4 s) — no black window.
**Test hygiene learned the hard way:** the phone is a shared personal
device — Fossify apps / Termux / the lock screen / the notification
shade kept stealing foreground mid-test, which masquerades as "app
not processing input / black screen" (the input lands on the other
app). Always: verify `ResumedActivity` before AND after each
measurement, keep swipes mid-screen (y 700–2100; bottom-edge = lock,
top-edge = shade), and re-pull the perf file from a recorded byte
offset (`stat -c %s` before launch) — the file accumulates across
processes. `nev=` in IT lines under-counts motion (a visual scroll is
the ground truth).