Skip to content

Instantly share code, notes, and snippets.

@stormslowly
Created April 2, 2026 05:46
Show Gist options
  • Select an option

  • Save stormslowly/ed758500de6f23211fd63b39eba5ed07 to your computer and use it in GitHub Desktop.

Select an option

Save stormslowly/ed758500de6f23211fd63b39eba5ed07 to your computer and use it in GitHub Desktop.
Repro: FSEvents stale event drain timing test
[package]
name = "fsevents-repro"
version = "0.1.0"
edition = "2021"
[dependencies]
notify = { version = "8", default-features = false, features = ["macos_fsevent"] }
tempfile = "3"
/// Tests: how long must we wait after watcher creation before copyDiff(step1)
/// so that stale FSEvents are fully drained and won't merge with step1 events?
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::{fs, thread};
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
use tempfile::TempDir;
fn trial_with_delay(delay_ms: u64) -> bool {
let tmp = TempDir::new().unwrap();
let index_js = tmp.path().join("index.js");
let foo_js = tmp.path().join("foo.js");
let origin = Instant::now();
fs::write(&index_js, b"export default 'index'").unwrap();
fs::write(&foo_js, b"export const v = 'foo'").unwrap();
let events: Arc<Mutex<Vec<(Duration, String)>>> = Arc::new(Mutex::new(Vec::new()));
let events_clone = events.clone();
let mut watcher = RecommendedWatcher::new(
move |res: notify::Result<Event>| {
let elapsed = origin.elapsed();
if let Ok(event) = res {
let mut evts = events_clone.lock().unwrap();
for path in &event.paths {
if let Some(name) = path.file_name() {
let name = name.to_string_lossy().into_owned();
if name == "index.js" || name == "foo.js" {
evts.push((elapsed, name));
}
}
}
}
},
notify::Config::default(),
)
.unwrap();
watcher.watch(tmp.path(), RecursiveMode::Recursive).unwrap();
// Wait the specified delay, then clear all accumulated events (simulating aggregate flush)
thread::sleep(Duration::from_millis(delay_ms));
events.lock().unwrap().clear();
// Now edit only foo.js (like copyDiff step1)
fs::write(&foo_js, b"export const v = 'fooo'").unwrap();
thread::sleep(Duration::from_millis(300));
drop(watcher);
// After clearing + step1, did we still see index.js? That's a stale leak.
let leaked = events.lock().unwrap().iter().any(|(_, name)| name == "index.js");
leaked
}
fn main() {
let trials = 200;
for delay_ms in [0, 10, 20, 50, 100, 200, 500] {
let mut leaked = 0;
for _ in 0..trials {
if trial_with_delay(delay_ms) {
leaked += 1;
}
}
println!("delay={delay_ms:>4}ms → stale leak: {leaked:>3}/{trials}");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment