use futures::{channel::oneshot, future::try_join_all};
use rayon::ThreadBuilder;
use spmc::{channel, Receiver, Sender};
use std::future::Future;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
#[wasm_bindgen(inline_js = r###"
export function spawnWorker(url, module, memory, address) {
return new Promise((resolve) => {
const worker = new Worker(url, {
type: "module",
});
worker.addEventListener("message", (event) => {
// This is needed in Node to wait one extra tick, so that way
// the Worker can fully initialize before we return.
setTimeout(() => {
resolve(worker);
// When running in Node, this allows the process to exit
// even though the Worker is still running.
if (worker.unref) {
worker.unref();
}
}, 0);
}, {
capture: true,
once: true,
});
worker.postMessage({
module,
memory,
address,
});
});
}
export function startTimer() {
// Starts a super-long timer in order to keep the Node
// process alive until we manually cancel it.
return setTimeout(() => {}, Math.pow(2, 31) - 1);
}
export function stopTimer(timer) {
clearTimeout(timer);
}
"###)]
extern "C" {
#[wasm_bindgen(js_name = spawnWorker)]
fn spawn_worker(
url: &web_sys::Url,
module: &JsValue,
memory: &JsValue,
address: *const Receiver<ThreadBuilder>,
) -> js_sys::Promise;
#[wasm_bindgen(js_name = startTimer)]
fn start_timer() -> f64;
#[wasm_bindgen(js_name = stopTimer)]
fn stop_timer(timer: f64);
}
#[allow(dead_code)]
pub fn spawn<A, F>(f: F) -> impl Future<Output = A>
where
A: Send + 'static,
F: FnOnce() -> A + Send + 'static,
{
struct Timer(f64);
impl Drop for Timer {
fn drop(&mut self) {
stop_timer(self.0);
}
}
let timer = Timer(start_timer());
let (sender, receiver) = oneshot::channel();
rayon::spawn(move || {
let _ = sender.send(f());
});
async move {
let output = receiver.await.unwrap_throw();
drop(timer);
output
}
}
async fn spawn_workers(url: web_sys::Url, num_threads: usize) -> Result<Sender<ThreadBuilder>, JsValue> {
let module = wasm_bindgen::module();
let memory = wasm_bindgen::memory();
let (sender, receiver) = channel();
let receiver = Box::leak(Box::new(receiver));
let workers =
try_join_all((0..num_threads).map(|_| JsFuture::from(spawn_worker(&url, &module, &memory, receiver)))).await?;
std::mem::forget(workers);
Ok(sender)
}
async fn spawn_global_thread_pool(url: web_sys::Url, num_threads: usize) -> Result<(), JsValue> {
if num_threads == 1 {
rayon::ThreadPoolBuilder::new().num_threads(1).use_current_thread().build_global().unwrap_throw();
} else {
let mut sender = spawn_workers(url, num_threads).await?;
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.spawn_handler(move |thread| {
sender.send(thread).unwrap_throw();
Ok(())
})
.build_global()
.unwrap_throw();
}
Ok(())
}
pub struct ThreadPool {
url: Option<web_sys::Url>,
num_threads: Option<usize>,
}
impl ThreadPool {
pub fn builder() -> Self {
Self { url: None, num_threads: None }
}
pub fn url(mut self, url: web_sys::Url) -> Self {
self.url = Some(url);
self
}
pub fn num_threads(mut self, num_threads: usize) -> Self {
self.num_threads = Some(num_threads);
self
}
fn defaults(self) -> (web_sys::Url, usize) {
(
self.url.expect("Missing url for ThreadPool"),
self.num_threads.unwrap_or_else(|| {
let window: web_sys::Window = js_sys::global().unchecked_into();
window.navigator().hardware_concurrency() as usize
}),
)
}
pub fn build_global(self) -> impl Future<Output = Result<(), JsValue>> {
let (url, num_threads) = self.defaults();
spawn_global_thread_pool(url, num_threads)
}
}
#[doc(hidden)]
#[wasm_bindgen(js_name = runRayonThread)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn run_rayon_thread(receiver: *const Receiver<ThreadBuilder>)
where
Receiver<ThreadBuilder>: Sync,
{
let receiver = unsafe { &*receiver };
receiver.recv().unwrap_throw().run();
}