1use std::{
12 any::Any,
13 marker::PhantomData,
14 num::NonZeroUsize,
15 panic::{self, AssertUnwindSafe, catch_unwind},
16 sync::{
17 Arc,
18 atomic::{AtomicUsize, Ordering},
19 mpmc::{self, Receiver, Sender},
20 },
21 thread::{self, Thread},
22 time::{Duration, Instant},
23};
24
25use parking_lot::Mutex;
26use tokio::{runtime::Handle, task::block_in_place};
27use tracing::{Span, info_span};
28
29use crate::{TurboTasksApi, manager::try_turbo_tasks, turbo_tasks_scope};
30
31type WorkQueueJob = (usize, Box<dyn FnOnce() + Send + 'static>);
33
34struct ScopeInner {
35 main_thread: Thread,
36 remaining_tasks: AtomicUsize,
37 panic: Mutex<Option<(Box<dyn Any + Send + 'static>, usize)>>,
40 work_queue: Receiver<WorkQueueJob>,
43}
44
45impl ScopeInner {
46 fn on_task_finished(&self, panic: Option<(Box<dyn Any + Send + 'static>, usize)>) {
47 if let Some((err, index)) = panic {
48 let mut old_panic = self.panic.lock();
49 if old_panic.as_ref().is_none_or(|&(_, i)| i > index) {
50 *old_panic = Some((err, index));
51 }
52 }
53 if self.remaining_tasks.fetch_sub(1, Ordering::Release) == 1 {
54 self.main_thread.unpark();
55 }
56 }
57
58 fn wait(&self) {
59 if self.remaining_tasks.load(Ordering::Acquire) == 0 {
60 return;
61 }
62
63 let _span = info_span!("blocking").entered();
64
65 const TIMEOUT: Duration = Duration::from_millis(1);
67 let beginning_park = Instant::now();
68
69 let mut timeout_remaining = TIMEOUT;
70 loop {
71 thread::park_timeout(timeout_remaining);
72 if self.remaining_tasks.load(Ordering::Acquire) == 0 {
73 return;
74 }
75 let elapsed = beginning_park.elapsed();
76 if elapsed >= TIMEOUT {
77 break;
78 }
79 timeout_remaining = TIMEOUT - elapsed;
80 }
81
82 block_in_place(|| {
84 while self.remaining_tasks.load(Ordering::Acquire) != 0 {
85 thread::park();
86 }
87 });
88 }
89
90 fn wait_and_rethrow_panic(&self) {
91 self.wait();
92 if let Some((err, _)) = self.panic.lock().take() {
93 panic::resume_unwind(err);
94 }
95 }
96
97 fn run_jobs(&self) {
101 while let Ok((index, job)) = self.work_queue.recv() {
102 let result = catch_unwind(AssertUnwindSafe(job));
103 let panic = result.err().map(|e| (e, index));
104 self.on_task_finished(panic);
105 }
106 }
107}
108
109pub struct Scope<'scope, 'env: 'scope, R: Send + 'env> {
113 results: &'scope [Mutex<Option<R>>],
114 index: AtomicUsize,
115 inner: Arc<ScopeInner>,
116 work_queue: Option<Sender<WorkQueueJob>>,
118 handle: Handle,
119 worker_tasks: NonZeroUsize,
122 turbo_tasks: Option<Arc<dyn TurboTasksApi>>,
125 span: Span,
126 env: PhantomData<&'env mut &'env ()>,
131}
132
133impl<'scope, 'env: 'scope, R: Send + 'env> Scope<'scope, 'env, R> {
134 unsafe fn new(results: &'scope [Mutex<Option<R>>]) -> Self {
140 let handle = Handle::current();
141 let worker_tasks = NonZeroUsize::new(handle.metrics().num_workers().min(results.len()))
143 .unwrap_or(NonZeroUsize::MIN);
144 let (sender, receiver) = mpmc::channel();
145 Self {
146 results,
147 index: AtomicUsize::new(0),
148 inner: Arc::new(ScopeInner {
149 main_thread: thread::current(),
150 remaining_tasks: AtomicUsize::new(0),
151 panic: Mutex::new(None),
152 work_queue: receiver,
153 }),
154 work_queue: Some(sender),
155 handle,
156 worker_tasks,
157 turbo_tasks: try_turbo_tasks(),
158 span: Span::current(),
159 env: PhantomData,
160 }
161 }
162
163 pub fn spawn<F>(&self, f: F)
165 where
166 F: FnOnce() -> R + Send + 'env,
167 {
168 let index = self.index.fetch_add(1, Ordering::Relaxed);
169 assert!(index < self.results.len(), "Too many tasks spawned");
170 let result_cell: &Mutex<Option<R>> = &self.results[index];
171
172 let f: Box<dyn FnOnce() + Send + 'scope> = Box::new(|| {
176 let result = f();
177 *result_cell.lock() = Some(result);
178 });
179 let f: *mut (dyn FnOnce() + Send + 'scope) = Box::into_raw(f);
180
181 let f = unsafe {
184 std::mem::transmute::<
185 *mut (dyn FnOnce() + Send + 'scope),
186 *mut (dyn FnOnce() + Send + 'static),
187 >(f)
188 };
189
190 let f = unsafe { Box::from_raw(f) };
192
193 self.inner.remaining_tasks.fetch_add(1, Ordering::Relaxed);
194
195 self.work_queue
199 .as_ref()
200 .expect("sender is only taken in Drop")
201 .send((index, f))
202 .expect("receiver is owned by inner and outlives the scope");
203
204 if index < self.worker_tasks.get() - 1 {
206 let inner = self.inner.clone();
207 let span = self.span.clone();
208 let turbo_tasks = self.turbo_tasks.clone();
209 self.handle.spawn(async move {
210 let _span = span.entered();
211 if let Some(turbo_tasks) = turbo_tasks {
212 turbo_tasks_scope(turbo_tasks, || inner.run_jobs());
214 } else {
215 inner.run_jobs();
216 }
217 });
218 }
219 }
220}
221
222impl<'scope, 'env: 'scope, R: Send + 'env> Drop for Scope<'scope, 'env, R> {
223 fn drop(&mut self) {
224 drop(
228 self.work_queue
229 .take()
230 .expect("sender is taken exactly once, here in Drop"),
231 );
232 self.inner.run_jobs();
234 self.inner.wait_and_rethrow_panic();
235 }
236}
237
238pub fn scope_bounded<'env, F, R>(number_of_tasks: usize, f: F) -> impl Iterator<Item = R>
253where
254 R: Send + 'env,
255 F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, R>) + 'env,
256{
257 let mut results = Vec::with_capacity(number_of_tasks);
258 for _ in 0..number_of_tasks {
259 results.push(Mutex::new(None));
260 }
261 let results = results.into_boxed_slice();
262 let result = {
263 let scope = unsafe { Scope::new(&results) };
265 catch_unwind(AssertUnwindSafe(|| f(&scope)))
266 };
267 if let Err(panic) = result {
268 panic::resume_unwind(panic);
269 }
270 results.into_iter().map(|mutex| {
271 mutex
272 .into_inner()
273 .expect("All values are set when the scope returns without panic")
274 })
275}
276
277#[cfg(test)]
278mod tests {
279 use std::{
280 panic::{AssertUnwindSafe, catch_unwind},
281 sync::atomic::AtomicUsize,
282 };
283
284 use super::*;
285
286 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
293 async fn test_scope_worker_threads_occupied() {
294 const WORKER_THREADS: usize = 2;
295 const JOBS: usize = 64;
296 const RELEASE_AFTER: Duration = Duration::from_secs(4);
297
298 let ready = Arc::new(AtomicUsize::new(0));
301 let mut occupiers = Vec::with_capacity(WORKER_THREADS);
302 for _ in 0..WORKER_THREADS {
303 let ready = ready.clone();
304 occupiers.push(tokio::spawn(async move {
305 ready.fetch_add(1, Ordering::SeqCst);
306 thread::sleep(RELEASE_AFTER);
307 }));
308 }
309 while ready.load(Ordering::SeqCst) < WORKER_THREADS {
311 tokio::task::yield_now().await;
312 }
313
314 let started = Instant::now();
315 let results = tokio::task::spawn_blocking(move || {
316 scope_bounded(JOBS, |scope| {
317 for i in 0..JOBS {
318 scope.spawn(move || i);
319 }
320 })
321 .collect::<Vec<_>>()
322 })
323 .await
324 .unwrap();
325 let elapsed = started.elapsed();
326
327 assert_eq!(results.len(), JOBS);
328 results.iter().enumerate().for_each(|(i, &result)| {
329 assert_eq!(result, i);
330 });
331 assert!(
332 elapsed < RELEASE_AFTER / 2,
333 "scope_bounded took {elapsed:?}; it should not depend on an occupied worker thread \
334 freeing up"
335 );
336
337 for occupier in occupiers {
338 occupier.await.unwrap();
339 }
340 }
341
342 #[tokio::test(flavor = "current_thread")]
345 async fn test_scope_current_thread_runtime() {
346 let results = tokio::task::spawn_blocking(|| {
347 scope_bounded(16, |scope| {
348 for i in 0..16 {
349 scope.spawn(move || i);
350 }
351 })
352 .collect::<Vec<_>>()
353 })
354 .await
355 .unwrap();
356 assert_eq!(results.len(), 16);
357 results.iter().enumerate().for_each(|(i, &result)| {
358 assert_eq!(result, i);
359 });
360 }
361
362 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
365 #[cfg_attr(
368 target_family = "wasm",
369 ignore = "timing bound is too strict for Node Worker startup"
370 )]
371 async fn test_scope_runs_in_parallel() {
372 const JOBS: usize = 16;
373 const PER_JOB: Duration = Duration::from_millis(50);
374 let started = Instant::now();
375 let results = tokio::task::spawn_blocking(|| {
376 scope_bounded(JOBS, |scope| {
377 for i in 0..JOBS {
378 scope.spawn(move || {
379 thread::sleep(PER_JOB);
380 i
381 });
382 }
383 })
384 .collect::<Vec<_>>()
385 })
386 .await
387 .unwrap();
388 let elapsed = started.elapsed();
389 assert_eq!(results.len(), JOBS);
390 assert!(
393 elapsed < (JOBS as u32 * PER_JOB) / 2,
394 "scope_bounded took {elapsed:?}; expected parallel speedup across worker threads"
395 );
396 }
397
398 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
399 async fn test_scope() {
400 let results = scope_bounded(1000, |scope| {
401 for i in 0..1000 {
402 scope.spawn(move || i);
403 }
404 });
405 let results = results.collect::<Vec<_>>();
406 results.iter().enumerate().for_each(|(i, &result)| {
407 assert_eq!(result, i);
408 });
409 assert_eq!(results.len(), 1000);
410 }
411
412 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
413 async fn test_empty_scope() {
414 let results = scope_bounded(0, |scope| {
415 if false {
416 scope.spawn(|| 42);
417 }
418 });
419 assert_eq!(results.count(), 0);
420 }
421
422 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
423 async fn test_single_task() {
424 let results = scope_bounded(1, |scope| {
425 scope.spawn(|| 42);
426 })
427 .collect::<Vec<_>>();
428 assert_eq!(results, vec![42]);
429 }
430
431 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
432 async fn test_task_finish_before_scope() {
433 let results = scope_bounded(1, |scope| {
434 scope.spawn(|| 42);
435 thread::sleep(std::time::Duration::from_millis(100));
436 })
437 .collect::<Vec<_>>();
438 assert_eq!(results, vec![42]);
439 }
440
441 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
442 async fn test_task_finish_after_scope() {
443 let results = scope_bounded(1, |scope| {
444 scope.spawn(|| {
445 thread::sleep(std::time::Duration::from_millis(100));
446 42
447 });
448 })
449 .collect::<Vec<_>>();
450 assert_eq!(results, vec![42]);
451 }
452
453 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
454 #[cfg_attr(
456 target_family = "wasm",
457 ignore = "no unwinding on wasm: std is built panic=abort, so catch_unwind cannot catch"
458 )]
459 async fn test_panic_in_scope_factory() {
460 let result = catch_unwind(AssertUnwindSafe(|| {
461 let _results = scope_bounded(1000, |scope| {
462 for i in 0..500 {
463 scope.spawn(move || i);
464 }
465 panic!("Intentional panic");
466 });
467 unreachable!();
468 }));
469 assert!(result.is_err());
470 assert_eq!(
471 result.unwrap_err().downcast_ref::<&str>(),
472 Some(&"Intentional panic")
473 );
474 }
475
476 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
477 #[cfg_attr(
479 target_family = "wasm",
480 ignore = "no unwinding on wasm: std is built panic=abort, so catch_unwind cannot catch"
481 )]
482 async fn test_panic_in_scope_task() {
483 let result = catch_unwind(AssertUnwindSafe(|| {
484 let _results = scope_bounded(1000, |scope| {
485 for i in 0..1000 {
486 scope.spawn(move || {
487 if i == 500 {
488 panic!("Intentional panic");
489 } else if i == 501 {
490 panic!("Wrong intentional panic");
491 } else {
492 i
493 }
494 });
495 }
496 });
497 unreachable!();
498 }));
499 assert!(result.is_err());
500 assert_eq!(
501 result.unwrap_err().downcast_ref::<&str>(),
502 Some(&"Intentional panic")
503 );
504 }
505}