From 99d155e859f72627fe34088c5862e9033249bf1a Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Fri, 30 Oct 2026 09:05:08 -0400 Subject: [PATCH] =?UTF-8?q?HANDOFF=20=C2=A78.10:=20pop-in=20root=20causes?= =?UTF-8?q?=20(paint-cap=20drop,=20scan=20storm,=20black=20window)=20+=20t?= =?UTF-8?q?est=20hygiene?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HANDOFF.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/HANDOFF.md b/HANDOFF.md index f67a09f..459d7a5 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -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 — see the M4 entry in §9 for the verification notes + emulator pinch 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).