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