Skip to main content

incular_runtime/
executor.rs

1//! Target executor behind [`TaskScheduler`](crate::tasks::TaskScheduler).
2//!
3//! Native targets run application work on a lazily started multi-thread Tokio
4//! runtime. Browser targets have one thread and an event loop owned by the
5//! page, so futures run as `spawn_local` microtasks and "blocking" work runs
6//! on that same thread after the current UI turn. Both backends report every
7//! outcome through the same completion callback; neither owns Incular task
8//! lifetime metadata.
9
10use std::{future::Future, time::Duration};
11
12use crate::tasks::TaskFailure;
13
14pub(crate) type Completion<T> = Box<dyn FnOnce(Result<T, TaskFailure>) + Send>;
15
16#[cfg(not(target_arch = "wasm32"))]
17pub(crate) use native::{AbortHandle, Executor};
18#[cfg(target_arch = "wasm32")]
19pub(crate) use web::{AbortHandle, Executor};
20
21/// Completes after `duration` on every target: a Tokio timer natively and a
22/// `setTimeout` in browsers.
23///
24/// Await it inside work started through the application's spawner (for
25/// example [`RuntimeSpawner::spawn_into`](crate::RuntimeSpawner::spawn_into));
26/// natively it needs the Tokio runtime those tasks run on.
27pub async fn sleep(duration: Duration) {
28    #[cfg(not(target_arch = "wasm32"))]
29    tokio::time::sleep(duration).await;
30    #[cfg(target_arch = "wasm32")]
31    web::sleep(duration).await;
32}
33
34#[cfg(not(target_arch = "wasm32"))]
35mod native {
36    use super::{Completion, Duration, Future, TaskFailure};
37    use tokio::runtime::{Builder, Handle, Runtime};
38
39    pub(crate) use tokio::task::AbortHandle;
40
41    const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100);
42
43    pub(crate) struct Executor {
44        /// Started by the first task: most windows never spawn async work,
45        /// and a multi-thread runtime parks one worker thread per core.
46        runtime: std::cell::OnceCell<Runtime>,
47        /// Handle of the shut-down runtime; later spawns resolve as stopped
48        /// instead of starting a new runtime.
49        stopped: Option<Handle>,
50    }
51
52    impl Executor {
53        pub(crate) fn new() -> Self {
54            Self {
55                runtime: std::cell::OnceCell::new(),
56                stopped: None,
57            }
58        }
59
60        pub(crate) fn handle(&self) -> Handle {
61            if let Some(stopped) = &self.stopped {
62                return stopped.clone();
63            }
64            self.runtime
65                .get_or_init(|| {
66                    Builder::new_multi_thread()
67                        .enable_time()
68                        .enable_io()
69                        .build()
70                        .expect("Tokio runtime construction")
71                })
72                .handle()
73                .clone()
74        }
75
76        pub(crate) fn spawn<F, T>(&self, future: F, complete: Completion<T>) -> AbortHandle
77        where
78            F: Future<Output = T> + Send + 'static,
79            T: Send + 'static,
80        {
81            let handle = self.handle();
82            let join = handle.spawn(future);
83            let abort = join.abort_handle();
84            handle.spawn(async move { complete(join_outcome(join.await)) });
85            abort
86        }
87
88        pub(crate) fn spawn_blocking<T>(
89            &self,
90            work: impl FnOnce() -> T + Send + 'static,
91            complete: Completion<T>,
92        ) -> AbortHandle
93        where
94            T: Send + 'static,
95        {
96            let handle = self.handle();
97            let join = handle.spawn_blocking(work);
98            let abort = join.abort_handle();
99            handle.spawn(async move { complete(join_outcome(join.await)) });
100            abort
101        }
102
103        pub(crate) fn shutdown(&mut self) {
104            // A runtime that never started is replaced by a thread-free one so
105            // spawns after shutdown keep resolving as stopped.
106            let runtime = self.runtime.take().unwrap_or_else(|| {
107                Builder::new_current_thread()
108                    .build()
109                    .expect("Tokio runtime construction")
110            });
111            self.stopped = Some(runtime.handle().clone());
112            // Tokio does not force-stop an already-running blocking closure.
113            // The bounded wait keeps UI shutdown from hanging indefinitely.
114            runtime.shutdown_timeout(SHUTDOWN_TIMEOUT);
115        }
116    }
117
118    fn join_outcome<T>(result: Result<T, tokio::task::JoinError>) -> Result<T, TaskFailure> {
119        match result {
120            Ok(value) => Ok(value),
121            Err(error) if error.is_cancelled() => Err(TaskFailure::Cancelled),
122            Err(error) if error.is_panic() => Err(TaskFailure::Panicked),
123            Err(_) => Err(TaskFailure::RuntimeStopped),
124        }
125    }
126}
127
128#[cfg(target_arch = "wasm32")]
129mod web {
130    use std::{
131        pin::Pin,
132        sync::{
133            Arc,
134            atomic::{AtomicBool, Ordering},
135        },
136        task::{Context, Poll, Waker},
137    };
138
139    use super::{Completion, Duration, Future, TaskFailure};
140    use wasm_bindgen::{JsCast, prelude::*};
141
142    /// Cancels a browser task before its next poll. Unlike Tokio, the browser
143    /// executor cannot preempt a running microtask, so cancellation of
144    /// "blocking" work that already started discards only its result.
145    #[derive(Clone)]
146    pub(crate) struct AbortHandle {
147        aborted: Arc<AtomicBool>,
148        waker: Arc<std::sync::Mutex<Option<Waker>>>,
149    }
150
151    impl AbortHandle {
152        pub(crate) fn abort(&self) {
153            self.aborted.store(true, Ordering::Release);
154            if let Some(waker) = self.waker.lock().expect("abort waker mutex").take() {
155                waker.wake();
156            }
157        }
158    }
159
160    struct Abortable<F> {
161        future: Pin<Box<F>>,
162        handle: AbortHandle,
163    }
164
165    impl<F: Future> Future for Abortable<F> {
166        type Output = Result<F::Output, TaskFailure>;
167
168        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
169            if self.handle.aborted.load(Ordering::Acquire) {
170                return Poll::Ready(Err(TaskFailure::Cancelled));
171            }
172            *self.handle.waker.lock().expect("abort waker mutex") = Some(cx.waker().clone());
173            self.future.as_mut().poll(cx).map(Ok)
174        }
175    }
176
177    pub(crate) struct Executor {
178        stopped: bool,
179    }
180
181    impl Executor {
182        pub(crate) const fn new() -> Self {
183            Self { stopped: false }
184        }
185
186        pub(crate) fn spawn<F, T>(&self, future: F, complete: Completion<T>) -> AbortHandle
187        where
188            F: Future<Output = T> + Send + 'static,
189            T: Send + 'static,
190        {
191            let handle = AbortHandle {
192                aborted: Arc::new(AtomicBool::new(self.stopped)),
193                waker: Arc::new(std::sync::Mutex::new(None)),
194            };
195            let abortable = Abortable {
196                future: Box::pin(future),
197                handle: handle.clone(),
198            };
199            wasm_bindgen_futures::spawn_local(async move { complete(abortable.await) });
200            handle
201        }
202
203        pub(crate) fn spawn_blocking<T>(
204            &self,
205            work: impl FnOnce() -> T + Send + 'static,
206            complete: Completion<T>,
207        ) -> AbortHandle
208        where
209            T: Send + 'static,
210        {
211            // There is no blocking pool in a browser. Deferring to a microtask
212            // keeps the call non-reentrant: the work runs after this UI turn.
213            self.spawn(async move { work() }, complete)
214        }
215
216        pub(crate) fn shutdown(&mut self) {
217            self.stopped = true;
218        }
219    }
220
221    #[wasm_bindgen]
222    extern "C" {
223        #[wasm_bindgen(js_name = setTimeout)]
224        fn set_timeout(handler: &js_sys::Function, timeout: i32) -> JsValue;
225    }
226
227    #[derive(Default)]
228    struct TimerState {
229        fired: bool,
230        waker: Option<Waker>,
231    }
232
233    /// A `setTimeout` future. It holds no JavaScript values, so it stays
234    /// `Send` like the Tokio timer it replaces, and it uses the global scope
235    /// so it also works in workers, where there is no `window`.
236    struct Sleep {
237        millis: i32,
238        armed: bool,
239        state: Arc<std::sync::Mutex<TimerState>>,
240    }
241
242    impl Future for Sleep {
243        type Output = ();
244
245        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
246            let mut state = self.state.lock().expect("timer state mutex");
247            if state.fired {
248                return Poll::Ready(());
249            }
250            state.waker = Some(cx.waker().clone());
251            drop(state);
252            if !self.armed {
253                self.armed = true;
254                let shared = self.state.clone();
255                let fire = Closure::once_into_js(move || {
256                    let mut state = shared.lock().expect("timer state mutex");
257                    state.fired = true;
258                    if let Some(waker) = state.waker.take() {
259                        waker.wake();
260                    }
261                });
262                let _ = set_timeout(fire.unchecked_ref(), self.millis);
263            }
264            Poll::Pending
265        }
266    }
267
268    pub(super) async fn sleep(duration: Duration) {
269        Sleep {
270            millis: i32::try_from(duration.as_millis()).unwrap_or(i32::MAX),
271            armed: false,
272            state: Arc::default(),
273        }
274        .await;
275    }
276}