gtauri: enable fast mtime-diff filesystem watcher on Android (adds & deletes reflected in ~1s)

This commit is contained in:
Greg Pomerantz 2026-11-03 08:49:06 -05:00
parent 2c03577a67
commit 15a857ed7f

View File

@ -1141,6 +1141,47 @@ fn spawn_fs_watcher(tx: mpsc::Sender<WEvent>) {
}); });
} }
fn collect_watched_dirs(roots: &[PathBuf]) -> HashSet<PathBuf> {
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<PathBuf>) -> HashMap<PathBuf, i64> {
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. /// Diff two dir-mtime signatures: dirs added, removed, or changed.
fn changed_dirs( fn changed_dirs(
old: &HashMap<PathBuf, i64>, old: &HashMap<PathBuf, i64>,
@ -1181,15 +1222,13 @@ fn watch_files(app: tauri::AppHandle) {
}); });
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
spawn_fs_watcher(tx); spawn_fs_watcher(tx);
// Desktop only: the mtime-diff machinery. On Android the MediaStore
// URI payloads ARE the change signal (exact paths, no scanning), let mut watched_dirs = collect_watched_dirs(&scan::roots());
// and diffing would cost a 10k-entry readdir every second on the let mut sig = fast_dir_sig(&watched_dirs);
// phone — pure overhead for what the payloads already deliver.
#[cfg(not(target_os = "android"))]
let mut sig: HashMap<PathBuf, i64> = HashMap::new();
#[cfg(not(target_os = "android"))]
let mut baseline = true; let mut baseline = true;
let mut last_dir_refresh = std::time::Instant::now();
let mut last_full = std::time::Instant::now(); let mut last_full = std::time::Instant::now();
loop { loop {
let woke = match rx.recv_timeout(std::time::Duration::from_secs(1)) { let woke = match rx.recv_timeout(std::time::Duration::from_secs(1)) {
Ok(e) => Some(e), Ok(e) => Some(e),
@ -1198,7 +1237,6 @@ fn watch_files(app: tauri::AppHandle) {
}; };
let do_full = matches!(woke, Some(WEvent::Full)); let do_full = matches!(woke, Some(WEvent::Full));
if let Some(WEvent::Fs) = woke { if let Some(WEvent::Fs) = woke {
// Coalesce the burst, let the write settle, coalesce again.
while rx.try_recv().is_ok() {} while rx.try_recv().is_ok() {}
std::thread::sleep(std::time::Duration::from_millis(600)); std::thread::sleep(std::time::Duration::from_millis(600));
while rx.try_recv().is_ok() {} while rx.try_recv().is_ok() {}
@ -1206,40 +1244,30 @@ fn watch_files(app: tauri::AppHandle) {
if do_full { if do_full {
tr("watch: full rescan (5min)"); tr("watch: full rescan (5min)");
rescan(&app); rescan(&app);
watched_dirs = collect_watched_dirs(&scan::roots());
sig = fast_dir_sig(&watched_dirs);
last_full = std::time::Instant::now(); last_full = std::time::Instant::now();
continue; 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 // MediaStore event path (Android): MainActivity drops a one-line
// payload per changed URI and wakes us via nativeFsChange. // payload per changed URI and wakes us via nativeFsChange.
// Consume on EVERY tick (single read_dir of our own app dir — let _applied_ms = if RESCANNING.load(std::sync::atomic::Ordering::Relaxed) {
// 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) {
false false
} else { } else {
apply_mediasotre_payload(&app) apply_mediasotre_payload(&app)
}; };
// Android fallback: the OS said "media changed" but no payload
// arrived (permission missing, scanner gap, or the payload was // Fast directory mtime-diff watch (Android and desktop): catches
// parked behind a rescan). Full rescan, throttled — the // out-of-band filesystem adds/removes within ~1 second without
// degraded path, not the common one. Parking behind a rescan // scanning 10k files.
// also counts as "not applied": the rescan's next 1 s tick let next = fast_dir_sig(&watched_dirs);
// 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 { let changed = if baseline {
baseline = false; baseline = false;
Vec::new() Vec::new()
@ -1247,11 +1275,12 @@ fn watch_files(app: tauri::AppHandle) {
changed_dirs(&sig, &next) changed_dirs(&sig, &next)
}; };
sig = next; sig = next;
if changed.is_empty() {
continue; if !changed.is_empty() {
}
tr(&format!("watch: {} changed dir(s)", changed.len())); tr(&format!("watch: {} changed dir(s)", changed.len()));
rescan_subtrees(&app, &changed); rescan_subtrees(&app, &changed);
watched_dirs = collect_watched_dirs(&scan::roots());
sig = fast_dir_sig(&watched_dirs);
} }
} }
} }