authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 19:34:43-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 20:10:08-07:00
log3aa700c6c057e00e4eaf633161e28c5611b8b687
tree0fb67ca31c04f78dbcf41f099821730c65996704
parent08d400c2cd4212f7438a51ea0993a4fe19944a62
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: a replay settles only once every event sent before it went quiet has been taken

Two quiet frames were not enough under load: ticks queued ahead of a finished thread's event, so the iCloud folder's notebooks could list after the tree was written. The window now sends itself a mark on finding nothing on its way and settles when that mark arrives still quiet, and looking up the iCloud folder counts as work on its way. Assisted-by: claude-opus-5.5

2 files changed, 40 insertions(+), 20 deletions(-)

crates/snowbound/src/icloud_macos.rs+3-2
......@@ -90,9 +90,10 @@ pub fn folder() -> Option<PathBuf> {
9090
9191/// Looks for the app's own folder in iCloud Drive on a thread of its own, as asking the
9292/// iCloud daemon can take a while, then calls `changed`, and again whenever a folder at its
93/// top comes or goes, as another device adds or removes a notebook.
93/// top comes or goes, as another device adds or removes a notebook. A replay settles only
94/// once it has looked.
9495pub fn look_up(changed: impl Fn() + Send + Sync + 'static) {
95 std::thread::spawn(move || {
96 crate::spawn(move || {
9697 let changed = std::sync::Arc::new(changed);
9798 let found = container().map(|root| {
9899 let report = std::sync::Arc::clone(&changed);
crates/snowbound/src/main.rs+37-18
......@@ -378,6 +378,9 @@ enum Replay {
378378 Settle(Option<PathBuf>, std::sync::mpsc::Sender<()>),
379379 /// A frame during a wait, as a visible window's display would ask for.
380380 Tick,
381 /// What the window sent itself when it found nothing on its way, at that instant: once it
382 /// arrives, so has every event sent before.
383 Mark(Instant),
381384 Appearance(winit::window::Theme),
382385 /// Resizes the window's content, in points.
383386 Resize([f32; 2]),
......@@ -915,10 +918,13 @@ struct State {
915918 zoom_typed: Option<String>,
916919 /// Where a replay asked the next frame to be written.
917920 snapshot: Option<PathBuf>,
918 /// A replay waiting for nothing to be on its way, and where it wants the accessibility
919 /// tree written then.
920 /// Whether the last frame found the window quiet too, which the tree waits for.
921 replay_settle: Option<(Option<PathBuf>, std::sync::mpsc::Sender<()>, bool)>,
921 /// A replay waiting for nothing to be on its way, where it wants the accessibility tree
922 /// written then, and the mark sent since the window was last found busy.
923 replay_settle: Option<(
924 Option<PathBuf>,
925 std::sync::mpsc::Sender<()>,
926 Option<Instant>,
927 )>,
922928 /// `SNOWBOUND_FRAMES`: a directory every frame drawn is also written to, named by
923929 /// milliseconds since the window opened.
924930 frames: Option<(PathBuf, Instant)>,
......@@ -5841,7 +5847,8 @@ fn spawn(work: impl FnOnce() + Send + 'static) {
58415847 RUNNING.fetch_add(1, Ordering::Relaxed);
58425848 let work = move || {
58435849 work();
5844 RUNNING.fetch_sub(1, Ordering::Relaxed);
5850 // Whatever `work` sent is in the queue before the count shows it done.
5851 RUNNING.fetch_sub(1, Ordering::Release);
58455852 };
58465853 #[cfg(not(target_arch = "wasm32"))]
58475854 std::thread::spawn(work);
......@@ -5919,6 +5926,10 @@ impl State {
59195926 }
59205927 UserEvent::Redraw => self.window.request_redraw(),
59215928 UserEvent::Replay(replay) => {
5929 let mark = match replay {
5930 Replay::Mark(at) => Some(at),
5931 _ => None,
5932 };
59225933 match replay {
59235934 Replay::Input(event) => self.input(event),
59245935 Replay::Pinch(factor) => {
......@@ -5928,9 +5939,9 @@ impl State {
59285939 }
59295940 Replay::Snapshot(path) => self.snapshot = Some(path),
59305941 Replay::Settle(path, settled) => {
5931 self.replay_settle = Some((path, settled, false));
5942 self.replay_settle = Some((path, settled, None));
59325943 }
5933 Replay::Tick | Replay::Quit => {}
5944 Replay::Tick | Replay::Mark(_) | Replay::Quit => {}
59345945 Replay::Appearance(appearance) => {
59355946 self.window.set_theme(Some(appearance));
59365947 self.set_appearance(appearance);
......@@ -5945,21 +5956,29 @@ impl State {
59455956 if let Err(error) = self.frame() {
59465957 eprintln!("{error}");
59475958 }
5948 // Quiet on two frames apart, what a thread sent as it ended has been taken.
5959 // Quiet still when the mark sent on finding it quiet arrives, what a thread sent
5960 // as it ended has been taken, however many frames wait in the queue before it.
59495961 let quiet = self.settled()
59505962 && self.opening.is_none()
5951 && RUNNING.load(Ordering::Relaxed) == 0
5963 && RUNNING.load(Ordering::Acquire) == 0
59525964 && self.search.pending.load(Ordering::Relaxed) == 0;
5953 if let Some((path, settled, was)) = self.replay_settle.take() {
5954 if !(quiet && was) {
5955 self.replay_settle = Some((path, settled, quiet));
5956 } else {
5957 if let Some(path) = path
5958 && let Err(error) = self.write_accessibility(&path)
5959 {
5960 eprintln!("{error}");
5965 if let Some((path, settled, sent)) = self.replay_settle.take() {
5966 match (quiet, sent) {
5967 (true, Some(sent)) if mark == Some(sent) => {
5968 if let Some(path) = path
5969 && let Err(error) = self.write_accessibility(&path)
5970 {
5971 eprintln!("{error}");
5972 }
5973 let _ = settled.send(());
5974 }
5975 (true, None) => {
5976 let now = Instant::now();
5977 let _ = self.proxy.send_event(UserEvent::Replay(Replay::Mark(now)));
5978 self.replay_settle = Some((path, settled, Some(now)));
59615979 }
5962 let _ = settled.send(());
5980 (true, sent) => self.replay_settle = Some((path, settled, sent)),
5981 (false, _) => self.replay_settle = Some((path, settled, None)),
59635982 }
59645983 }
59655984 }