From 9f7936c3c65fee7363dc2e87cef7658c2e2b08b9 Mon Sep 17 00:00:00 2001 From: mpratsaki <110930793+mpratsaki@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:32:15 +0200 Subject: [PATCH] LRC-LIB-MISSING: a slow count's timed refresh doesn't hold up settling While a headless run settles, its simulated clock runs several times faster than wall time, so with every Missing Photos count pending, a count that takes about a second started the next 5 s refresh before the last one finished and settle() never ended (120 s timeout with a 1 s count, 48 s with 600 ms). That would also stall lightcraft-cli snapshot on a library on a slow share. Only the first count and a recount after a catalog change, which add or take away the Missing Photos row, are pending work now; the timed refresh is not. Settling takes as long as the count (1.0 s for 1 s, 3.0 s for 3 s). The regression test also checks that settling ends while a slow count refreshes. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/left/sections.rs | 22 +++++++++++++++------- crates/ui-egui/src/tests/panels.rs | 3 +++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/panels/left/sections.rs b/crates/ui-egui/src/panels/left/sections.rs index 16a4d652c..7c59cf845 100644 --- a/crates/ui-egui/src/panels/left/sections.rs +++ b/crates/ui-egui/src/panels/left/sections.rs @@ -381,9 +381,11 @@ fn new_album_menu(app: &mut LightcraftApp, ui: &mut egui::Ui) { /// How many library photos have no file (Local browse records are not checked; see /// `cmd::missing::checked_path`). Checking stats every file, which on a network share takes /// seconds, so it runs on a worker thread: the count shown is the last finished one, refreshed -/// at most every 5 s, and at once (after the running check) when the catalog changed. The check -/// counts as pending sidebar work ([`super::fs_cached_running`]): the Missing Photos row it adds -/// moves every row below it. +/// at most every 5 s, and at once (after the running check) when the catalog changed. The first +/// count and a recount after a catalog change are pending sidebar work +/// ([`super::fs_cached_running`]): the Missing Photos row they add or take away moves every row +/// below it. The timed refresh is not: it rarely changes the count, and on a share where counting +/// takes seconds, a headless run's fast simulated clock would start the next one before settling. pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize { use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::AcqRel, Ordering::Relaxed}; #[derive(Clone, Default)] @@ -401,11 +403,13 @@ pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize if !fresh && !job.running.load(Relaxed) { // the same scope as the Missing Photos view: library photos only, never Local browse records let paths = lightcraft_engine::cmd::missing::candidates(&app.session.catalog); + let pending = (job.rev != rev).then(|| super::local::fs_running_counter(ui.ctx())); + if let Some(p) = &pending { + p.fetch_add(1, AcqRel); + } job.running.store(true, Relaxed); job.at = now; job.rev = rev; - let pending = super::local::fs_running_counter(ui.ctx()); - pending.fetch_add(1, AcqRel); #[cfg(test)] let test_delay = app.missing_count_test_delay; let (n, running, ctx, done) = (job.n.clone(), job.running.clone(), ui.ctx().clone(), pending.clone()); @@ -418,14 +422,18 @@ pub(super) fn missing_count(app: &mut LightcraftApp, ui: &mut egui::Ui) -> usize n.store(missing, Relaxed); running.store(false, Relaxed); // after the count is stored: a frame that sees the pending count drop also sees the row - done.fetch_sub(1, AcqRel); + if let Some(d) = done { + d.fetch_sub(1, AcqRel); + } ctx.request_repaint(); }; #[cfg(not(target_arch = "wasm32"))] if std::thread::Builder::new().name("lc-missing-count".into()).spawn(work).is_err() { // no thread to spare: keep the last count and try again after the next 5 s job.running.store(false, Relaxed); - pending.fetch_sub(1, AcqRel); + if let Some(p) = &pending { + p.fetch_sub(1, AcqRel); + } } #[cfg(target_arch = "wasm32")] work(); diff --git a/crates/ui-egui/src/tests/panels.rs b/crates/ui-egui/src/tests/panels.rs index 79b8267fd..bc37df9fc 100644 --- a/crates/ui-egui/src/tests/panels.rs +++ b/crates/ui-egui/src/tests/panels.rs @@ -1883,6 +1883,9 @@ fn settling_waits_for_the_missing_photos_count() { std::thread::sleep(Duration::from_millis(5)); } assert_eq!(widget(&h, "source:libfolder:/lc-missing-count/trip"), settled, "nothing moves the folder rows after settling"); + // the timed refresh of a slow count is not waited for: the fast simulated clock would start + // the next one before the last finished, and settling would never end + assert!(h.settle(Duration::from_secs(30)), "the sidebar settles while a slow count refreshes"); } /// Right-click a folder ▸ Set Color Label ▸ a colour: the folder's row shows the label's dot (a