1use std::{
2 fs::OpenOptions,
3 io::Write,
4 iter,
5 path::{Path, PathBuf},
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10use clap::{Args, Parser, Subcommand};
11use rand::{Rng, RngCore, SeedableRng};
12use rustc_hash::FxHashSet;
13use tokio::time::sleep;
14use turbo_rcstr::{RcStr, rcstr};
15use turbo_tasks::{NonLocalValue, ResolvedVc, TransientInstance, Vc, trace::TraceRawVcs};
16use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
17use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath};
18
19#[derive(Parser)]
27#[command()]
28struct Cli {
29 #[command(subcommand)]
30 command: Commands,
31}
32
33#[derive(Subcommand)]
34enum Commands {
35 FsWatcher(FsWatcher),
37}
38
39#[derive(Args)]
40struct FsWatcher {
41 #[arg(long)]
42 fs_root: PathBuf,
43 #[arg(long, default_value_t = 4)]
44 depth: usize,
45 #[arg(long, default_value_t = 6)]
46 width: usize,
47 #[arg(long, default_value_t = 100)]
48 notify_timeout_ms: u64,
49 #[arg(long, default_value_t = 200)]
50 file_modifications: u32,
51 #[arg(long, default_value_t = 2)]
52 directory_modifications: u32,
53 #[arg(long)]
54 print_missing_invalidations: bool,
55 #[arg(long)]
57 start_watching_late: bool,
58}
59
60#[tokio::main]
61async fn main() -> anyhow::Result<()> {
62 register();
63 let cli = Cli::parse();
64
65 match cli.command {
66 Commands::FsWatcher(args) => fuzz_fs_watcher(args).await,
67 }
68}
69
70#[derive(Default, NonLocalValue, TraceRawVcs)]
71struct PathInvalidations(#[turbo_tasks(trace_ignore)] Arc<Mutex<FxHashSet<RcStr>>>);
72
73async fn fuzz_fs_watcher(args: FsWatcher) -> anyhow::Result<()> {
74 std::fs::create_dir(&args.fs_root)?;
75 let fs_root = args.fs_root.canonicalize()?;
76 let _guard = FsCleanup {
77 path: &fs_root.clone(),
78 };
79
80 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
81 BackendOptions::default(),
82 noop_backing_storage(),
83 ));
84 tt.run_once(async move {
85 let invalidations = TransientInstance::new(PathInvalidations::default());
86 let fs_root_rcstr = RcStr::from(fs_root.to_str().unwrap());
87 let project_fs = disk_file_system_operation(fs_root_rcstr.clone())
88 .resolve_strongly_consistent()
89 .await?;
90 let project_root = disk_file_system_root_operation(project_fs)
91 .resolve_strongly_consistent()
92 .await?
93 .owned()
94 .await?;
95 create_directory_tree(&mut FxHashSet::default(), &fs_root, args.depth, args.width)?;
96
97 if !args.start_watching_late {
98 project_fs.await?.start_watching(None).await?;
99 }
100
101 let read_all_paths_op =
102 read_all_paths_operation(invalidations.clone(), project_root, args.depth, args.width);
103 read_all_paths_op.read_strongly_consistent().await?;
104 {
105 let mut invalidations = invalidations.0.lock().unwrap();
106 println!("read all {} files", invalidations.len());
107 invalidations.clear();
108 }
109
110 if args.start_watching_late {
111 project_fs.await?.start_watching(None).await?;
112 }
113
114 let mut rand_buf = [0; 16];
115 let mut rng = rand::rngs::SmallRng::from_rng(&mut rand::rng());
116 loop {
117 let mut modified_file_paths = FxHashSet::default();
118 for _ in 0..args.file_modifications {
119 let path = fs_root.join(pick_random_file(args.depth, args.width));
120 let mut f = OpenOptions::new().write(true).truncate(true).open(&path)?;
121 rng.fill_bytes(&mut rand_buf);
122 f.write_all(&rand_buf)?;
123 f.flush()?;
124 modified_file_paths.insert(path);
125 }
126 for _ in 0..args.directory_modifications {
127 let dir = pick_random_directory(args.depth, args.width);
128 let path = fs_root.join(dir.path);
129 std::fs::remove_dir_all(&path)?;
130 std::fs::create_dir(&path)?;
131 create_directory_tree(
132 &mut modified_file_paths,
133 &path,
134 args.depth - dir.depth,
135 args.width,
136 )?;
137 }
138 sleep(Duration::from_millis(args.notify_timeout_ms)).await;
141 read_all_paths_op.read_strongly_consistent().await?;
142 {
143 let mut invalidations = invalidations.0.lock().unwrap();
144 println!(
145 "modified {} files and found {} invalidations",
146 modified_file_paths.len(),
147 invalidations.len()
148 );
149 if args.print_missing_invalidations {
150 let absolute_path_invalidations = invalidations
151 .iter()
152 .map(|relative_path| fs_root.join(relative_path))
153 .collect::<FxHashSet<PathBuf>>();
154 let mut missing = modified_file_paths
155 .difference(&absolute_path_invalidations)
156 .collect::<Vec<_>>();
157 missing.sort_unstable();
158 for path in &missing {
159 println!(" missing {path:?}");
160 }
161 }
162 invalidations.clear();
163 }
164 }
165 })
166 .await
167}
168
169#[turbo_tasks::function(operation)]
170fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
171 DiskFileSystem::new(rcstr!("project"), fs_root)
172}
173
174#[turbo_tasks::function(operation)]
175fn disk_file_system_root_operation(fs: ResolvedVc<DiskFileSystem>) -> Vc<FileSystemPath> {
176 fs.root()
177}
178
179#[turbo_tasks::function]
180async fn read_path(
181 invalidations: TransientInstance<PathInvalidations>,
182 path: FileSystemPath,
183) -> anyhow::Result<()> {
184 let path_str = path.path.clone();
185 invalidations.0.lock().unwrap().insert(path_str);
186 let _ = path.read().await?;
187 Ok(())
188}
189
190#[turbo_tasks::function(operation)]
191async fn read_all_paths_operation(
192 invalidations: TransientInstance<PathInvalidations>,
193 root: FileSystemPath,
194 depth: usize,
195 width: usize,
196) -> anyhow::Result<()> {
197 async fn read_all_paths_inner(
198 invalidations: TransientInstance<PathInvalidations>,
199 parent: FileSystemPath,
200 depth: usize,
201 width: usize,
202 ) -> anyhow::Result<()> {
203 for child_id in 0..width {
204 let child_name = child_id.to_string();
205 let child_path = parent.join(&child_name)?;
206 if depth == 1 {
207 read_path(invalidations.clone(), child_path).await?;
208 } else {
209 Box::pin(read_all_paths_inner(
210 invalidations.clone(),
211 child_path,
212 depth - 1,
213 width,
214 ))
215 .await?;
216 }
217 }
218 Ok(())
219 }
220 read_all_paths_inner(invalidations, root, depth, width).await
221}
222
223fn create_directory_tree(
224 modified_file_paths: &mut FxHashSet<PathBuf>,
225 parent: &Path,
226 depth: usize,
227 width: usize,
228) -> anyhow::Result<()> {
229 let mut rng = rand::rng();
230 let mut rand_buf = [0; 16];
231 for child_id in 0..width {
232 let child_name = child_id.to_string();
233 let child_path = parent.join(&child_name);
234 if depth == 1 {
235 let mut f = std::fs::File::create(&child_path)?;
236 rng.fill_bytes(&mut rand_buf);
237 f.write_all(&rand_buf)?;
238 f.flush()?;
239 modified_file_paths.insert(child_path);
240 } else {
241 std::fs::create_dir(&child_path)?;
242 create_directory_tree(modified_file_paths, &child_path, depth - 1, width)?;
243 }
244 }
245 Ok(())
246}
247
248fn pick_random_file(depth: usize, width: usize) -> PathBuf {
249 let mut rng = rand::rng();
250 iter::repeat_with(|| rng.random_range(0..width).to_string())
251 .take(depth)
252 .collect()
253}
254
255struct RandomDirectory {
256 depth: usize,
257 path: PathBuf,
258}
259
260fn pick_random_directory(max_depth: usize, width: usize) -> RandomDirectory {
261 let mut rng = rand::rng();
262 let depth = rng.random_range(1..(max_depth - 1));
264 let path = iter::repeat_with(|| rng.random_range(0..width).to_string())
265 .take(depth)
266 .collect();
267 RandomDirectory { depth, path }
268}
269
270struct FsCleanup<'a> {
271 path: &'a Path,
272}
273
274impl Drop for FsCleanup<'_> {
275 fn drop(&mut self) {
276 std::fs::remove_dir_all(self.path).unwrap();
277 }
278}
279
280fn register() {
281 turbo_tasks::register();
282 turbo_tasks_fs::register();
283 include!(concat!(env!("OUT_DIR"), "/register.rs"));
284}