1use crate::{PixelsError, Result};
28use std::collections::VecDeque;
29use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
30use std::sync::{Arc, Condvar, Mutex, MutexGuard};
31
32type Task = Box<dyn FnOnce() + Send>;
34
35const IDLE_PARK: std::time::Duration = std::time::Duration::from_secs(1);
43
44thread_local! {
45 static ON_WORKER: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
47}
48
49struct Shared {
51 queue: Mutex<VecDeque<Task>>,
53 workers: usize,
55 shutdown: AtomicBool,
57 pending: AtomicUsize,
59 idle: Mutex<()>,
61 wake: Condvar,
62}
63
64impl Shared {
65 fn queue(&self) -> MutexGuard<'_, VecDeque<Task>> {
66 self.queue
69 .lock()
70 .unwrap_or_else(std::sync::PoisonError::into_inner)
71 }
72
73 fn signal_one(&self) {
75 let _guard = self
78 .idle
79 .lock()
80 .unwrap_or_else(std::sync::PoisonError::into_inner);
81 self.wake.notify_one();
82 }
83
84 fn signal_all(&self) {
86 let _guard = self
87 .idle
88 .lock()
89 .unwrap_or_else(std::sync::PoisonError::into_inner);
90 self.wake.notify_all();
91 }
92}
93
94#[derive(Debug)]
99pub struct ThreadPool {
100 shared: Arc<Shared>,
101 workers: Vec<std::thread::JoinHandle<()>>,
102 threads: usize,
103}
104
105impl std::fmt::Debug for Shared {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("Shared")
108 .field("workers", &self.workers)
109 .field("pending", &self.pending.load(Ordering::Relaxed))
110 .finish_non_exhaustive()
111 }
112}
113
114impl ThreadPool {
115 pub fn new(threads: usize) -> Result<Self> {
125 let threads = threads.max(1);
126 let shared = Arc::new(Shared {
127 queue: Mutex::new(VecDeque::new()),
128 workers: threads,
129 shutdown: AtomicBool::new(false),
130 pending: AtomicUsize::new(0),
131 idle: Mutex::new(()),
132 wake: Condvar::new(),
133 });
134
135 let mut workers = Vec::with_capacity(threads);
136 for index in 0..threads {
137 let shared = Arc::clone(&shared);
138 let handle = std::thread::Builder::new()
139 .name(format!("otf-pixels-worker-{index}"))
140 .spawn(move || worker_loop(&shared))
141 .map_err(|e| PixelsError::io("spawning a scheduler worker thread", e))?;
142 workers.push(handle);
143 }
144 Ok(Self {
145 shared,
146 workers,
147 threads,
148 })
149 }
150
151 #[must_use]
156 pub fn default_threads() -> usize {
157 std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get)
158 }
159
160 pub fn with_default_threads() -> Result<Self> {
166 Self::new(Self::default_threads())
167 }
168
169 #[must_use]
175 pub fn on_worker_thread() -> bool {
176 ON_WORKER.with(std::cell::Cell::get)
177 }
178
179 #[must_use]
181 pub const fn threads(&self) -> usize {
182 self.threads
183 }
184
185 pub fn spawn(&self, task: impl FnOnce() + Send + 'static) {
190 self.shared.pending.fetch_add(1, Ordering::SeqCst);
191 self.shared.queue().push_back(Box::new(task));
192 self.shared.signal_one();
193 }
194
195 pub fn run_all<F>(&self, tasks: Vec<F>) -> Result<()>
217 where
218 F: FnOnce() -> Result<()> + Send + 'static,
219 {
220 if tasks.is_empty() {
221 return Ok(());
222 }
223 let count = tasks.len();
224 let batch = Arc::new(Batch::new(count));
225 self.shared.pending.fetch_add(count, Ordering::SeqCst);
228 {
229 let mut queue = self.shared.queue();
230 for (index, task) in tasks.into_iter().enumerate() {
231 let batch = Arc::clone(&batch);
232 queue.push_back(Box::new(move || {
233 let outcome = catch(task);
234 batch.finish(index, outcome);
235 }));
236 }
237 }
238 if count >= self.shared.workers {
239 self.shared.signal_all();
240 } else {
241 for _ in 0..count {
242 self.shared.signal_one();
243 }
244 }
245 batch.wait();
246 batch.first_error()
247 }
248}
249
250#[derive(Debug)]
252struct Batch {
253 slots: Vec<Mutex<Option<PixelsError>>>,
256 remaining: AtomicUsize,
257 finished: Mutex<bool>,
258 complete: Condvar,
259}
260
261impl Batch {
262 fn new(count: usize) -> Self {
263 Self {
264 slots: (0..count).map(|_| Mutex::new(None)).collect(),
265 remaining: AtomicUsize::new(count),
266 finished: Mutex::new(false),
267 complete: Condvar::new(),
268 }
269 }
270
271 fn finish(&self, index: usize, outcome: Result<()>) {
273 if let Err(error) = outcome {
274 if let Some(slot) = self.slots.get(index) {
275 *slot
276 .lock()
277 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(error);
278 }
279 }
280 if self.remaining.fetch_sub(1, Ordering::SeqCst) == 1 {
281 let mut finished = self
282 .finished
283 .lock()
284 .unwrap_or_else(std::sync::PoisonError::into_inner);
285 *finished = true;
286 self.complete.notify_all();
287 }
288 }
289
290 fn wait(&self) {
292 let mut finished = self
293 .finished
294 .lock()
295 .unwrap_or_else(std::sync::PoisonError::into_inner);
296 while !*finished {
297 finished = self
298 .complete
299 .wait(finished)
300 .unwrap_or_else(std::sync::PoisonError::into_inner);
301 }
302 }
303
304 fn first_error(&self) -> Result<()> {
306 for slot in &self.slots {
307 let mut slot = slot
308 .lock()
309 .unwrap_or_else(std::sync::PoisonError::into_inner);
310 if let Some(error) = slot.take() {
311 return Err(error);
312 }
313 }
314 Ok(())
315 }
316}
317
318fn catch<F: FnOnce() -> Result<()>>(task: F) -> Result<()> {
320 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(task)) {
321 Ok(result) => result,
322 Err(payload) => {
323 let detail = panic_message(payload.as_ref());
324 Err(PixelsError::graph(format!(
325 "a scheduler task panicked: {detail}"
326 )))
327 }
328 }
329}
330
331fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
333 if let Some(text) = payload.downcast_ref::<&str>() {
334 return (*text).to_owned();
335 }
336 if let Some(text) = payload.downcast_ref::<String>() {
337 return text.clone();
338 }
339 "non-string panic payload".to_owned()
340}
341
342fn worker_loop(shared: &Arc<Shared>) {
344 ON_WORKER.with(|flag| flag.set(true));
345 loop {
346 if shared.shutdown.load(Ordering::SeqCst) && shared.pending.load(Ordering::SeqCst) == 0 {
347 return;
348 }
349 let next = shared.queue().pop_front();
352 if let Some(task) = next {
353 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(task));
355 shared.pending.fetch_sub(1, Ordering::SeqCst);
356 continue;
357 }
358 let guard = shared
362 .idle
363 .lock()
364 .unwrap_or_else(std::sync::PoisonError::into_inner);
365 if !shared.queue().is_empty() {
366 continue;
367 }
368 if shared.shutdown.load(Ordering::SeqCst) {
371 return;
372 }
373 let _unused = shared
374 .wake
375 .wait_timeout(guard, IDLE_PARK)
376 .unwrap_or_else(std::sync::PoisonError::into_inner);
377 }
378}
379
380impl Drop for ThreadPool {
381 fn drop(&mut self) {
382 self.shared.shutdown.store(true, Ordering::SeqCst);
383 self.shared.signal_all();
384 for handle in self.workers.drain(..) {
385 let _ = handle.join();
388 }
389 }
390}
391
392#[cfg(test)]
393#[allow(
394 clippy::unwrap_used,
395 clippy::indexing_slicing,
396 clippy::panic,
397 reason = "tests operate on known-good values and assert shapes directly"
398)]
399mod tests {
400 use super::*;
401
402 fn counter() -> Arc<AtomicUsize> {
404 Arc::new(AtomicUsize::new(0))
405 }
406
407 #[test]
408 fn workers_know_they_are_workers() {
409 assert!(!ThreadPool::on_worker_thread());
410 let pool = ThreadPool::new(2).unwrap();
411 let seen = Arc::new(AtomicBool::new(false));
412 let flag = Arc::clone(&seen);
413 pool.run_all(vec![move || {
414 flag.store(ThreadPool::on_worker_thread(), Ordering::SeqCst);
415 Ok(())
416 }])
417 .unwrap();
418 assert!(seen.load(Ordering::SeqCst));
419 assert!(!ThreadPool::on_worker_thread());
420 }
421
422 #[test]
423 fn an_idle_pool_wakes_for_new_work_rather_than_polling_for_it() {
424 let pool = ThreadPool::new(2).unwrap();
427 std::thread::sleep(std::time::Duration::from_millis(50));
428 let started = std::time::Instant::now();
429 pool.run_all(vec![|| Ok(())]).unwrap();
430 assert!(
431 started.elapsed() < IDLE_PARK / 2,
432 "an idle pool took {:?} to run one task",
433 started.elapsed()
434 );
435 }
436
437 #[test]
438 fn every_task_runs_exactly_once() {
439 let pool = ThreadPool::new(4).unwrap();
440 let count = counter();
441 let tasks: Vec<_> = (0..1000)
442 .map(|_| {
443 let count = Arc::clone(&count);
444 move || {
445 count.fetch_add(1, Ordering::Relaxed);
446 Ok(())
447 }
448 })
449 .collect();
450 pool.run_all(tasks).unwrap();
451 assert_eq!(count.load(Ordering::Relaxed), 1000);
452 }
453
454 #[test]
455 fn tasks_share_state_through_arcs() {
456 let pool = ThreadPool::new(4).unwrap();
459 let data: Arc<Vec<usize>> = Arc::new((0..100).collect());
460 let total = counter();
461 let tasks: Vec<_> = (0..10)
462 .map(|chunk| {
463 let (data, total) = (Arc::clone(&data), Arc::clone(&total));
464 move || {
465 let sum: usize = data[chunk * 10..(chunk + 1) * 10].iter().sum();
466 total.fetch_add(sum, Ordering::Relaxed);
467 Ok(())
468 }
469 })
470 .collect();
471 pool.run_all(tasks).unwrap();
472 assert_eq!(total.load(Ordering::Relaxed), (0..100).sum::<usize>());
473 }
474
475 #[test]
476 fn the_lowest_indexed_failure_is_reported() {
477 let pool = ThreadPool::new(8).unwrap();
480 for attempt in 0..25 {
481 let tasks: Vec<_> = (0..64)
482 .map(|i| {
483 move || {
484 if i == 5 || i == 40 {
485 return Err(PixelsError::malformed("test", format!("task {i}")));
486 }
487 Ok(())
488 }
489 })
490 .collect();
491 let err = pool.run_all(tasks).unwrap_err();
492 assert!(
493 err.to_string().contains("task 5"),
494 "attempt {attempt}: {err}"
495 );
496 }
497 }
498
499 #[test]
500 fn a_panicking_task_becomes_an_error_not_an_abort() {
501 let pool = ThreadPool::new(4).unwrap();
502 let tasks: Vec<_> = (0..8)
503 .map(|i| {
504 move || {
505 assert!(i != 3, "kernel defect");
506 Ok(())
507 }
508 })
509 .collect();
510 let err = pool.run_all(tasks).unwrap_err();
511 assert_eq!(err.code(), crate::ErrorCode::Graph);
512 assert!(err.to_string().contains("panicked"), "got: {err}");
513 assert!(err.to_string().contains("kernel defect"), "got: {err}");
514
515 let count = counter();
517 let c = Arc::clone(&count);
518 pool.run_all(vec![move || {
519 c.fetch_add(1, Ordering::Relaxed);
520 Ok(())
521 }])
522 .unwrap();
523 assert_eq!(count.load(Ordering::Relaxed), 1);
524 }
525
526 #[test]
527 fn a_single_threaded_pool_still_completes() {
528 let pool = ThreadPool::new(1).unwrap();
530 let count = counter();
531 let tasks: Vec<_> = (0..100)
532 .map(|_| {
533 let count = Arc::clone(&count);
534 move || {
535 count.fetch_add(1, Ordering::Relaxed);
536 Ok(())
537 }
538 })
539 .collect();
540 pool.run_all(tasks).unwrap();
541 assert_eq!(count.load(Ordering::Relaxed), 100);
542 assert_eq!(pool.threads(), 1);
543 }
544
545 #[test]
546 fn zero_threads_is_clamped_to_one() {
547 assert_eq!(ThreadPool::new(0).unwrap().threads(), 1);
548 }
549
550 #[test]
551 fn an_empty_batch_is_a_no_op() {
552 let pool = ThreadPool::new(2).unwrap();
553 let tasks: Vec<fn() -> Result<()>> = Vec::new();
554 pool.run_all(tasks).unwrap();
555 }
556
557 #[test]
558 fn repeated_batches_reuse_the_same_workers() {
559 let pool = ThreadPool::new(4).unwrap();
561 let count = counter();
562 for _ in 0..50 {
563 let tasks: Vec<_> = (0..20)
564 .map(|_| {
565 let count = Arc::clone(&count);
566 move || {
567 count.fetch_add(1, Ordering::Relaxed);
568 Ok(())
569 }
570 })
571 .collect();
572 pool.run_all(tasks).unwrap();
573 }
574 assert_eq!(count.load(Ordering::Relaxed), 1000);
575 }
576
577 #[test]
578 fn outstanding_spawned_work_completes_before_drop() {
579 let done = counter();
580 {
581 let pool = ThreadPool::new(4).unwrap();
582 for _ in 0..200 {
583 let done = Arc::clone(&done);
584 pool.spawn(move || {
585 done.fetch_add(1, Ordering::Relaxed);
586 });
587 }
588 }
590 assert_eq!(done.load(Ordering::Relaxed), 200);
591 }
592
593 #[test]
594 fn default_threads_is_at_least_one() {
595 assert!(ThreadPool::default_threads() >= 1);
596 assert!(ThreadPool::with_default_threads().unwrap().threads() >= 1);
597 }
598
599 #[test]
600 fn work_is_actually_distributed_across_workers() {
601 let pool = ThreadPool::new(4).unwrap();
604 let seen: Arc<Mutex<std::collections::HashSet<std::thread::ThreadId>>> =
605 Arc::new(Mutex::new(std::collections::HashSet::new()));
606 let tasks: Vec<_> = (0..200)
607 .map(|_| {
608 let seen = Arc::clone(&seen);
609 move || {
610 std::thread::sleep(std::time::Duration::from_micros(100));
615 seen.lock().unwrap().insert(std::thread::current().id());
616 Ok(())
617 }
618 })
619 .collect();
620 pool.run_all(tasks).unwrap();
621 let count = seen.lock().unwrap().len();
622 assert!(
623 count > 1,
624 "all work ran on one thread; the queue is not being shared"
625 );
626 }
627
628 #[test]
629 fn nested_arcs_keep_results_alive_across_batches() {
630 let pool = ThreadPool::new(4).unwrap();
632 let stage1: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(vec![0; 16]));
633 let tasks: Vec<_> = (0..16_u64)
634 .map(|i| {
635 let out = Arc::clone(&stage1);
636 move || {
637 out.lock().unwrap()[i as usize] = i * 2;
638 Ok(())
639 }
640 })
641 .collect();
642 pool.run_all(tasks).unwrap();
643
644 let total = Arc::new(AtomicUsize::new(0));
645 let tasks: Vec<_> = (0..16_usize)
646 .map(|i| {
647 let (input, total) = (Arc::clone(&stage1), Arc::clone(&total));
648 move || {
649 let v = input.lock().unwrap()[i];
650 total.fetch_add(v as usize, Ordering::Relaxed);
651 Ok(())
652 }
653 })
654 .collect();
655 pool.run_all(tasks).unwrap();
656 assert_eq!(
657 total.load(Ordering::Relaxed),
658 (0..16).map(|i| i * 2).sum::<usize>()
659 );
660 }
661}