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 let cli = Cli::parse();
63
64 match cli.command {
65 Commands::FsWatcher(args) => fuzz_fs_watcher(args).await,
66 }
67}
68
69#[derive(Default, NonLocalValue, TraceRawVcs)]
70struct PathInvalidations(#[turbo_tasks(trace_ignore)] Arc<Mutex<FxHashSet<RcStr>>>);
71
72async fn fuzz_fs_watcher(args: FsWatcher) -> anyhow::Result<()> {
73 std::fs::create_dir(&args.fs_root)?;
74 let fs_root = args.fs_root.canonicalize()?;
75 let _guard = FsCleanup {
76 path: &fs_root.clone(),
77 };
78
79 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
80 BackendOptions::default(),
81 noop_backing_storage(),
82 ));
83 tt.run_once(async move {
84 let invalidations = TransientInstance::new(PathInvalidations::default());
85 let fs_root_rcstr = RcStr::from(fs_root.to_str().unwrap());
86 let project_fs = disk_file_system_operation(fs_root_rcstr.clone())
87 .resolve_strongly_consistent()
88 .await?;
89 let project_root = disk_file_system_root_operation(project_fs)
90 .resolve_strongly_consistent()
91 .await?
92 .owned()
93 .await?;
94 create_directory_tree(&mut FxHashSet::default(), &fs_root, args.depth, args.width)?;
95
96 if !args.start_watching_late {
97 project_fs.await?.start_watching(None).await?;
98 }
99
100 let read_all_paths_op =
101 read_all_paths_operation(invalidations.clone(), project_root, args.depth, args.width);
102 read_all_paths_op.read_strongly_consistent().await?;
103 {
104 let mut invalidations = invalidations.0.lock().unwrap();
105 println!("read all {} files", invalidations.len());
106 invalidations.clear();
107 }
108
109 if args.start_watching_late {
110 project_fs.await?.start_watching(None).await?;
111 }
112
113 let mut rand_buf = [0; 16];
114 let mut rng = rand::rngs::SmallRng::from_rng(&mut rand::rng());
115 loop {
116 let mut modified_file_paths = FxHashSet::default();
117 for _ in 0..args.file_modifications {
118 let path = fs_root.join(pick_random_file(args.depth, args.width));
119 let mut f = OpenOptions::new().write(true).truncate(true).open(&path)?;
120 rng.fill_bytes(&mut rand_buf);
121 f.write_all(&rand_buf)?;
122 f.flush()?;
123 modified_file_paths.insert(path);
124 }
125 for _ in 0..args.directory_modifications {
126 let dir = pick_random_directory(args.depth, args.width);
127 let path = fs_root.join(dir.path);
128 std::fs::remove_dir_all(&path)?;
129 std::fs::create_dir(&path)?;
130 create_directory_tree(
131 &mut modified_file_paths,
132 &path,
133 args.depth - dir.depth,
134 args.width,
135 )?;
136 }
137 sleep(Duration::from_millis(args.notify_timeout_ms)).await;
140 read_all_paths_op.read_strongly_consistent().await?;
141 {
142 let mut invalidations = invalidations.0.lock().unwrap();
143 println!(
144 "modified {} files and found {} invalidations",
145 modified_file_paths.len(),
146 invalidations.len()
147 );
148 if args.print_missing_invalidations {
149 let absolute_path_invalidations = invalidations
150 .iter()
151 .map(|relative_path| fs_root.join(relative_path))
152 .collect::<FxHashSet<PathBuf>>();
153 let mut missing = modified_file_paths
154 .difference(&absolute_path_invalidations)
155 .collect::<Vec<_>>();
156 missing.sort_unstable();
157 for path in &missing {
158 println!(" missing {path:?}");
159 }
160 }
161 invalidations.clear();
162 }
163 }
164 })
165 .await
166}
167
168#[turbo_tasks::function(operation)]
169fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
170 DiskFileSystem::new(rcstr!("project"), fs_root)
171}
172
173#[turbo_tasks::function(operation)]
174fn disk_file_system_root_operation(fs: ResolvedVc<DiskFileSystem>) -> Vc<FileSystemPath> {
175 fs.root()
176}
177
178#[turbo_tasks::function]
179async fn read_path(
180 invalidations: TransientInstance<PathInvalidations>,
181 path: FileSystemPath,
182) -> anyhow::Result<()> {
183 let path_str = path.path.clone();
184 invalidations.0.lock().unwrap().insert(path_str);
185 let _ = path.read().await?;
186 Ok(())
187}
188
189#[turbo_tasks::function(operation)]
190async fn read_all_paths_operation(
191 invalidations: TransientInstance<PathInvalidations>,
192 root: FileSystemPath,
193 depth: usize,
194 width: usize,
195) -> anyhow::Result<()> {
196 async fn read_all_paths_inner(
197 invalidations: TransientInstance<PathInvalidations>,
198 parent: FileSystemPath,
199 depth: usize,
200 width: usize,
201 ) -> anyhow::Result<()> {
202 for child_id in 0..width {
203 let child_name = child_id.to_string();
204 let child_path = parent.join(&child_name)?;
205 if depth == 1 {
206 read_path(invalidations.clone(), child_path).await?;
207 } else {
208 Box::pin(read_all_paths_inner(
209 invalidations.clone(),
210 child_path,
211 depth - 1,
212 width,
213 ))
214 .await?;
215 }
216 }
217 Ok(())
218 }
219 read_all_paths_inner(invalidations, root, depth, width).await
220}
221
222fn create_directory_tree(
223 modified_file_paths: &mut FxHashSet<PathBuf>,
224 parent: &Path,
225 depth: usize,
226 width: usize,
227) -> anyhow::Result<()> {
228 let mut rng = rand::rng();
229 let mut rand_buf = [0; 16];
230 for child_id in 0..width {
231 let child_name = child_id.to_string();
232 let child_path = parent.join(&child_name);
233 if depth == 1 {
234 let mut f = std::fs::File::create(&child_path)?;
235 rng.fill_bytes(&mut rand_buf);
236 f.write_all(&rand_buf)?;
237 f.flush()?;
238 modified_file_paths.insert(child_path);
239 } else {
240 std::fs::create_dir(&child_path)?;
241 create_directory_tree(modified_file_paths, &child_path, depth - 1, width)?;
242 }
243 }
244 Ok(())
245}
246
247fn pick_random_file(depth: usize, width: usize) -> PathBuf {
248 let mut rng = rand::rng();
249 iter::repeat_with(|| rng.random_range(0..width).to_string())
250 .take(depth)
251 .collect()
252}
253
254struct RandomDirectory {
255 depth: usize,
256 path: PathBuf,
257}
258
259fn pick_random_directory(max_depth: usize, width: usize) -> RandomDirectory {
260 let mut rng = rand::rng();
261 let depth = rng.random_range(1..(max_depth - 1));
263 let path = iter::repeat_with(|| rng.random_range(0..width).to_string())
264 .take(depth)
265 .collect();
266 RandomDirectory { depth, path }
267}
268
269struct FsCleanup<'a> {
270 path: &'a Path,
271}
272
273impl Drop for FsCleanup<'_> {
274 fn drop(&mut self) {
275 std::fs::remove_dir_all(self.path).unwrap();
276 }
277}