turbo_tasks_fuzz/
symlink_stress.rs1use std::{
2 path::{Path, PathBuf},
3 time::{Duration, Instant},
4};
5
6const PROGRESS_INTERVAL: Duration = Duration::from_secs(1);
7
8use clap::Args;
9use rand::{RngExt, SeedableRng};
10use turbo_rcstr::{RcStr, rcstr};
11use turbo_tasks::{
12 Effects, OperationVc, ResolvedVc, TryJoinIterExt, Vc,
13 read_strongly_consistent_and_apply_effects, take_effects,
14};
15use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
16use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath, LinkContent, LinkType};
17
18#[derive(Args)]
19pub struct SymlinkStress {
20 #[arg(long)]
21 fs_root: PathBuf,
22 #[arg(long, default_value_t = 20)]
24 target_count: usize,
25 #[arg(long, default_value_t = 50)]
27 symlink_count: usize,
28 #[arg(long, default_value_t = 16)]
30 parallelism: usize,
31 #[arg(long, default_value_t = 5)]
33 duration_secs: u64,
34}
35
36#[turbo_tasks::function(operation, root)]
37async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
38 let _ = op.resolve().strongly_consistent().await?;
39 Ok(take_effects(op).await?.cell())
40}
41
42pub async fn run(args: SymlinkStress) -> anyhow::Result<()> {
43 std::fs::create_dir(&args.fs_root)?;
44 let fs_root = args.fs_root.canonicalize()?;
45 let _guard = FsCleanup {
46 path: &fs_root.clone(),
47 };
48
49 let targets_dir = fs_root.join("_targets");
51 std::fs::create_dir(&targets_dir)?;
52 for i in 0..args.target_count {
53 std::fs::create_dir(targets_dir.join(i.to_string()))?;
54 }
55
56 let symlinks_dir = fs_root.join("_symlinks");
58 std::fs::create_dir(&symlinks_dir)?;
59
60 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
61 BackendOptions::default(),
62 noop_backing_storage(),
63 ));
64
65 let target_count = args.target_count;
66 let symlink_count = args.symlink_count;
67 let parallelism = args.parallelism;
68 let duration = Duration::from_secs(args.duration_secs);
69
70 tt.run_once(async move {
71 let project_fs = disk_file_system_operation(RcStr::from(fs_root.to_str().unwrap()))
72 .resolve()
73 .strongly_consistent()
74 .await?;
75 let project_root = disk_file_system_root_operation(project_fs)
76 .resolve()
77 .strongly_consistent()
78 .await?
79 .owned()
80 .await?;
81
82 let symlinks_path = project_root.join("_symlinks")?;
84 let initial_target = RcStr::from("../_targets/0");
85
86 println!("creating {symlink_count} initial symlinks...");
87
88 read_strongly_consistent_and_apply_effects(
89 extract_effects_operation(create_initial_symlinks_operation(
90 symlinks_path.clone(),
91 symlink_count,
92 initial_target,
93 )),
94 |e| e,
95 )
96 .await?;
97
98 println!(
99 "starting stress test with parallelism={} for {}s...",
100 parallelism,
101 duration.as_secs()
102 );
103
104 let mut rng = rand::rngs::SmallRng::from_rng(&mut rand::rng());
105 let mut total_writes: u64 = 0;
106 let mut last_progress_writes: u64 = 0;
107 let start_time = Instant::now();
108 let mut last_progress_time = start_time;
109
110 loop {
111 if start_time.elapsed() >= duration {
113 break;
114 }
115
116 let updates: Vec<(usize, usize)> = (0..parallelism)
118 .map(|_| {
119 let symlink_idx = rng.random_range(0..symlink_count);
120 let target_idx = rng.random_range(0..target_count);
121 (symlink_idx, target_idx)
122 })
123 .collect();
124
125 read_strongly_consistent_and_apply_effects(
127 extract_effects_operation(write_symlinks_batch_operation(
128 symlinks_path.clone(),
129 updates,
130 )),
131 |e| e,
132 )
133 .await?;
134
135 total_writes += parallelism as u64;
136
137 let now = Instant::now();
139 if now.duration_since(last_progress_time) >= PROGRESS_INTERVAL {
140 let interval_writes = total_writes - last_progress_writes;
141 let interval_duration = now.duration_since(last_progress_time);
142 let writes_per_sec = interval_writes as f64 / interval_duration.as_secs_f64();
143 println!(
144 "{:.1}s: {} writes, {:.0} writes/sec",
145 start_time.elapsed().as_secs_f64(),
146 total_writes,
147 writes_per_sec
148 );
149 last_progress_time = now;
150 last_progress_writes = total_writes;
151 }
152 }
153
154 let elapsed = start_time.elapsed();
156 let writes_per_sec = total_writes as f64 / elapsed.as_secs_f64();
157 println!(
158 "completed {} symlink writes in {:.2}s ({:.0} writes/sec)",
159 total_writes,
160 elapsed.as_secs_f64(),
161 writes_per_sec
162 );
163
164 Ok(())
165 })
166 .await?;
167
168 tt.stop_and_wait().await;
169 Ok(())
170}
171
172#[turbo_tasks::function(operation)]
173fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
174 DiskFileSystem::new(rcstr!("project"), Vc::cell(fs_root))
175}
176
177#[turbo_tasks::function(operation)]
178fn disk_file_system_root_operation(fs: ResolvedVc<DiskFileSystem>) -> Vc<FileSystemPath> {
179 fs.root()
180}
181
182#[turbo_tasks::function(operation)]
183async fn create_initial_symlinks_operation(
184 symlinks_dir: FileSystemPath,
185 count: usize,
186 target: RcStr,
187) -> anyhow::Result<()> {
188 (0..count)
189 .map(|i| write_symlink(symlinks_dir.clone(), i, target.clone()))
190 .try_join()
191 .await?;
192 Ok(())
193}
194
195#[turbo_tasks::function(operation)]
196async fn write_symlinks_batch_operation(
197 symlinks_dir: FileSystemPath,
198 updates: Vec<(usize, usize)>,
199) -> anyhow::Result<()> {
200 updates
201 .into_iter()
202 .map(|(symlink_idx, target_idx)| {
203 let target = RcStr::from(format!("../_targets/{}", target_idx));
204 write_symlink(symlinks_dir.clone(), symlink_idx, target)
205 })
206 .try_join()
207 .await?;
208 Ok(())
209}
210
211#[turbo_tasks::function]
212async fn write_symlink(
213 symlinks_dir: FileSystemPath,
214 symlink_idx: usize,
215 target: RcStr,
216) -> anyhow::Result<()> {
217 let symlink_path = symlinks_dir.join(&symlink_idx.to_string())?;
218 let link_content = LinkContent::Link {
219 target,
220 link_type: LinkType::DIRECTORY,
221 };
222 symlink_path
223 .fs()
224 .write_link(symlink_path.clone(), link_content.cell())
225 .await?;
226 Ok(())
227}
228
229struct FsCleanup<'a> {
230 path: &'a Path,
231}
232
233impl Drop for FsCleanup<'_> {
234 fn drop(&mut self) {
235 std::fs::remove_dir_all(self.path).unwrap();
236 }
237}