Skip to main content

plat/
web_pool.rs

1// Cooperative worker jobs for the browser. Parallel loops run sequentially in the app
2// worker; background jobs advance a bounded number of items between browser UI steps.
3
4use alloc::{boxed::Box, sync::Arc, vec::Vec};
5use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
6use rt::sync::SpinLock;
7
8/// Maximum native worker count; the browser has zero auxiliary compute threads.
9pub const THR_MAX: usize = 64;
10/// The caller cancelled a loop or background job.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct Cancelled;
13
14/// Cooperative progress and cancellation state exposed to one work item.
15pub struct JobCtl {
16    count: usize,
17    done: AtomicUsize,
18    cancel: AtomicBool,
19    running: AtomicBool,
20}
21impl JobCtl {
22    fn new(count: usize) -> Self {
23        Self {
24            count,
25            done: AtomicUsize::new(0),
26            cancel: AtomicBool::new(false),
27            running: AtomicBool::new(false),
28        }
29    }
30    /// Cancel work not yet started; long items can poll `cancelled`.
31    pub fn cancel(&self) {
32        self.cancel.store(true, Ordering::Relaxed);
33    }
34    /// Whether cancellation has been requested.
35    pub fn cancelled(&self) -> bool {
36        self.cancel.load(Ordering::Relaxed)
37    }
38    /// Number of items in this job.
39    pub fn count(&self) -> u64 {
40        self.count as u64
41    }
42    /// Number of completely processed items.
43    pub fn items_done(&self) -> u64 {
44        self.done.load(Ordering::Relaxed) as u64
45    }
46}
47
48struct Record {
49    ctl: JobCtl,
50    function: Option<Box<dyn Fn(usize, &JobCtl) + Send + Sync>>,
51}
52static QUEUE: SpinLock<Vec<Arc<Record>>> = SpinLock::new(Vec::new());
53
54fn step(record: &Record) -> bool {
55    let ctl = &record.ctl;
56    if !ctl.running.load(Ordering::Relaxed) {
57        return false;
58    }
59    let i = ctl.done.load(Ordering::Relaxed);
60    if i >= ctl.count || ctl.cancelled() || record.function.is_none() {
61        ctl.running.store(false, Ordering::Relaxed);
62        crate::wake();
63        return false;
64    }
65    if let Some(function) = &record.function {
66        function(i, ctl);
67    }
68    ctl.done.store(i + 1, Ordering::Relaxed);
69    if i + 1 >= ctl.count || ctl.cancelled() {
70        ctl.running.store(false, Ordering::Relaxed);
71        crate::wake();
72    }
73    true
74}
75
76/// Process at most `items` background work items before yielding to the host again.
77pub fn pump_jobs(items: usize) {
78    for _ in 0..items {
79        let record = {
80            let mut q = QUEUE.lock();
81            if q.is_empty() {
82                None
83            } else {
84                Some(q.remove(0))
85            }
86        };
87        let Some(record) = record else {
88            break;
89        };
90        let _ = step(&record);
91        if record.ctl.running.load(Ordering::Relaxed) {
92            QUEUE.lock().push(record);
93        }
94    }
95}
96
97/// Browser compute remains on its app worker; no auxiliary OS threads are started.
98pub fn thr_init(_count: u32) -> u32 {
99    0
100}
101/// Number of auxiliary workers.
102pub fn thr_count() -> u32 {
103    0
104}
105/// Cancel and complete all queued cooperative jobs.
106pub fn thr_shutdown() {
107    let queue = core::mem::take(&mut *QUEUE.lock());
108    for record in queue {
109        record.ctl.cancel();
110        let _ = step(&record);
111    }
112}
113/// The app worker always uses scratch slot zero.
114pub fn thr_self() -> usize {
115    0
116}
117/// Run the existing parallel-loop contract sequentially, checking cancellation per item.
118pub fn parallel_for(count: usize, f: &(dyn Fn(usize, &JobCtl) + Sync)) -> Result<(), Cancelled> {
119    let ctl = JobCtl::new(count);
120    for i in 0..count {
121        if ctl.cancelled() {
122            break;
123        }
124        f(i, &ctl);
125        ctl.done.store(i + 1, Ordering::Relaxed);
126    }
127    if ctl.cancelled() {
128        Err(Cancelled)
129    } else {
130        Ok(())
131    }
132}
133
134/// A background job whose items run between browser frames.
135pub struct Job {
136    record: Arc<Record>,
137}
138impl Default for Job {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143impl Job {
144    /// Create an empty, completed job.
145    pub fn new() -> Self {
146        Self {
147            record: Arc::new(Record {
148                ctl: JobCtl::new(0),
149                function: None,
150            }),
151        }
152    }
153    /// Replace a completed job's work; item counts must fit wasm32 address space.
154    pub fn set(&mut self, count: u64, f: impl Fn(usize, &JobCtl) + Send + Sync + 'static) -> bool {
155        if !self.done() || count > usize::MAX as u64 {
156            return false;
157        }
158        self.record = Arc::new(Record {
159            ctl: JobCtl::new(count as usize),
160            function: Some(Box::new(f)),
161        });
162        true
163    }
164    /// Replace only the item count after completion.
165    pub fn set_count(&mut self, count: u64) -> bool {
166        if !self.done() || count > usize::MAX as u64 {
167            return false;
168        }
169        // Stale completed queue references are removed before requesting unique ownership.
170        QUEUE.lock().retain(|r| !Arc::ptr_eq(r, &self.record));
171        let Some(record) = Arc::get_mut(&mut self.record) else {
172            return false;
173        };
174        record.ctl.count = count as usize;
175        true
176    }
177    /// Queue the job; execution begins during the next host step.
178    pub fn start(&self) -> bool {
179        if !self.done() {
180            return false;
181        }
182        let ctl = &self.record.ctl;
183        ctl.done.store(0, Ordering::Relaxed);
184        ctl.cancel.store(false, Ordering::Relaxed);
185        QUEUE.lock().retain(|r| !Arc::ptr_eq(r, &self.record));
186        if ctl.count == 0 || self.record.function.is_none() {
187            crate::wake();
188            return true;
189        }
190        ctl.running.store(true, Ordering::Relaxed);
191        QUEUE.lock().push(self.record.clone());
192        crate::wake();
193        true
194    }
195    /// Finish remaining work synchronously when a caller explicitly waits.
196    pub fn wait(&self) -> Result<(), Cancelled> {
197        while step(&self.record) {}
198        if self.record.ctl.cancelled() {
199            Err(Cancelled)
200        } else {
201            Ok(())
202        }
203    }
204    /// Request cancellation at the next item boundary.
205    pub fn cancel(&self) {
206        self.record.ctl.cancel();
207    }
208    /// Whether the job has completed or has not started.
209    pub fn done(&self) -> bool {
210        !self.record.ctl.running.load(Ordering::Relaxed)
211    }
212    /// Completed item count.
213    pub fn items_done(&self) -> u64 {
214        self.record.ctl.items_done()
215    }
216    /// Total item count.
217    pub fn count(&self) -> u64 {
218        self.record.ctl.count()
219    }
220}
221impl Drop for Job {
222    fn drop(&mut self) {
223        self.cancel();
224        let _ = self.wait();
225    }
226}