15a857ed7f
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 10ec0b52cc |
gtauri: fix phantom rows, dead deletion detection, 5-min launch stall
Root causes found by reproducing each on the emulator (2.8k-file fixture with mixed jpg/mp4): 1. walk_dir_nostats (incremental rescan of >2000-file dirs) had NO image-extension filter: it re-enumerated every file, injecting all ~1,727 videos into the DB. This is what made the count swing 8,439 -> 10,166 and the grid full of blank cells (404 rows). Now filters through is_image_path() like every other walk. 2. walk_dir aborted the ENTIRE walk on any per-file stat failure (file deleted mid-walk), so an error during the walk meant the walk was never applied and deletions were never detected. Per-file stat errors are now skipped; only readdir/root errors fail the walk (an errored walk is still not applied). 3. dir_sig stat'ed every entry of every root dir every watch tick: 10k+ stats/second on the phone = 10s+ of FUSE I/O per second = the long delays. On Android the MediaStore URI payloads already carry the exact change signal, so the mtime-diff machinery now runs desktop-only. The Android watch tick is a single read_dir of our own app dir (no-op when empty): 1s backstop, 30s-throttled fallback full rescan for events with no payload, 5-min full pass. 4. TOCTOU: a delete consumed while a full rescan was mid-walk lost to the walk's stale snapshot (walk re-added the row). RESCANNING gate parks payloads during the walk; they apply after it, so mid-walk deletions always win. Reproduced deterministically (churn during the startup walk), fixed, re-verified. Emulator stress (all passing): full-walk convergence, add +1s, delete +1s, video add/delete = zero rows, 60-op churn converges to exact disk count, 3x relaunch idempotency, startup-walk race converges (1s normally, 5-min backstop when the OS scanner drops burst events), no-blank-cells visual check, 0.00% dropped frames. Phone verified: 10,166 -> 8,439 on first launch (all video phantoms purged), add appears ~2s after the OS indexes it, delete same, DB returns to 8,439 exactly. ContentObserver registration + onChange logging added (it fired reliably once the stale instance was replaced). |
|||
| bb349dac82 |
gtauri: URI-driven MediaStore events; remove truncation machinery
Watching is now exactly what the OS tells us, with no filesystem scanning on the event path: - MainActivity's ContentObserver receives the changed URI from Android. It queries that ONE row (~ms, no debounce) and drops a one-line payload: A|media_id|path|size (row exists) or D|media_id (row gone). Per-event files (.ms_recent.N, never overwritten) so a burst cannot clobber an unread event. A zero-arg JNI call wakes Rust; a 1 s backstop also drains the queue, so a lost wake self-heals in a second. - Rust applies the payload directly: upsert with media_id stored, or delete resolved by id (items row first, then a media_map table that records id→path for EVERY add event — including rows created by a filesystem walk, which doesn't know the id). Content changes reset decode-failure state so a still-being-written photo retries. - The disk is the truth: the payload size is ignored in favor of a stat (the scanner can leave _size NULL). - The full FS walk remains as startup baseline + 5-min reconciliation, and the mtime-diff incremental rescan remains as a second independent path. Verified on the emulator: add → grid in ~2 s (incl. OS scanner), delete → grid in ~2 s via media_id, missed wake drained in 1 s. Truncation machinery REMOVED (per user: FUSE is reliable; the 'truncations' were counting errors and a stale-DB cross-check firing on legitimate changes): the two-strike mass-delete guard, the walked_max watermark, the layer-2 re-walk, the per-directory stale cross-check, and the read_dir_full retries are all gone. The one invariant that replaces them: a walk that hits ANY filesystem error fails and is NOT applied (an error is not 'those files are gone'). The phone DB now converges to the exact disk count (8,439 = 8,399 Camera + 27 Screenshots + 11 Messaging + 2 Bluesky); the earlier 10,166/8,438 swings were real disk-state changes, not truncation. Emulator display fixed: the AVD image ships with debug.hwui.drawing_enabled=0 (every window renders black, HWUI drawing disabled system-wide). 'adb shell setprop debug.hwui.drawing_enabled 1' after each boot restores rendering — it is a runtime prop, so it must be re-set on every cold boot. |
|||
| 1bc1d6beb3 |
gtauri: fix camera-photo latency, delete blank boxes, truncation guard
- Camera-photo latency: root cause was READ_MEDIA_IMAGES never granted (granted=false on the phone), so the MediaStore fast-path query silently returned nothing and photos only appeared via the slow full FUSE walk. The app now requests the permission at runtime (once, with a single retry on denial). The camera writes through MediaStore at capture time, so the ContentObserver fires immediately and the photo appears in ~1 s. - Delete blank boxes: two JS flaws — (1) no img onerror handler, so a cell that requested a thumb for an already-deleted file (404) sat as an empty dark box forever; (2) recycled cells were matched by slot index only, so after a deletion shifted the list a cell could keep a stale id. Now: onerror drops stale ids (idSet membership) or retries transient ones, and cells reassign on idx OR id change. - Truncation guard (chicken-and-egg): the mass-delete suspicion threshold was 20% but observed FUSE truncations lose 15-17%, which sailed under it and poisoned the DB. Now 10%; a confirmed deletion (two-strike passed) lowers the new walked_max watermark, a refused one keeps it. Layer-2 re-walk compares against max(db, walked_max) so a poisoned (shrunk) DB cannot silence truncation detection. Emulator suite: healthy→truncated (all 3 layers, 0 loss), poisoned-DB→truncated (layer 2 now fires), healthy walk self-heals. - Verified on the phone: DB matches disk exactly (8,398 Camera + 27 screenshots + 11 messaging + 2 bluesky = 8,438); an earlier 10,166 count was a transient user-system state, not truncation. |
|||
| 9e52dc0355 |
gtauri: MediaStore fast path + js-ready handshake + virtualized grid
- MediaStore fast path: Kotlin's ContentObserver queries MediaStore for images added in the last 120s and writes path|added|size lines to .ms_recent, then wakes Rust via the proven zero-arg nativeFsChange() JNI. Rust upserts those rows and emits immediately — a camera photo appears in ~1s without waiting for the ~10s FUSE re-read of a 10,000-file directory. (Direct Kotlin->Rust JNI string passing and Rust->Java calls crash under the emulator's x86_64->arm64 native bridge; file handoff plus zero-arg JNI is the only reliable channel.) - js-ready handshake: the first gallery-files emit fires ~30 ms after process start, before the WebView's JS exists (Tauri events are fire-and-forget, so it was lost). JS now signals 'gtauri-js-ready'; Rust re-emits the current state. - Virtualized grid: the document is ONE spacer (full scroll height) plus ONE band of recycled cells pinned by transform — ~80 DOM nodes total for a 10,000-photo library. Launch cost no longer scales with library size; the first screen renders as soon as the WebView boots and the first thumbs arrive. - 'Missing photos' report resolved: the full walk of the 9,961-file Camera dir enumerates exactly 8,438 images with no guard firing; the difference is videos, which a photo gallery correctly excludes. |
|||
| 58b203e4fa |
gtauri: event-driven watching + defense against stale VFS dir listings
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%). |
|||
| 02d1f097ac |
gtauri: fix deep-scroll stall + instant viewer
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.
|
|||
| e019a950b1 |
gtauri: Tauri v2 gallery harness WORKING on emulator
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). |