Skip to main content

long_screen/kernels/
pool.rs

1//! Data-parallel `for` over independent chunks, run on helper Web Workers that share this module's memory, or on
2//! rayon's thread pool in a native build.
3//!
4//! Only the threaded wasm build (`target_feature = "atomics"`, shared imported memory) and native builds with the
5//! `rayon` feature have helpers; every other build, and any wasm call made while a job is already running, executes
6//! the chunks inline in index order. Natively the helpers are the other threads of the rayon pool the call runs in
7//! (the global pool, or one a host entered with `ThreadPool::install`); a nested call joins that pool. Kernels
8//! are written so their output never depends on which thread ran a chunk or in what order: chunks write
9//! disjoint output ranges, and any reduction is kept per chunk and folded afterwards in chunk order.
10//!
11//! Contract for chunk bodies: no heap allocation and no locks. The calling thread may be a browser main thread,
12//! which cannot block on `memory.atomic.wait32`; it never waits here (it runs chunks itself and then spins for
13//! the last few), and because chunk bodies never touch the allocator the helpers can never hold its lock while
14//! the caller needs it.
15
16/// Helper threads currently parked in [`worker_loop`] (0 in single-threaded builds).
17pub fn helpers() -> usize {
18    imp::helpers()
19}
20
21/// Runs `body(i)` for every `i in 0..chunks`, in parallel when helpers exist.
22pub fn par_for<F: Fn(usize) + Sync>(chunks: usize, body: F) {
23    imp::par_for(chunks, &body)
24}
25
26/// Splits `0..len` into at most `parts` contiguous ranges of near-equal size, returned as the start of range
27/// `i` for `i in 0..=parts` (so range `i` is `bounds(i)..bounds(i + 1)`).
28#[inline]
29pub fn split(len: usize, parts: usize, i: usize) -> usize {
30    len * i / parts.max(1)
31}
32
33/// A chunk count for `len` units of work with at least `min` units per chunk: enough chunks to balance
34/// load across helpers, few enough that dispatch stays negligible. Single-threaded builds get 1.
35pub fn chunks_for(len: usize, min: usize) -> usize {
36    let threads = helpers() + 1;
37    if threads == 1 || len == 0 {
38        return 1;
39    }
40    (len / min.max(1)).clamp(1, threads * 4)
41}
42
43/// A raw pointer chunk bodies may share; each chunk derives its own disjoint slice from it.
44#[derive(Clone, Copy)]
45pub struct SyncPtr<T>(pub *mut T);
46// SAFETY: only ever used to hand disjoint ranges of one buffer to concurrently running chunks.
47unsafe impl<T> Sync for SyncPtr<T> {}
48unsafe impl<T> Send for SyncPtr<T> {}
49impl<T> SyncPtr<T> {
50    #[inline]
51    pub fn get(self) -> *mut T {
52        self.0
53    }
54}
55
56/// Entered once by each helper instance; never returns.
57pub fn worker_loop() -> ! {
58    imp::worker_loop()
59}
60
61#[cfg(target_feature = "atomics")]
62mod imp {
63    use core::arch::wasm32::{memory_atomic_notify, memory_atomic_wait32};
64    use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering::SeqCst};
65
66    /// Seqlock-style job epoch: odd while the caller rewrites the job fields, even once they are published.
67    static EPOCH: AtomicU32 = AtomicU32::new(0);
68    static HELPERS: AtomicUsize = AtomicUsize::new(0);
69    /// Helpers between "saw this epoch" and "done claiming"; the caller does not rewrite the job while > 0.
70    static ACTIVE: AtomicUsize = AtomicUsize::new(0);
71    static BUSY: AtomicBool = AtomicBool::new(false);
72    static JOB_DATA: AtomicUsize = AtomicUsize::new(0);
73    static JOB_CALL: AtomicUsize = AtomicUsize::new(0);
74    static JOB_CHUNKS: AtomicUsize = AtomicUsize::new(0);
75    static NEXT: AtomicUsize = AtomicUsize::new(0);
76    static PENDING: AtomicUsize = AtomicUsize::new(0);
77
78    pub fn helpers() -> usize {
79        HELPERS.load(SeqCst)
80    }
81
82    unsafe fn trampoline<F: Fn(usize) + Sync>(data: usize, i: usize) {
83        (*(data as *const F))(i)
84    }
85
86    fn run_chunks(data: usize, call: usize, chunks: usize) {
87        // SAFETY: `call` was produced from `trampoline::<F>` for the `F` behind `data`, in this same module.
88        let f: unsafe fn(usize, usize) = unsafe { core::mem::transmute(call) };
89        loop {
90            let i = NEXT.fetch_add(1, SeqCst);
91            if i >= chunks {
92                return;
93            }
94            unsafe { f(data, i) };
95            PENDING.fetch_sub(1, SeqCst);
96        }
97    }
98
99    pub fn par_for<F: Fn(usize) + Sync>(chunks: usize, body: &F) {
100        if chunks <= 1 || HELPERS.load(SeqCst) == 0 || BUSY.swap(true, SeqCst) {
101            (0..chunks).for_each(body);
102            return;
103        }
104        // Unpublish, wait out helpers still looking at the previous job, then publish this one.
105        EPOCH.fetch_add(1, SeqCst);
106        while ACTIVE.load(SeqCst) != 0 {
107            core::hint::spin_loop();
108        }
109        let data = body as *const F as usize;
110        let call = trampoline::<F> as unsafe fn(usize, usize) as usize;
111        JOB_DATA.store(data, SeqCst);
112        JOB_CALL.store(call, SeqCst);
113        JOB_CHUNKS.store(chunks, SeqCst);
114        PENDING.store(chunks, SeqCst);
115        NEXT.store(0, SeqCst);
116        EPOCH.fetch_add(1, SeqCst);
117        unsafe { memory_atomic_notify(EPOCH.as_ptr() as *mut i32, u32::MAX) };
118        run_chunks(data, call, chunks);
119        while PENDING.load(SeqCst) != 0 {
120            core::hint::spin_loop();
121        }
122        BUSY.store(false, SeqCst);
123    }
124
125    pub fn worker_loop() -> ! {
126        HELPERS.fetch_add(1, SeqCst);
127        let mut seen = EPOCH.load(SeqCst);
128        loop {
129            let e = EPOCH.load(SeqCst);
130            if e == seen || e & 1 == 1 {
131                // SAFETY: EPOCH is a naturally aligned 32-bit atomic in shared memory.
132                unsafe { memory_atomic_wait32(EPOCH.as_ptr() as *mut i32, e as i32, -1) };
133                continue;
134            }
135            ACTIVE.fetch_add(1, SeqCst);
136            if EPOCH.load(SeqCst) == e {
137                seen = e;
138                run_chunks(
139                    JOB_DATA.load(SeqCst),
140                    JOB_CALL.load(SeqCst),
141                    JOB_CHUNKS.load(SeqCst),
142                );
143            }
144            ACTIVE.fetch_sub(1, SeqCst);
145        }
146    }
147}
148
149#[cfg(all(feature = "rayon", not(target_feature = "atomics")))]
150mod imp {
151    use rayon::prelude::*;
152
153    pub fn helpers() -> usize {
154        rayon::current_num_threads().saturating_sub(1)
155    }
156    pub fn par_for<F: Fn(usize) + Sync>(chunks: usize, body: &F) {
157        if chunks <= 1 {
158            (0..chunks).for_each(body);
159        } else {
160            (0..chunks).into_par_iter().for_each(body);
161        }
162    }
163    pub fn worker_loop() -> ! {
164        panic!("worker_loop is the threaded wasm build's; native helpers are rayon's threads")
165    }
166}
167
168#[cfg(all(not(feature = "rayon"), not(target_feature = "atomics")))]
169mod imp {
170    pub fn helpers() -> usize {
171        0
172    }
173    pub fn par_for<F: Fn(usize) + Sync>(chunks: usize, body: &F) {
174        (0..chunks).for_each(body)
175    }
176    pub fn worker_loop() -> ! {
177        panic!("worker_loop requires the threaded core build")
178    }
179}