From 15a857ed7f039e7d3c16849fc4c7828a1fcb85f4 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Tue, 3 Nov 2026 08:49:06 -0500 Subject: [PATCH] gtauri: enable fast mtime-diff filesystem watcher on Android (adds & deletes reflected in ~1s) --- harnesses/gtauri/src/lib.rs | 115 ++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 43 deletions(-) diff --git a/harnesses/gtauri/src/lib.rs b/harnesses/gtauri/src/lib.rs index d953911..70aa4a5 100644 --- a/harnesses/gtauri/src/lib.rs +++ b/harnesses/gtauri/src/lib.rs @@ -1141,6 +1141,47 @@ fn spawn_fs_watcher(tx: mpsc::Sender) { }); } +fn collect_watched_dirs(roots: &[PathBuf]) -> HashSet { + let mut dirs = HashSet::new(); + for r in roots { + if r.is_dir() { + dirs.insert(r.clone()); + if let Ok(rd) = std::fs::read_dir(r) { + for ent in rd.flatten() { + if let Ok(ft) = ent.file_type() { + if ft.is_dir() { + let p = ent.path(); + if !p.file_name().map_or(false, |n| n.to_string_lossy().starts_with('.')) { + dirs.insert(p); + } + } + } + } + } + } + } + if let Ok(mut conn) = db::open(&db::db_path()) { + if let Ok(entries) = db::list_entries(&mut conn) { + for e in entries { + if let Some(parent) = std::path::Path::new(&e.path).parent() { + dirs.insert(parent.to_path_buf()); + } + } + } + } + dirs +} + +fn fast_dir_sig(dirs: &HashSet) -> HashMap { + let mut out = HashMap::new(); + for d in dirs { + if let Ok(m) = std::fs::metadata(d) { + out.insert(d.clone(), mtime_ns(&m)); + } + } + out +} + /// Diff two dir-mtime signatures: dirs added, removed, or changed. fn changed_dirs( old: &HashMap, @@ -1181,15 +1222,13 @@ fn watch_files(app: tauri::AppHandle) { }); #[cfg(not(target_os = "android"))] spawn_fs_watcher(tx); - // Desktop only: the mtime-diff machinery. On Android the MediaStore - // URI payloads ARE the change signal (exact paths, no scanning), - // and diffing would cost a 10k-entry readdir every second on the - // phone — pure overhead for what the payloads already deliver. - #[cfg(not(target_os = "android"))] - let mut sig: HashMap = HashMap::new(); - #[cfg(not(target_os = "android"))] + + let mut watched_dirs = collect_watched_dirs(&scan::roots()); + let mut sig = fast_dir_sig(&watched_dirs); let mut baseline = true; + let mut last_dir_refresh = std::time::Instant::now(); let mut last_full = std::time::Instant::now(); + loop { let woke = match rx.recv_timeout(std::time::Duration::from_secs(1)) { Ok(e) => Some(e), @@ -1198,7 +1237,6 @@ fn watch_files(app: tauri::AppHandle) { }; let do_full = matches!(woke, Some(WEvent::Full)); if let Some(WEvent::Fs) = woke { - // Coalesce the burst, let the write settle, coalesce again. while rx.try_recv().is_ok() {} std::thread::sleep(std::time::Duration::from_millis(600)); while rx.try_recv().is_ok() {} @@ -1206,52 +1244,43 @@ fn watch_files(app: tauri::AppHandle) { if do_full { tr("watch: full rescan (5min)"); rescan(&app); + watched_dirs = collect_watched_dirs(&scan::roots()); + sig = fast_dir_sig(&watched_dirs); last_full = std::time::Instant::now(); continue; } + + // Refresh watched dirs periodically (every 30s) to pick up new subdirs + if last_dir_refresh.elapsed() >= std::time::Duration::from_secs(30) { + watched_dirs = collect_watched_dirs(&scan::roots()); + last_dir_refresh = std::time::Instant::now(); + } + // MediaStore event path (Android): MainActivity drops a one-line // payload per changed URI and wakes us via nativeFsChange. - // Consume on EVERY tick (single read_dir of our own app dir — - // no-op when empty, so a lost JNI wake self-heals within the 1 s - // backstop) — but NOT while a full rescan is mid-walk: those - // payloads wait as files and apply after the walk's apply, so - // mid-walk deletions win over the walk's stale snapshot. - let applied_ms = if RESCANNING.load(std::sync::atomic::Ordering::Relaxed) { + let _applied_ms = if RESCANNING.load(std::sync::atomic::Ordering::Relaxed) { false } else { apply_mediasotre_payload(&app) }; - // Android fallback: the OS said "media changed" but no payload - // arrived (permission missing, scanner gap, or the payload was - // parked behind a rescan). Full rescan, throttled — the - // degraded path, not the common one. Parking behind a rescan - // also counts as "not applied": the rescan's next 1 s tick - // re-evaluates and either drains the payload or rescans. - #[cfg(target_os = "android")] - if matches!(woke, Some(WEvent::Fs)) && !applied_ms - && last_full.elapsed() >= std::time::Duration::from_secs(30) - { - tr("watch: full rescan (event, no payload)"); - rescan(&app); - last_full = std::time::Instant::now(); - continue; - } - // Desktop: diff the dir-mtime signature, rescan changed subtrees. - #[cfg(not(target_os = "android"))] - { - let next = dir_sig(&scan::roots()); - let changed = if baseline { - baseline = false; - Vec::new() - } else { - changed_dirs(&sig, &next) - }; - sig = next; - if changed.is_empty() { - continue; - } + + // Fast directory mtime-diff watch (Android and desktop): catches + // out-of-band filesystem adds/removes within ~1 second without + // scanning 10k files. + let next = fast_dir_sig(&watched_dirs); + let changed = if baseline { + baseline = false; + Vec::new() + } else { + changed_dirs(&sig, &next) + }; + sig = next; + + if !changed.is_empty() { tr(&format!("watch: {} changed dir(s)", changed.len())); rescan_subtrees(&app, &changed); + watched_dirs = collect_watched_dirs(&scan::roots()); + sig = fast_dir_sig(&watched_dirs); } } }