portalis_transpiler/wasi_threading/
pool.rs1use anyhow::{Result, Context, anyhow};
6use std::sync::Arc;
7use std::time::Duration;
8use super::{WasiQueue, ThreadingError, WasiThread, ThreadHandle};
9
10#[cfg(not(target_arch = "wasm32"))]
11use rayon;
12
13#[derive(Debug, Clone)]
15pub struct ThreadPoolConfig {
16 pub num_threads: Option<usize>,
18 pub thread_name_prefix: Option<String>,
20 pub stack_size: Option<usize>,
22 pub max_pending_tasks: Option<usize>,
24 pub enable_work_stealing: bool,
26}
27
28impl ThreadPoolConfig {
29 pub fn new() -> Self {
31 Self {
32 num_threads: None,
33 thread_name_prefix: Some("worker".to_string()),
34 stack_size: None,
35 max_pending_tasks: None,
36 enable_work_stealing: true,
37 }
38 }
39
40 pub fn num_threads(mut self, n: usize) -> Self {
42 self.num_threads = Some(n);
43 self
44 }
45
46 pub fn thread_name_prefix(mut self, prefix: impl Into<String>) -> Self {
48 self.thread_name_prefix = Some(prefix.into());
49 self
50 }
51
52 pub fn stack_size(mut self, size: usize) -> Self {
54 self.stack_size = Some(size);
55 self
56 }
57
58 pub fn max_pending_tasks(mut self, max: usize) -> Self {
60 self.max_pending_tasks = Some(max);
61 self
62 }
63
64 pub fn enable_work_stealing(mut self, enabled: bool) -> Self {
66 self.enable_work_stealing = enabled;
67 self
68 }
69}
70
71impl Default for ThreadPoolConfig {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77pub struct ThreadPoolBuilder {
79 config: ThreadPoolConfig,
80}
81
82impl ThreadPoolBuilder {
83 pub fn new() -> Self {
85 Self {
86 config: ThreadPoolConfig::default(),
87 }
88 }
89
90 pub fn num_threads(mut self, n: usize) -> Self {
92 self.config.num_threads = Some(n);
93 self
94 }
95
96 pub fn thread_name_prefix(mut self, prefix: impl Into<String>) -> Self {
98 self.config.thread_name_prefix = Some(prefix.into());
99 self
100 }
101
102 pub fn stack_size(mut self, size: usize) -> Self {
104 self.config.stack_size = Some(size);
105 self
106 }
107
108 pub fn max_pending_tasks(mut self, max: usize) -> Self {
110 self.config.max_pending_tasks = Some(max);
111 self
112 }
113
114 pub fn enable_work_stealing(mut self, enabled: bool) -> Self {
116 self.config.enable_work_stealing = enabled;
117 self
118 }
119
120 pub fn build(self) -> Result<ThreadPool> {
122 ThreadPool::with_config(self.config)
123 }
124}
125
126impl Default for ThreadPoolBuilder {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132type WorkItem = Box<dyn FnOnce() + Send + 'static>;
134
135pub struct WorkResult<T> {
137 handle: ThreadHandle<T>,
138}
139
140impl<T> WorkResult<T> {
141 pub fn wait(self) -> Result<T> {
143 self.handle.join()
144 }
145
146 #[cfg(not(target_arch = "wasm32"))]
148 pub fn wait_timeout(self, timeout: Duration) -> Result<T>
149 where
150 T: Send + 'static,
151 {
152 self.handle.join_timeout(timeout)
153 }
154
155 #[cfg(target_arch = "wasm32")]
156 pub fn wait_timeout(self, _timeout: Duration) -> Result<T> {
157 self.wait()
158 }
159}
160
161pub struct ThreadPool {
163 #[cfg(not(target_arch = "wasm32"))]
164 task_queue: Option<Arc<WasiQueue<WorkItem>>>,
165
166 #[cfg(not(target_arch = "wasm32"))]
167 workers: Vec<ThreadHandle<()>>,
168
169 #[cfg(not(target_arch = "wasm32"))]
170 rayon_pool: Option<Arc<rayon::ThreadPool>>,
171
172 #[cfg(target_arch = "wasm32")]
173 task_queue: Arc<WasiQueue<WorkItem>>,
174
175 #[allow(dead_code)]
176 config: ThreadPoolConfig,
177 is_shutdown: Arc<parking_lot::Mutex<bool>>,
178}
179
180impl ThreadPool {
181 pub fn new(num_threads: usize) -> Result<Self> {
183 let config = ThreadPoolConfig::new().num_threads(num_threads);
184 Self::with_config(config)
185 }
186
187 pub fn with_config(config: ThreadPoolConfig) -> Result<Self> {
189 let num_threads = config.num_threads.unwrap_or_else(|| {
190 #[cfg(not(target_arch = "wasm32"))]
191 {
192 WasiThread::available_parallelism()
193 }
194 #[cfg(target_arch = "wasm32")]
195 {
196 1
197 }
198 });
199
200 #[cfg(not(target_arch = "wasm32"))]
201 {
202 if config.enable_work_stealing {
204 let thread_name_prefix = config.thread_name_prefix.clone().unwrap_or_else(|| "worker".to_string());
205
206 let pool = rayon::ThreadPoolBuilder::new()
207 .num_threads(num_threads)
208 .thread_name(move |i| {
209 format!("{}-{}", thread_name_prefix, i)
210 })
211 .build()
212 .context("Failed to create rayon thread pool")?;
213
214 Ok(Self {
215 task_queue: None,
216 workers: Vec::new(),
217 rayon_pool: Some(Arc::new(pool)),
218 config,
219 is_shutdown: Arc::new(parking_lot::Mutex::new(false)),
220 })
221 } else {
222 let task_queue: Arc<WasiQueue<WorkItem>> = if let Some(max) = config.max_pending_tasks {
224 Arc::new(WasiQueue::with_capacity(max))
225 } else {
226 Arc::new(WasiQueue::new())
227 };
228
229 let mut workers = Vec::with_capacity(num_threads);
230 let is_shutdown = Arc::new(parking_lot::Mutex::new(false));
231
232 for i in 0..num_threads {
233 let queue_clone = task_queue.clone();
234 let shutdown_clone = is_shutdown.clone();
235 let name = format!("{}-{}", config.thread_name_prefix.as_deref().unwrap_or("worker"), i);
236
237 let thread_config = super::ThreadConfig::new().with_name(name);
238
239 let handle = WasiThread::spawn_with_config(move || {
240 loop {
241 if *shutdown_clone.lock() {
243 break;
244 }
245
246 if let Some(task) = queue_clone.try_pop() {
248 task();
249 } else {
250 super::thread_sleep(Duration::from_millis(10));
252 }
253 }
254 }, thread_config)?;
255
256 workers.push(handle);
257 }
258
259 Ok(Self {
260 task_queue: Some(task_queue),
261 workers,
262 rayon_pool: None,
263 config,
264 is_shutdown,
265 })
266 }
267 }
268
269 #[cfg(target_arch = "wasm32")]
270 {
271 let task_queue = if let Some(max) = config.max_pending_tasks {
273 Arc::new(WasiQueue::with_capacity(max))
274 } else {
275 Arc::new(WasiQueue::new())
276 };
277
278 Ok(Self {
279 task_queue,
280 config,
281 is_shutdown: Arc::new(parking_lot::Mutex::new(false)),
282 })
283 }
284 }
285
286 pub fn execute<F>(&self, f: F) -> Result<()>
288 where
289 F: FnOnce() + Send + 'static,
290 {
291 if *self.is_shutdown.lock() {
292 return Err(anyhow!(ThreadingError::InvalidOperation("Thread pool is shut down".to_string())));
293 }
294
295 #[cfg(not(target_arch = "wasm32"))]
296 {
297 if let Some(ref rayon_pool) = self.rayon_pool {
298 rayon_pool.spawn(f);
299 Ok(())
300 } else if let Some(ref queue) = self.task_queue {
301 queue.push(Box::new(f))
302 } else {
303 Err(anyhow!(ThreadingError::InvalidOperation("Thread pool not properly initialized".to_string())))
304 }
305 }
306
307 #[cfg(target_arch = "wasm32")]
308 {
309 self.task_queue.push(Box::new(f))
310 }
311 }
312
313 pub fn submit<F, T>(&self, f: F) -> Result<WorkResult<T>>
315 where
316 F: FnOnce() -> T + Send + 'static,
317 T: Send + 'static,
318 {
319 if *self.is_shutdown.lock() {
320 return Err(anyhow!(ThreadingError::InvalidOperation("Thread pool is shut down".to_string())));
321 }
322
323 let handle = WasiThread::spawn(f)?;
326 Ok(WorkResult { handle })
327 }
328
329 #[cfg(not(target_arch = "wasm32"))]
331 pub fn parallel_for_each<T, F>(&self, items: Vec<T>, f: F) -> Result<()>
332 where
333 T: Send,
334 F: Fn(T) + Send + Sync,
335 {
336 if let Some(ref pool) = self.rayon_pool {
337 pool.install(|| {
338 rayon::scope(|s| {
339 for item in items {
340 s.spawn(|_| f(item));
341 }
342 });
343 });
344 Ok(())
345 } else {
346 Err(anyhow!(ThreadingError::InvalidOperation("Parallel operations require work stealing enabled".to_string())))
347 }
348 }
349
350 #[cfg(not(target_arch = "wasm32"))]
352 pub fn parallel_map<T, R, F>(&self, items: Vec<T>, f: F) -> Result<Vec<R>>
353 where
354 T: Send,
355 R: Send,
356 F: Fn(T) -> R + Send + Sync,
357 {
358 if let Some(ref pool) = self.rayon_pool {
359 Ok(pool.install(|| {
360 use rayon::prelude::*;
361 items.into_par_iter().map(f).collect()
362 }))
363 } else {
364 Err(anyhow!(ThreadingError::InvalidOperation("Parallel operations require work stealing enabled".to_string())))
365 }
366 }
367
368 pub fn num_threads(&self) -> usize {
370 #[cfg(not(target_arch = "wasm32"))]
371 {
372 if let Some(ref pool) = self.rayon_pool {
373 pool.current_num_threads()
374 } else {
375 self.workers.len()
376 }
377 }
378
379 #[cfg(target_arch = "wasm32")]
380 {
381 self.config.num_threads.unwrap_or(1)
382 }
383 }
384
385 pub fn pending_tasks(&self) -> usize {
387 #[cfg(not(target_arch = "wasm32"))]
388 {
389 if let Some(ref queue) = self.task_queue {
390 queue.len()
391 } else {
392 0
393 }
394 }
395
396 #[cfg(target_arch = "wasm32")]
397 {
398 self.task_queue.len()
399 }
400 }
401
402 pub fn shutdown(self) -> Result<()> {
404 *self.is_shutdown.lock() = true;
405
406 #[cfg(not(target_arch = "wasm32"))]
407 {
408 for worker in self.workers {
410 worker.join()?;
411 }
412 }
413
414 Ok(())
415 }
416
417 #[cfg(not(target_arch = "wasm32"))]
419 pub fn shutdown_timeout(self, timeout: Duration) -> Result<()> {
420 *self.is_shutdown.lock() = true;
421
422 let start = std::time::Instant::now();
423 for worker in self.workers {
424 let remaining = timeout.saturating_sub(start.elapsed());
425 if remaining.is_zero() {
426 return Err(anyhow!(ThreadingError::Timeout("Thread pool shutdown timed out".to_string())));
427 }
428 worker.join_timeout(remaining)?;
429 }
430
431 Ok(())
432 }
433}
434
435static GLOBAL_POOL: once_cell::sync::Lazy<Result<ThreadPool>> = once_cell::sync::Lazy::new(|| {
437 ThreadPool::new(WasiThread::available_parallelism())
438});
439
440pub fn spawn<F>(f: F) -> Result<()>
442where
443 F: FnOnce() + Send + 'static,
444{
445 GLOBAL_POOL.as_ref()
446 .map_err(|e| anyhow!("Global thread pool not available: {}", e))?
447 .execute(f)
448}
449
450pub fn submit<F, T>(f: F) -> Result<WorkResult<T>>
452where
453 F: FnOnce() -> T + Send + 'static,
454 T: Send + 'static,
455{
456 GLOBAL_POOL.as_ref()
457 .map_err(|e| anyhow!("Global thread pool not available: {}", e))?
458 .submit(f)
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use std::sync::atomic::{AtomicUsize, Ordering};
465
466 #[test]
467 #[cfg(not(target_arch = "wasm32"))]
468 fn test_thread_pool_basic() {
469 let pool = ThreadPool::new(4).unwrap();
470 let counter = Arc::new(AtomicUsize::new(0));
471
472 for _ in 0..10 {
473 let counter_clone = counter.clone();
474 pool.execute(move || {
475 counter_clone.fetch_add(1, Ordering::SeqCst);
476 }).unwrap();
477 }
478
479 std::thread::sleep(Duration::from_millis(100));
481
482 assert_eq!(counter.load(Ordering::SeqCst), 10);
483 }
484
485 #[test]
486 #[cfg(not(target_arch = "wasm32"))]
487 fn test_thread_pool_submit() {
488 let pool = ThreadPool::new(2).unwrap();
489
490 let result = pool.submit(|| {
491 42
492 }).unwrap();
493
494 assert_eq!(result.wait().unwrap(), 42);
495 }
496
497 #[test]
498 #[cfg(not(target_arch = "wasm32"))]
499 fn test_thread_pool_rayon() {
500 let pool = ThreadPoolBuilder::new()
501 .num_threads(4)
502 .enable_work_stealing(true)
503 .build()
504 .unwrap();
505
506 let items = vec![1, 2, 3, 4, 5];
507 let results = pool.parallel_map(items, |x| x * 2).unwrap();
508
509 assert_eq!(results, vec![2, 4, 6, 8, 10]);
510 }
511
512 #[test]
513 fn test_thread_pool_config() {
514 let config = ThreadPoolConfig::new()
515 .num_threads(8)
516 .thread_name_prefix("test")
517 .max_pending_tasks(100);
518
519 assert_eq!(config.num_threads, Some(8));
520 assert_eq!(config.thread_name_prefix.as_deref(), Some("test"));
521 assert_eq!(config.max_pending_tasks, Some(100));
522 }
523}