From bf82b57a0a8eee7bbfa2c380a8ed8ff675525571 Mon Sep 17 00:00:00 2001 From: Alexander Bystrow <2510233+alexbystrow@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:46:30 +0200 Subject: [PATCH 1/2] LR-IMP-LOCAL: Opening on Missing Photos no longer checks every original Forgetting untouched Local records on open kept the visible photos, and on Missing Photos working them out stats every original on the UI thread before the availability worker runs. A 37k-photo library on a sleeping SMB share froze the window for minutes. Missing Photos lists library photos only, never Local records, so it can keep nothing: skip it. --- crates/engine/src/cmd/browse.rs | 6 ++++- crates/engine/src/tests_forget_local.rs | 35 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/browse.rs b/crates/engine/src/cmd/browse.rs index 655965688..4f83c77fc 100644 --- a/crates/engine/src/cmd/browse.rs +++ b/crates/engine/src/cmd/browse.rs @@ -148,7 +148,11 @@ fn stamp_browsed(s: &mut Session, root: &str, files: &[String]) { /// referred to by an undo / redo step. fn in_use(s: &mut Session) -> std::collections::HashSet { let mut keep = held(s); - keep.extend(s.visible().iter().copied()); + // Missing Photos lists library photos only, never Local records, so it keeps nothing here; and + // working it out checks every original (minutes on a sleeping NAS when the library opens on it) + if s.source != crate::LibrarySource::Missing { + keep.extend(s.visible().iter().copied()); + } keep } diff --git a/crates/engine/src/tests_forget_local.rs b/crates/engine/src/tests_forget_local.rs index 229b262a0..439e2a285 100644 --- a/crates/engine/src/tests_forget_local.rs +++ b/crates/engine/src/tests_forget_local.rs @@ -259,3 +259,38 @@ fn browsing_again_forgets_untouched_records_of_deleted_files() { assert_eq!(s.catalog.to_snapshot(), expect); let _ = std::fs::remove_dir_all(&root); } + +/// A library last shown on Missing Photos opens without checking its originals: that view holds +/// no Local records, so forgetting them needn't work it out (each check can wait for a sleeping +/// NAS; a big library on one froze the window for minutes on start-up). +#[test] +fn opening_on_missing_photos_checks_no_originals() { + let root = temp_dir("missing"); + let (a, lib) = (root.join("a"), root.join("lib")); + for i in 0..5 { + png(&a.join(format!("m{i}.png")), i); + } + let clock = Arc::new(Mutex::new("2026-08-01T10:00:00".to_string())); + let mut s = open(&lib, &clock); + s.execute("library.import", &json!({"paths": [a.to_string_lossy()], "mode": "add"})).unwrap(); + std::fs::remove_dir_all(&a).unwrap(); + s.execute("library.source", &json!({"kind": "missing"})).unwrap(); + assert_eq!(s.visible().len(), 5, "all five are missing"); + s.close_library().unwrap(); + drop(s); + + let checks = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let mut s = Session::new().with_fs(); + let c = clock.clone(); + s.clock = Box::new(move || c.lock().unwrap().clone()); + let n = checks.clone(); + s.media.availability.set_probe(Arc::new(move |p| { + n.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Path::new(p).exists() + })); + s.open_library(&lib, false).unwrap(); + assert_eq!(s.source, LibrarySource::Missing, "the view is restored"); + assert_eq!(checks.load(std::sync::atomic::Ordering::SeqCst), 0, "opening checked originals"); + assert!(s.library.as_ref().unwrap().forgot_local.is_some(), "forgetting still ran"); + let _ = std::fs::remove_dir_all(&root); +} From 085d18d69248fff65aa9045593299976a65c29e0 Mon Sep 17 00:00:00 2001 From: Alexander Bystrow <2510233+alexbystrow@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:54:33 +0200 Subject: [PATCH 2/2] LR-IMP-LOCAL: Opening on Missing Photos picks no photo from it either Review round 1: with no active photo saved (an empty Missing view leaves none), open_stores worked the view out to pick its first photo, with the same synchronous check of every original. Skip that on Missing Photos; the test now opens both an empty and a full Missing view. --- crates/engine/src/library.rs | 3 ++ crates/engine/src/tests_forget_local.rs | 52 +++++++++++++++++-------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/library.rs b/crates/engine/src/library.rs index 51655aea5..4d37fa6a2 100644 --- a/crates/engine/src/library.rs +++ b/crates/engine/src/library.rs @@ -552,7 +552,10 @@ impl Session { self.selection.ids.retain(|id| self.catalog.photo(*id).is_some()); self.selection.active = self.selection.active.filter(|id| self.catalog.photo(*id).is_some()); } + // not on Missing Photos: working that view out checks every original (minutes on a + // sleeping NAS), and the app checks them in the background once it runs if self.selection.active.is_none() + && self.source != LibrarySource::Missing && let Some(first) = self.visible_cloned().first() { self.selection = Selection::single(*first); diff --git a/crates/engine/src/tests_forget_local.rs b/crates/engine/src/tests_forget_local.rs index 439e2a285..9f5d1f572 100644 --- a/crates/engine/src/tests_forget_local.rs +++ b/crates/engine/src/tests_forget_local.rs @@ -260,9 +260,24 @@ fn browsing_again_forgets_untouched_records_of_deleted_files() { let _ = std::fs::remove_dir_all(&root); } +/// Opens `lib` with a probe that counts the originals it checks. +fn open_counting(lib: &Path, clock: &Arc>) -> (Session, Arc) { + let checks = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let mut s = Session::new().with_fs(); + let c = clock.clone(); + s.clock = Box::new(move || c.lock().unwrap().clone()); + let n = checks.clone(); + s.media.availability.set_probe(Arc::new(move |p| { + n.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Path::new(p).exists() + })); + s.open_library(lib, false).unwrap(); + (s, checks) +} + /// A library last shown on Missing Photos opens without checking its originals: that view holds -/// no Local records, so forgetting them needn't work it out (each check can wait for a sleeping -/// NAS; a big library on one froze the window for minutes on start-up). +/// no Local records, so forgetting them needn't work it out, and no photo is picked from it (each +/// check can wait for a sleeping NAS; a big library on one froze the window for minutes). #[test] fn opening_on_missing_photos_checks_no_originals() { let root = temp_dir("missing"); @@ -273,24 +288,29 @@ fn opening_on_missing_photos_checks_no_originals() { let clock = Arc::new(Mutex::new("2026-08-01T10:00:00".to_string())); let mut s = open(&lib, &clock); s.execute("library.import", &json!({"paths": [a.to_string_lossy()], "mode": "add"})).unwrap(); - std::fs::remove_dir_all(&a).unwrap(); s.execute("library.source", &json!({"kind": "missing"})).unwrap(); - assert_eq!(s.visible().len(), 5, "all five are missing"); + // every original is there: the view is empty and nothing is selected + assert!(s.visible().is_empty()); + assert!(s.selection.active.is_none()); s.close_library().unwrap(); drop(s); - - let checks = Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let mut s = Session::new().with_fs(); - let c = clock.clone(); - s.clock = Box::new(move || c.lock().unwrap().clone()); - let n = checks.clone(); - s.media.availability.set_probe(Arc::new(move |p| { - n.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - Path::new(p).exists() - })); - s.open_library(&lib, false).unwrap(); + let (s, checks) = open_counting(&lib, &clock); assert_eq!(s.source, LibrarySource::Missing, "the view is restored"); - assert_eq!(checks.load(std::sync::atomic::Ordering::SeqCst), 0, "opening checked originals"); + assert_eq!(checks.load(std::sync::atomic::Ordering::SeqCst), 0, "opening an empty Missing view checked originals"); assert!(s.library.as_ref().unwrap().forgot_local.is_some(), "forgetting still ran"); + drop(s); + + // the originals go: the view lists them, one is active + std::fs::remove_dir_all(&a).unwrap(); + let mut s = open(&lib, &clock); + s.execute("library.source", &json!({"kind": "all"})).unwrap(); + s.execute("library.source", &json!({"kind": "missing"})).unwrap(); + assert_eq!(s.visible().len(), 5, "all five are missing"); + assert!(s.selection.active.is_some()); + s.close_library().unwrap(); + drop(s); + let (s, checks) = open_counting(&lib, &clock); + assert_eq!(s.source, LibrarySource::Missing); + assert_eq!(checks.load(std::sync::atomic::Ordering::SeqCst), 0, "opening a full Missing view checked originals"); let _ = std::fs::remove_dir_all(&root); }