scheduler/scheduler.rs
1mod clock;
2mod executor;
3mod test_scheduler;
4#[cfg(test)]
5mod tests;
6
7pub use clock::*;
8pub use executor::*;
9pub use test_scheduler::*;
10
11use async_task::Runnable;
12use futures::channel::oneshot;
13use std::{
14 any::Any,
15 future::Future,
16 panic::Location,
17 pin::Pin,
18 sync::Arc,
19 task::{Context, Poll},
20 time::Duration,
21};
22
23/// Task priority for background tasks.
24///
25/// Higher priority tasks are more likely to be scheduled before lower priority tasks,
26/// but this is not a strict guarantee - the scheduler may interleave tasks of different
27/// priorities to prevent starvation.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
29#[repr(u8)]
30pub enum Priority {
31 /// Realtime priority
32 ///
33 /// Spawning a task with this priority will spin it off on a separate thread dedicated just to that task. Only use for audio.
34 RealtimeAudio,
35 /// High priority - use for tasks critical to user experience/responsiveness.
36 High,
37 /// Medium priority - suitable for most use cases.
38 #[default]
39 Medium,
40 /// Low priority - use for background work that can be deprioritized.
41 Low,
42}
43
44impl Priority {
45 /// Returns the relative probability weight for this priority level.
46 /// Used by schedulers to determine task selection probability.
47 pub const fn weight(self) -> u32 {
48 match self {
49 Priority::High => 60,
50 Priority::Medium => 30,
51 Priority::Low => 10,
52 // realtime priorities are not considered for probability scheduling
53 Priority::RealtimeAudio => 0,
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug)]
59pub struct SpawnTime(pub Instant);
60
61/// Metadata attached to runnables for debugging and profiling.
62#[derive(Clone, Debug)]
63pub struct RunnableMeta {
64 /// The source location where the task was spawned.
65 pub location: &'static Location<'static>,
66 /// The moment the task was spawned.
67 pub spawned: SpawnTime,
68}
69
70impl RunnableMeta {
71 #[track_caller]
72 pub fn new_with_callers_location() -> Self {
73 Self {
74 location: core::panic::Location::caller(),
75 spawned: SpawnTime(Instant::now()),
76 }
77 }
78}
79
80pub trait Scheduler: Send + Sync {
81 /// Block until the given future completes or timeout occurs.
82 ///
83 /// Returns `true` if the future completed, `false` if it timed out.
84 /// The future is passed as a pinned mutable reference so the caller
85 /// retains ownership and can continue polling or return it on timeout.
86 #[cfg(not(target_family = "wasm"))]
87 fn block(
88 &self,
89 session_id: Option<SessionId>,
90 future: Pin<&mut dyn Future<Output = ()>>,
91 timeout: Option<Duration>,
92 ) -> bool;
93
94 /// Schedule a runnable on the local (session-pinned) queue for `session_id`.
95 /// Runnables scheduled here run in order on whichever thread drains the
96 /// session — the main thread for ordinary sessions, or a dedicated OS
97 /// thread for sessions created via `spawn_dedicated_thread`.
98 fn schedule_local(&self, session_id: SessionId, runnable: Runnable<RunnableMeta>);
99
100 /// Schedule a background task with the given priority.
101 fn schedule_background_with_priority(
102 &self,
103 runnable: Runnable<RunnableMeta>,
104 priority: Priority,
105 );
106
107 /// Spawn a closure on a dedicated realtime thread for audio processing.
108 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
109
110 /// Schedule a background task with default (medium) priority.
111 fn schedule_background(&self, runnable: Runnable<RunnableMeta>) {
112 self.schedule_background_with_priority(runnable, Priority::default());
113 }
114
115 #[track_caller]
116 fn timer(&self, timeout: Duration) -> Timer;
117 fn clock(&self) -> Arc<dyn Clock>;
118
119 /// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`].
120 ///
121 /// `PlatformScheduler` runs the closure on a new OS thread (see
122 /// [`spawn_dedicated_thread`]). `TestScheduler` runs it on the test
123 /// scheduler's loop alongside everything else so determinism under
124 /// `TestScheduler::many` is preserved.
125 ///
126 /// This is the dyn-safe entry point: the closure's output is type-erased
127 /// as `Box<dyn Any + Send + Sync>` so the trait stays object-safe.
128 /// Callers typically reach for the type-safe wrappers on
129 /// [`LocalExecutor::spawn_dedicated`] and
130 /// [`BackgroundExecutor::spawn_dedicated`], which compose this method
131 /// with [`Task::downcast`] to recover the closure's concrete return type.
132 fn spawn_dedicated(
133 self: Arc<Self>,
134 f: Box<
135 dyn FnOnce(
136 LocalExecutor,
137 )
138 -> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
139 + Send
140 + 'static,
141 >,
142 ) -> Task<Box<dyn Any + Send + Sync>>;
143
144 fn as_test(&self) -> Option<&TestScheduler> {
145 None
146 }
147}
148
149/// Spawn work on a fresh OS thread that's exclusive to the returned task and
150/// anything spawned on the executor it provides. Blocking syscalls inside that
151/// work don't disturb any other executor in the process.
152///
153/// `f` is called on the dedicated thread with a [`LocalExecutor`] pinned
154/// to it. The future `f` returns may freely be `!Send`. The returned `Task`
155/// resolves to that future's output: dropping it cancels the root, but
156/// detached children keep running until they finish. The thread shuts down
157/// once the executor and every task on it are gone.
158///
159/// This function never blocks: the returned task starts as an asynchronous
160/// rendezvous that resolves to the root task's handle once the dedicated
161/// thread has spawned it, and behaves like that handle from then on. That
162/// makes it safe to call from threads that must not block, such as the web
163/// main thread. On wasm targets the thread is a web worker and requires the
164/// `wasm-threads` cargo feature (and a shared-memory build); without it this
165/// panics. Spawning a worker is far more expensive than an OS thread —
166/// module instantiation plus a fresh function table — so on the web treat a
167/// dedicated session as a long-lived place to send work, not a per-job
168/// convenience.
169///
170/// The caller is responsible for supplying a `session_id` that's distinct from
171/// every other live session on `scheduler`. Concrete schedulers typically wrap
172/// this in an inherent method that allocates the id from their own counter.
173pub fn spawn_dedicated_thread<F, Fut>(
174 session_id: SessionId,
175 scheduler: Arc<dyn Scheduler>,
176 f: F,
177) -> Task<Fut::Output>
178where
179 F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
180 Fut: Future + 'static,
181 Fut::Output: Send + 'static,
182{
183 let (task, delivery) = Task::rendezvous();
184 let thread_body = move || {
185 let (runnable_sender, runnable_receiver) = flume::unbounded::<Runnable<RunnableMeta>>();
186 let dispatch = move |runnable: Runnable<RunnableMeta>| {
187 let _ = runnable_sender.send(runnable);
188 };
189 let executor = LocalExecutor::new(session_id, scheduler, dispatch);
190 let root_task = executor.spawn(f(executor.clone()));
191 // If the caller already dropped or detached the rendezvous task,
192 // delivery applies that disposition to the root task here.
193 delivery.deliver(root_task);
194 // After this drop, every strong reference to the runnable sender
195 // lives inside a spawned task or a user-held executor clone. The
196 // recv loop exits once all of those are gone.
197 drop(executor);
198
199 while let Ok(runnable) = runnable_receiver.recv() {
200 runnable.run();
201 }
202 };
203 spawn_dedicated_os_thread(session_id, thread_body);
204 task
205}
206
207fn spawn_dedicated_os_thread(session_id: SessionId, thread_body: impl FnOnce() + Send + 'static) {
208 let thread_name = format!("spawn_dedicated session {:?}", session_id);
209 #[cfg(not(target_family = "wasm"))]
210 std::thread::Builder::new()
211 .name(thread_name)
212 .spawn(thread_body)
213 .expect("failed to spawn dedicated thread");
214 #[cfg(all(target_family = "wasm", feature = "wasm-threads"))]
215 wasm_thread::Builder::new()
216 .name(thread_name)
217 .spawn(thread_body)
218 .expect("failed to spawn dedicated thread");
219 #[cfg(all(target_family = "wasm", not(feature = "wasm-threads")))]
220 {
221 let _ = (thread_name, thread_body);
222 panic!("spawn_dedicated on wasm requires the scheduler crate's `wasm-threads` feature");
223 }
224}
225
226#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
227pub struct SessionId(u16);
228
229impl SessionId {
230 pub fn new(id: u16) -> Self {
231 SessionId(id)
232 }
233}
234
235pub struct Timer(oneshot::Receiver<()>);
236
237impl Timer {
238 pub fn new(rx: oneshot::Receiver<()>) -> Self {
239 Timer(rx)
240 }
241}
242
243impl Future for Timer {
244 type Output = ();
245
246 fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
247 match Pin::new(&mut self.0).poll(cx) {
248 Poll::Ready(_) => Poll::Ready(()),
249 Poll::Pending => Poll::Pending,
250 }
251 }
252}