Created
April 2, 2026 05:46
-
-
Save stormslowly/ed758500de6f23211fd63b39eba5ed07 to your computer and use it in GitHub Desktop.
Repro: FSEvents stale event drain timing test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [package] | |
| name = "fsevents-repro" | |
| version = "0.1.0" | |
| edition = "2021" | |
| [dependencies] | |
| notify = { version = "8", default-features = false, features = ["macos_fsevent"] } | |
| tempfile = "3" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// 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