Live watching is now event-driven, no tree polling:
- Android: MainActivity registers a MediaStore ContentObserver
(Images, notifyForDescendants) and wakes the Rust watcher thread via
a JNI call (Java_wow_gmp_gallery_MainActivity_nativeFsChange) — a
single channel send, no main-thread work. Covers camera saves,
media-scanner indexing, other apps' indexed writes.
- Desktop: inotify/FSEvents via notify on the scan roots.
- On wake: coalesce + debounce, diff dir mtimes, rescan only the
changed subtrees. The 1 s recv timeout doubles as a cheap backstop
(raw out-of-band copies the scanner hasn't indexed yet); a 5-minute
full pass is the safety net. minSdk 30 (3-arg ContentObserver API).
Stale-listing truncation bug found and fixed (reproduced on the
emulator's 9p /sdcard; the phone's FUSE does the same): a readdir over
a large directory can return a SILENT early EOF — a stale cached
listing with NO error. The phone's 9961-file Camera dir truncated at
~8400 once and db::apply deleted ~1700 good rows. Three layers now:
1. scan::read_dir_full — retry whole-dir reads on mid-stream errors;
2. per-directory cross-check in walk_dir: if the enumeration is far
below the DB's count for that dir, re-read up to 3x keeping the
best listing; whole-walk re-check in rescan keeps the larger of two
walks;
3. db::apply mass-delete guard: never delete >20% of the DB in one
apply unless a previous walk already saw the low count (two-strike;
a meta.walked row tracks it). Genuine gradual deletions still flow
through (verified: a 4000-file mass move streams the grid down
progressively), and a second consecutive low walk still deletes.
Emulator reliability suite (all pass): 3x launch count stability,
add +1 / remove -1 in ~2 s, 20-file burst, DB recovery from damage,
stale-listing refusal (0 rows lost), two-strike delete, live restore,
idle 0.0% dropped frames, deep fling loads thumbnails. A/B vs ttimg
(no-backend CSS-scroll control): adb-swipe drop deltas are at the
input-injection artifact level, idle identical (0.0%).
Root causes found via per-event tracing (trace.log) + heartbeats:
1. Decode backlog: single worker at ~300ms/photo could not keep up with
fast long scrolls — phone session ended with a 2400-deep queue
draining at ~3 cells/s (13+ min to catch up). That was the
'scroll far down and nothing loads no matter how long I wait' zone.
- 4 parallel decode workers (~12 photos/s)
- viewport-aware stale drops: prefetch jobs more than 60 rows behind
the viewport are dropped, not decoded (fleeing leaves no backlog)
- Q_CAP bounds the queue; JS 1.5s safety net re-requests
- queue peak in repro test: 42 (was 2405); all 850 decodable
emulator images now load (708 before)
2. Stale-anchor bug: sendAnchor was leading-edge throttled with no
trailing call — if a scroll ended inside the 150ms window the Rust
anchor stayed stale forever, and the worker then dropped fresh
URGENT jobs (tap → full read) as 'off-screen'. Fix: trailing
anchor send; and URGENT jobs are never stale-dropped (only
prefetch work is droppable).
3. Viewer latency: full-res read ran on Chromium's main thread
(wry shouldInterceptRequest is synchronous) — 30-50MB flash read
froze the whole WebView. Now: thumb shown instantly (mem cache,
~1ms), full read queued urgent and done off-main; full-ready event
swaps in the full-res (256MB full-res LRU makes re-opens instant).
Measured click→full-res: 4ms on emulator.
4. Scan: skip OS junk (._* AppleDouble forks, dotfiles) — the phone's
DCIM carried 72 of them, rendering as permanent gray cells.
5. Viewer for a corrupt file now shows 'image unavailable' instead of
'loading…' forever; grid retry timers skip far-off-screen cells.
Chromium owns scroll/compositor/lazy-load/image-cache; Rust owns
scan/SQLite/thumbnail-decode behind a gallery:// custom protocol.
Hard-won gotchas (see in-code comments):
- Android: no native custom-scheme support → wry rewrites the URL to
http://gallery.localhost/... (custom_protocol_workaround). The JS
MUST request that form; the handler receives the reverted
gallery://localhost/... and must strip the authority.
- tauri_build::build() in build.rs must run UNCONDITIONALLY (build
scripts run on the host: a cfg(target_os) gate silently disables it,
which skips the rustc-cfg=mobile emission → no mobile_entry_point →
.so with no JNI symbols → UnsatisfiedLinkError).
- build.rs must list frontend files for rerun-if-changed (a dir-level
directive only watches the dir mtime, not content edits) — the .so
bakes the frontend at compile time and cargo otherwise ships stale
UI with a 'successful' build.
- logcat drops ~75% of lines under load: use the file trace.
- fetch() is CORS-restricted (page origin http://tauri.localhost);
<img> is not — add ACAO:* anyway.
Emulator (5853 real files): grid renders, lazy thumbs via
gallery:// with on-disk cache (87 files 35-72KB), FR log clean at
60Hz, viewer opens full-res with compositor-only pan/zoom. The single
'broken' cell is a 17-byte fake test JPEG (correct behavior).