Skip to main content

gpui_platform/
platform_scheduler.rs

1use crate::PlatformDispatcher;
2use async_task::Runnable;
3use chrono::{DateTime, Utc};
4use futures::channel::oneshot;
5use scheduler::Instant;
6use scheduler::RunnableMeta;
7use scheduler::{
8    Clock, LocalExecutor, Priority, Scheduler, SessionId, Task, TestScheduler, Timer,
9    spawn_dedicated_thread,
10};
11#[cfg(not(target_family = "wasm"))]
12use std::task::{Context, Poll};
13use std::{
14    any::Any,
15    future::Future,
16    pin::Pin,
17    sync::{
18        Arc,
19        atomic::{AtomicU16, Ordering},
20    },
21    time::Duration,
22};
23
24/// A production implementation of [`Scheduler`] that wraps a [`PlatformDispatcher`].
25///
26/// This allows GPUI to use the scheduler crate's executor types with the platform's
27/// native dispatch mechanisms (e.g., Grand Central Dispatch on macOS).
28pub struct PlatformScheduler {
29    dispatcher: Arc<dyn PlatformDispatcher>,
30    clock: Arc<PlatformClock>,
31    next_session_id: AtomicU16,
32    #[cfg(feature = "profiler")]
33    foreground_runnables: crate::profiler::ForegroundRunnableCounter,
34}
35
36impl PlatformScheduler {
37    /// Creates a scheduler that dispatches work through the given platform dispatcher.
38    pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
39        Self {
40            dispatcher: dispatcher.clone(),
41            clock: Arc::new(PlatformClock { dispatcher }),
42            next_session_id: AtomicU16::new(0),
43            #[cfg(feature = "profiler")]
44            foreground_runnables: crate::profiler::foreground_runnable_counter(),
45        }
46    }
47
48    /// Creates a foreground executor backed by this scheduler.
49    pub fn foreground_executor(self: &Arc<Self>) -> LocalExecutor {
50        let session_id = self.next_session_id();
51        let scheduler = Arc::downgrade(self);
52        LocalExecutor::new(session_id, self.clone(), move |runnable| {
53            if let Some(scheduler) = scheduler.upgrade() {
54                scheduler.schedule_local(session_id, runnable);
55            }
56        })
57    }
58
59    fn next_session_id(&self) -> SessionId {
60        SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst))
61    }
62
63    #[cfg(feature = "profiler")]
64    pub(crate) fn foreground_runnable_counter(&self) -> crate::profiler::ForegroundRunnableCounter {
65        self.foreground_runnables.clone()
66    }
67}
68
69impl Scheduler for PlatformScheduler {
70    #[cfg(not(target_family = "wasm"))]
71    fn block(
72        &self,
73        _session_id: Option<SessionId>,
74        mut future: Pin<&mut dyn Future<Output = ()>>,
75        timeout: Option<Duration>,
76    ) -> bool {
77        use waker_fn::waker_fn;
78        let deadline = timeout.map(|t| Instant::now() + t);
79        let parker = parking::Parker::new();
80        let unparker = parker.unparker();
81        let waker = waker_fn(move || {
82            unparker.unpark();
83        });
84        let mut cx = Context::from_waker(&waker);
85        if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
86            return true;
87        }
88
89        let park_deadline = |deadline: Instant| {
90            // Timer expirations are only delivered every ~15.6 milliseconds by default on Windows.
91            // We increase the resolution during this wait so that short timeouts stay reasonably short.
92            let _timer_guard = self.dispatcher.increase_timer_resolution();
93            parker.park_deadline(deadline)
94        };
95
96        loop {
97            match deadline {
98                Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => {
99                    return false;
100                }
101                Some(_) => (),
102                None => parker.park(),
103            }
104            if let Poll::Ready(()) = future.as_mut().poll(&mut cx) {
105                break true;
106            }
107        }
108    }
109
110    fn schedule_local(&self, _session_id: SessionId, runnable: Runnable<RunnableMeta>) {
111        #[cfg(feature = "profiler")]
112        self.foreground_runnables.queued();
113        self.dispatcher
114            .dispatch_on_main_thread(runnable, Priority::default());
115    }
116
117    fn schedule_background_with_priority(
118        &self,
119        runnable: Runnable<RunnableMeta>,
120        priority: Priority,
121    ) {
122        self.dispatcher.dispatch(runnable, priority);
123    }
124
125    fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
126        self.dispatcher.spawn_realtime(f);
127    }
128
129    #[track_caller]
130    fn timer(&self, duration: Duration) -> Timer {
131        let (tx, rx) = oneshot::channel();
132        let dispatcher = self.dispatcher.clone();
133
134        // Create a runnable that will send the completion signal
135        let location = std::panic::Location::caller();
136        let (runnable, _task) = async_task::Builder::new()
137            .metadata(RunnableMeta {
138                location,
139                spawned: scheduler::SpawnTime(Instant::now()),
140            })
141            .spawn(
142                move |_| async move {
143                    let _ = tx.send(());
144                },
145                move |runnable| {
146                    dispatcher.dispatch_after(duration, runnable);
147                },
148            );
149        runnable.schedule();
150
151        Timer::new(rx)
152    }
153
154    fn clock(&self) -> Arc<dyn Clock> {
155        self.clock.clone()
156    }
157
158    fn spawn_dedicated(
159        self: Arc<Self>,
160        f: Box<
161            dyn FnOnce(
162                    LocalExecutor,
163                )
164                    -> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
165                + Send
166                + 'static,
167        >,
168    ) -> Task<Box<dyn Any + Send + Sync>> {
169        let session_id = self.next_session_id();
170        spawn_dedicated_thread(session_id, self, move |executor| f(executor))
171    }
172
173    fn as_test(&self) -> Option<&TestScheduler> {
174        None
175    }
176}
177
178/// A production clock that uses the platform dispatcher's time.
179struct PlatformClock {
180    dispatcher: Arc<dyn PlatformDispatcher>,
181}
182
183impl Clock for PlatformClock {
184    fn utc_now(&self) -> DateTime<Utc> {
185        Utc::now()
186    }
187
188    fn now(&self) -> Instant {
189        self.dispatcher.now()
190    }
191}
192
193#[cfg(all(test, not(target_family = "wasm")))]
194mod tests {
195    use super::*;
196    use crate::RunnableVariant;
197    use scheduler::BackgroundExecutor;
198    use std::time::Instant as StdInstant;
199
200    // `spawn_dedicated` shouldn't touch the platform dispatcher at all;
201    // panicking on every method ensures the test catches it if it does.
202    struct SmokeDispatcher;
203
204    impl PlatformDispatcher for SmokeDispatcher {
205        fn is_main_thread(&self) -> bool {
206            false
207        }
208        fn dispatch(&self, _runnable: RunnableVariant, _priority: Priority) {
209            panic!("SmokeDispatcher should not be asked to dispatch in this test");
210        }
211        fn dispatch_on_main_thread(&self, _runnable: RunnableVariant, _priority: Priority) {
212            panic!("SmokeDispatcher does not implement a main thread");
213        }
214        fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
215            panic!("SmokeDispatcher does not implement timers");
216        }
217        fn spawn_realtime(&self, _f: Box<dyn FnOnce() + Send>) {
218            panic!("SmokeDispatcher does not implement realtime");
219        }
220    }
221
222    #[test]
223    fn dedicated_executor_tasks_share_one_thread() {
224        let background =
225            BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
226        let dedicated = scheduler::DedicatedExecutor::new(&background);
227
228        let first = dedicated.spawn(async { std::thread::current().id() });
229        let second = dedicated.spawn(async { std::thread::current().id() });
230
231        let first = futures::executor::block_on(first);
232        let second = futures::executor::block_on(second);
233
234        assert_eq!(first, second, "tasks must share the dedicated thread");
235        assert_ne!(
236            first,
237            std::thread::current().id(),
238            "dedicated tasks must not run on the spawning thread"
239        );
240    }
241
242    #[test]
243    fn spawn_dedicated_runs_on_a_real_separate_thread() {
244        let background =
245            BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
246        let started = StdInstant::now();
247        let task = background.spawn_dedicated(|_executor| async move {
248            // A genuine blocking syscall on the dedicated thread. If
249            // `spawn_dedicated` were running the future on any shared
250            // executor, this would stall that executor.
251            let thread_id_before = std::thread::current().id();
252            std::thread::sleep(Duration::from_millis(50));
253            let thread_id_after = std::thread::current().id();
254            assert_eq!(thread_id_before, thread_id_after);
255            (thread_id_before, "slept")
256        });
257        let (dedicated_thread_id, message) = futures::executor::block_on(task);
258        let elapsed = started.elapsed();
259        assert_eq!(message, "slept");
260        assert_ne!(
261            dedicated_thread_id,
262            std::thread::current().id(),
263            "dedicated future ran on the test thread"
264        );
265        assert!(
266            elapsed >= Duration::from_millis(40),
267            "expected the dedicated thread to genuinely sleep, elapsed = {:?}",
268            elapsed
269        );
270    }
271
272    #[test]
273    fn spawn_dedicated_returns_not_send_future_output() {
274        // The whole point of `spawn_dedicated` is that the future can be
275        // `!Send`. Constructing one with `Rc<RefCell<_>>` ensures the
276        // signature actually permits it.
277        use std::cell::RefCell;
278        use std::rc::Rc;
279
280        let background =
281            BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
282        let task = background.spawn_dedicated(|_executor| async move {
283            let state = Rc::new(RefCell::new(0_i32));
284            for _ in 0..3 {
285                *state.borrow_mut() += 1;
286            }
287            *state.borrow()
288        });
289        let output = futures::executor::block_on(task);
290        assert_eq!(output, 3);
291    }
292
293    #[test]
294    fn spawn_dedicated_dropping_task_cancels_future() {
295        use parking_lot::Mutex;
296        use std::sync::mpsc;
297
298        let background =
299            BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
300
301        let (started_tx, started_rx) = mpsc::channel::<()>();
302        let (after_park_tx, after_park_rx) = mpsc::channel::<()>();
303        let observed_post_await_write = Arc::new(Mutex::new(false));
304
305        let task = {
306            let observed_post_await_write = observed_post_await_write.clone();
307            background.spawn_dedicated(move |_executor| async move {
308                // Announce that the future is live on the dedicated thread.
309                started_tx
310                    .send(())
311                    .expect("started signal must be received");
312                // Park forever. Dropping the `Task` must cancel us here so
313                // the code below this `await` never runs.
314                futures::future::pending::<()>().await;
315                *observed_post_await_write.lock() = true;
316                after_park_tx
317                    .send(())
318                    .expect("after-park signal must be received");
319            })
320        };
321
322        // Wait until the dedicated future is actually parked at the await.
323        started_rx
324            .recv_timeout(Duration::from_secs(2))
325            .expect("dedicated future failed to start");
326
327        // Drop the root Task: this must cancel the future.
328        drop(task);
329
330        // If cancellation works, the future never advances past `pending`,
331        // so this recv must time out.
332        assert!(
333            after_park_rx
334                .recv_timeout(Duration::from_millis(100))
335                .is_err(),
336            "dedicated future advanced past the await after its Task was dropped"
337        );
338        assert!(
339            !*observed_post_await_write.lock(),
340            "dedicated future ran code past the cancellation point"
341        );
342    }
343
344    #[test]
345    fn spawn_dedicated_thread_tears_down_after_work_completes() {
346        use std::sync::mpsc;
347
348        // Fires from `Drop` so we observe teardown of the dedicated future's
349        // captured state on whichever thread runs its destructor.
350        struct DropSignal {
351            tx: Option<mpsc::Sender<std::thread::ThreadId>>,
352        }
353        impl Drop for DropSignal {
354            fn drop(&mut self) {
355                if let Some(tx) = self.tx.take() {
356                    let _ = tx.send(std::thread::current().id());
357                }
358            }
359        }
360
361        let background =
362            BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
363        let (started_tx, started_rx) = mpsc::channel::<std::thread::ThreadId>();
364        let (drop_tx, drop_rx) = mpsc::channel::<std::thread::ThreadId>();
365
366        let task = background.spawn_dedicated(move |_executor| async move {
367            // Captured by the future's state. When the future completes and
368            // its state is dropped on the dedicated thread, this guard's
369            // `Drop` fires and reports the thread id it ran on.
370            let _guard = DropSignal { tx: Some(drop_tx) };
371            started_tx
372                .send(std::thread::current().id())
373                .expect("started signal must be received");
374            // Future returns immediately. The dedicated thread should then
375            // drop the future (firing _guard), exit the recv loop, and exit.
376        });
377
378        let dedicated_thread_id = started_rx
379            .recv_timeout(Duration::from_secs(2))
380            .expect("dedicated future failed to start");
381        assert_ne!(
382            dedicated_thread_id,
383            std::thread::current().id(),
384            "dedicated future ran on the test thread"
385        );
386
387        // Drive the root task to completion so its body finishes.
388        futures::executor::block_on(task);
389
390        // The guard's drop runs from the dedicated thread as it tears down
391        // the future's captured state. If the executor/recv-loop were
392        // keeping the future alive past task completion, this would hang.
393        let drop_thread_id = drop_rx
394            .recv_timeout(Duration::from_secs(2))
395            .expect("dedicated future's captured state was not dropped after task completion");
396        assert_eq!(
397            drop_thread_id, dedicated_thread_id,
398            "dedicated future's captured state must be dropped on the dedicated thread, not elsewhere"
399        );
400    }
401
402    #[test]
403    fn spawn_dedicated_detached_child_outlives_root() {
404        use std::sync::mpsc;
405
406        let background =
407            BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
408
409        // `gate_rx` lets the detached child park until the test explicitly
410        // releases it — after we've already observed the root completing.
411        let (gate_tx, gate_rx) = mpsc::channel::<()>();
412        let (child_done_tx, child_done_rx) = mpsc::channel::<std::thread::ThreadId>();
413
414        let task = background.spawn_dedicated(move |executor| async move {
415            executor
416                .spawn(async move {
417                    // Blocking on `recv` is normally wrong inside an
418                    // executor, but the dedicated thread is exclusive to
419                    // this session, so blocking the only future on it is
420                    // fine — this is the property `spawn_dedicated` is
421                    // designed to provide.
422                    gate_rx
423                        .recv()
424                        .expect("gate sender dropped before child resumed");
425                    child_done_tx
426                        .send(std::thread::current().id())
427                        .expect("child_done receiver dropped");
428                })
429                .detach();
430            // Root finishes here. The detached child must keep the
431            // dedicated thread alive until it completes.
432        });
433
434        futures::executor::block_on(task);
435
436        // Negative assertion: the child has not finished, because the gate
437        // hasn't been released yet.
438        assert!(
439            child_done_rx
440                .recv_timeout(Duration::from_millis(50))
441                .is_err(),
442            "detached child finished before being released"
443        );
444
445        // Release the gate. The detached child should now complete on the
446        // dedicated thread.
447        gate_tx.send(()).expect("gate receiver dropped");
448
449        let child_thread_id = child_done_rx
450            .recv_timeout(Duration::from_secs(2))
451            .expect("detached child failed to complete after gate was released");
452        assert_ne!(
453            child_thread_id,
454            std::thread::current().id(),
455            "detached child ran on the test thread instead of the dedicated thread"
456        );
457    }
458}