Skip to main content

gpui_platform/
executor_runtime.rs

1use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc, time::Duration};
2#[cfg(not(target_family = "wasm"))]
3use std::{mem, pin::Pin};
4
5#[cfg(not(target_family = "wasm"))]
6use futures::channel::mpsc;
7use futures::prelude::*;
8use scheduler::Instant;
9use scheduler::Scheduler;
10
11use crate::{ActivityGuard, PlatformDispatcher, PlatformScheduler};
12
13pub use scheduler::{
14    DedicatedExecutor, FallibleTask, LocalExecutor as SchedulerLocalExecutor, Priority,
15    RunnableMeta, Task,
16};
17
18/// A pointer to the executor that is currently running,
19/// for spawning background tasks.
20#[derive(Clone)]
21pub struct BackgroundExecutor {
22    inner: scheduler::BackgroundExecutor,
23    dispatcher: Arc<dyn PlatformDispatcher>,
24}
25
26/// A pointer to the executor that is currently running,
27/// for spawning tasks on the main thread.
28#[derive(Clone)]
29pub struct ForegroundExecutor {
30    inner: scheduler::LocalExecutor,
31    dispatcher: Arc<dyn PlatformDispatcher>,
32    #[cfg(feature = "profiler")]
33    foreground_runnables: Option<crate::profiler::ForegroundRunnableCounter>,
34    not_send: PhantomData<Rc<()>>,
35}
36
37/// A pointer to the executor that is currently running,
38/// for spawning background tasks.
39impl BackgroundExecutor {
40    /// Creates a new BackgroundExecutor from the given PlatformDispatcher.
41    pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
42        #[cfg(any(test, feature = "test-support"))]
43        let scheduler: Arc<dyn Scheduler> = if let Some(test_dispatcher) = dispatcher.as_test() {
44            test_dispatcher.scheduler().clone()
45        } else {
46            Arc::new(PlatformScheduler::new(dispatcher.clone()))
47        };
48
49        #[cfg(not(any(test, feature = "test-support")))]
50        let scheduler: Arc<dyn Scheduler> = Arc::new(PlatformScheduler::new(dispatcher.clone()));
51
52        Self {
53            inner: scheduler::BackgroundExecutor::new(scheduler),
54            dispatcher,
55        }
56    }
57
58    /// Returns the underlying scheduler::BackgroundExecutor.
59    ///
60    /// This is used by Ex to pass the executor to thread/worktree code.
61    pub fn scheduler_executor(&self) -> scheduler::BackgroundExecutor {
62        self.inner.clone()
63    }
64
65    /// Spawn a closure on a fresh session pinned to its own
66    /// [`SchedulerLocalExecutor`]. The closure runs on a new OS thread under the
67    /// platform scheduler, or on the test scheduler's loop in tests.
68    ///
69    /// Prefer this over [`Self::spawn`] for futures whose polls need more stack
70    /// than shared background threads guarantee. Dedicated threads get the
71    /// standard library's default 2 MiB, while `spawn` polls futures on whatever
72    /// threads the platform dispatcher provides — on macOS those are GCD workers
73    /// whose stacks are fixed at 512 KiB by the kernel.
74    ///
75    /// Kept from the base: `agent_servers` builds its ACP connection on it, and
76    /// the refactor's version of this file only has `prevent_app_nap`.
77    #[track_caller]
78    pub fn spawn_dedicated<F, Fut>(&self, f: F) -> Task<Fut::Output>
79    where
80        F: FnOnce(SchedulerLocalExecutor) -> Fut + Send + 'static,
81        Fut: Future + 'static,
82        Fut::Output: Send + Sync + 'static,
83    {
84        self.inner.spawn_dedicated(f)
85    }
86
87    /// Prevents App Nap-style throttling while the returned guard is held.
88    ///
89    /// This does not prevent the system from entering idle sleep.
90    pub fn prevent_app_nap(&self, reason: &str) -> ActivityGuard {
91        self.dispatcher.prevent_app_nap(reason)
92    }
93
94    /// Enqueues the given future to be run to completion on a background thread.
95    #[track_caller]
96    pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
97    where
98        R: Send + 'static,
99    {
100        self.spawn_with_priority(Priority::default(), future.boxed())
101    }
102
103    /// Enqueues the given future to be run to completion on a background thread with the given priority.
104    ///
105    /// When `Priority::RealtimeAudio` is used, the task runs on a dedicated thread with
106    /// realtime scheduling priority, suitable for audio processing.
107    #[track_caller]
108    pub fn spawn_with_priority<R>(
109        &self,
110        priority: Priority,
111        future: impl Future<Output = R> + Send + 'static,
112    ) -> Task<R>
113    where
114        R: Send + 'static,
115    {
116        if priority == Priority::RealtimeAudio {
117            self.inner.spawn_realtime(future)
118        } else {
119            self.inner.spawn_with_priority(priority, future)
120        }
121    }
122
123    /// Runs background tasks that may borrow from their environment and waits for all of them to complete.
124    ///
125    /// Dropping the returned future cancels its tasks and synchronously waits for their futures to
126    /// be destroyed before returning.
127    #[cfg(not(target_family = "wasm"))]
128    pub async fn scoped<'scope, F>(&self, scheduler: F)
129    where
130        F: FnOnce(&mut Scope<'scope>),
131    {
132        let mut scope = Scope::new(self.clone(), Priority::default());
133        (scheduler)(&mut scope);
134        let spawned = mem::take(&mut scope.futures)
135            .into_iter()
136            .map(|f| self.spawn_with_priority(scope.priority, f))
137            .collect::<Vec<_>>();
138        for task in spawned {
139            task.await;
140        }
141    }
142
143    /// Runs prioritized background tasks that may borrow from their environment and waits for all
144    /// of them to complete.
145    ///
146    /// Dropping the returned future cancels its tasks and synchronously waits for their futures to
147    /// be destroyed before returning.
148    #[cfg(not(target_family = "wasm"))]
149    pub async fn scoped_priority<'scope, F>(&self, priority: Priority, scheduler: F)
150    where
151        F: FnOnce(&mut Scope<'scope>),
152    {
153        let mut scope = Scope::new(self.clone(), priority);
154        (scheduler)(&mut scope);
155        let spawned = mem::take(&mut scope.futures)
156            .into_iter()
157            .map(|f| self.spawn_with_priority(scope.priority, f))
158            .collect::<Vec<_>>();
159        for task in spawned {
160            task.await;
161        }
162    }
163
164    /// Get the current time.
165    ///
166    /// Calling this instead of `std::time::Instant::now` allows the use
167    /// of fake timers in tests.
168    pub fn now(&self) -> Instant {
169        self.inner.scheduler().clock().now()
170    }
171
172    /// Returns a task that will complete after the given duration.
173    /// Depending on other concurrent tasks the elapsed duration may be longer
174    /// than requested.
175    #[track_caller]
176    pub fn timer(&self, duration: Duration) -> Task<()> {
177        if duration.is_zero() {
178            return Task::ready(());
179        }
180        self.spawn(self.inner.scheduler().timer(duration))
181    }
182
183    /// In tests, run an arbitrary number of tasks (determined by the SEED environment variable)
184    #[cfg(any(test, feature = "test-support"))]
185    pub fn simulate_random_delay(&self) -> impl Future<Output = ()> + use<> {
186        self.dispatcher.as_test().unwrap().simulate_random_delay()
187    }
188
189    /// In tests, move time forward. This does not run any tasks, but does make `timer`s ready.
190    #[cfg(any(test, feature = "test-support"))]
191    pub fn advance_clock(&self, duration: Duration) {
192        self.dispatcher.as_test().unwrap().advance_clock(duration)
193    }
194
195    /// In tests, run one task.
196    #[cfg(any(test, feature = "test-support"))]
197    pub fn tick(&self) -> bool {
198        self.dispatcher.as_test().unwrap().scheduler().tick()
199    }
200
201    /// In tests, run tasks until the scheduler would park.
202    ///
203    /// Under the scheduler-backed test dispatcher, `tick()` will not advance the clock, so a pending
204    /// timer can keep `has_pending_tasks()` true even after all currently-runnable tasks have been
205    /// drained. To preserve the historical semantics that tests relied on (drain all work that can
206    /// make progress), we advance the clock to the next timer when no runnable tasks remain.
207    #[cfg(any(test, feature = "test-support"))]
208    pub fn run_until_parked(&self) {
209        let scheduler = self.dispatcher.as_test().unwrap().scheduler();
210        scheduler.run();
211    }
212
213    /// In tests, prevents `run_until_parked` from panicking if there are outstanding tasks.
214    #[cfg(any(test, feature = "test-support"))]
215    pub fn allow_parking(&self) {
216        self.dispatcher
217            .as_test()
218            .unwrap()
219            .scheduler()
220            .allow_parking();
221
222        if std::env::var("GPUI_RUN_UNTIL_PARKED_LOG").ok().as_deref() == Some("1") {
223            log::warn!("[gpui::executor] allow_parking: enabled");
224        }
225    }
226
227    /// Sets the range of ticks to run before timing out in block_on.
228    #[cfg(any(test, feature = "test-support"))]
229    pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
230        self.dispatcher
231            .as_test()
232            .unwrap()
233            .scheduler()
234            .set_timeout_ticks(range);
235    }
236
237    /// Undoes the effect of [`Self::allow_parking`].
238    #[cfg(any(test, feature = "test-support"))]
239    pub fn forbid_parking(&self) {
240        self.dispatcher
241            .as_test()
242            .unwrap()
243            .scheduler()
244            .forbid_parking();
245    }
246
247    /// In tests, returns the rng used by the dispatcher.
248    #[cfg(any(test, feature = "test-support"))]
249    pub fn rng(&self) -> scheduler::SharedRng {
250        self.dispatcher.as_test().unwrap().scheduler().rng()
251    }
252
253    /// How many CPUs are available to the dispatcher.
254    pub fn num_cpus(&self) -> usize {
255        #[cfg(any(test, feature = "test-support"))]
256        if let Some(test) = self.dispatcher.as_test() {
257            return test.num_cpus_override().unwrap_or(4);
258        }
259        num_cpus::get()
260    }
261
262    /// Override the number of CPUs reported by this executor in tests.
263    /// Panics if not called on a test executor.
264    #[cfg(any(test, feature = "test-support"))]
265    pub fn set_num_cpus(&self, count: usize) {
266        self.dispatcher
267            .as_test()
268            .expect("set_num_cpus can only be called on a test executor")
269            .set_num_cpus(count);
270    }
271
272    /// Whether we're on the main thread.
273    pub fn is_main_thread(&self) -> bool {
274        self.dispatcher.is_main_thread()
275    }
276
277    #[doc(hidden)]
278    pub fn dispatcher(&self) -> &Arc<dyn PlatformDispatcher> {
279        &self.dispatcher
280    }
281}
282
283impl ForegroundExecutor {
284    /// Creates a new ForegroundExecutor from the given PlatformDispatcher.
285    pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
286        #[cfg(any(test, feature = "test-support"))]
287        let (scheduler, session_id): (Arc<dyn Scheduler>, _) =
288            if let Some(test_dispatcher) = dispatcher.as_test() {
289                (
290                    test_dispatcher.scheduler().clone(),
291                    test_dispatcher.session_id(),
292                )
293            } else {
294                let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
295                let inner = platform_scheduler.foreground_executor();
296                return Self {
297                    inner,
298                    dispatcher,
299                    #[cfg(feature = "profiler")]
300                    foreground_runnables: Some(platform_scheduler.foreground_runnable_counter()),
301                    not_send: PhantomData,
302                };
303            };
304
305        #[cfg(not(any(test, feature = "test-support")))]
306        let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
307        #[cfg(not(any(test, feature = "test-support")))]
308        let inner = platform_scheduler.foreground_executor();
309        #[cfg(all(not(any(test, feature = "test-support")), feature = "profiler"))]
310        let foreground_runnables = Some(platform_scheduler.foreground_runnable_counter());
311
312        #[cfg(any(test, feature = "test-support"))]
313        let inner = {
314            let scheduler_for_dispatch = Arc::downgrade(&scheduler);
315            scheduler::LocalExecutor::new(session_id, scheduler, move |runnable| {
316                if let Some(scheduler) = scheduler_for_dispatch.upgrade() {
317                    scheduler.schedule_local(session_id, runnable);
318                }
319            })
320        };
321
322        #[cfg(all(any(test, feature = "test-support"), feature = "profiler"))]
323        // The deterministic test scheduler does not invoke GPUI's task profiler
324        // hooks, so an increment here would have no matching decrement.
325        let foreground_runnables = None;
326
327        Self {
328            inner,
329            dispatcher,
330            #[cfg(feature = "profiler")]
331            foreground_runnables,
332            not_send: PhantomData,
333        }
334    }
335
336    /// Enqueues the given Task to run on the main thread.
337    #[track_caller]
338    pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
339    where
340        R: 'static,
341    {
342        self.inner.spawn(future.boxed_local())
343    }
344
345    /// Enqueues the given Task to run on the main thread with the given priority.
346    #[track_caller]
347    pub fn spawn_with_priority<R>(
348        &self,
349        _priority: Priority,
350        future: impl Future<Output = R> + 'static,
351    ) -> Task<R>
352    where
353        R: 'static,
354    {
355        // Priority is ignored for foreground tasks - they run in order on the main thread
356        self.inner.spawn(future)
357    }
358
359    /// On platforms with dedicated support, enqueues the given future to run
360    /// on the main thread during platform idle time. Without a `timeout`,
361    /// polls may be deferred indefinitely while the platform stays busy;
362    /// with one, a poll still waiting after that long runs as ordinary main-thread work.
363    /// Each poll occupies part of one idle slice, so long synchronous stretches
364    /// should bound themselves against [`Self::idle_time_remaining`] and yield.
365    ///
366    /// On platforms without dedicated support, schedules the given future to run
367    /// with a low priority, ignoring `timeout`.
368    #[track_caller]
369    pub fn spawn_when_idle<R>(
370        &self,
371        timeout: Option<Duration>,
372        future: impl Future<Output = R> + 'static,
373    ) -> Task<R>
374    where
375        R: 'static,
376    {
377        let dispatcher = self.dispatcher.clone();
378        #[cfg(feature = "profiler")]
379        let foreground_runnables = self.foreground_runnables.clone();
380        self.inner
381            .spawn_with_dispatch(future.boxed_local(), move |runnable| {
382                #[cfg(feature = "profiler")]
383                if let Some(foreground_runnables) = &foreground_runnables {
384                    foreground_runnables.queued();
385                }
386                dispatcher.dispatch_on_main_thread_when_idle(runnable, timeout);
387            })
388    }
389
390    /// The time remaining in the current idle slice, when called from a task
391    /// spawned via [`Self::spawn_when_idle`] on a platform that meters idle
392    /// time. `None` when idle time is unmetered (or the caller is not inside
393    /// an idle slice); work that must bound itself should then fall back to a
394    /// budget of its own.
395    pub fn idle_time_remaining(&self) -> Option<Duration> {
396        self.dispatcher.idle_time_remaining()
397    }
398
399    /// Used by the test harness to run an async test in a synchronous fashion.
400    #[cfg(all(not(target_family = "wasm"), any(test, feature = "test-support")))]
401    #[track_caller]
402    pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
403        use std::cell::Cell;
404
405        let scheduler = self.inner.scheduler();
406
407        let output = Cell::new(None);
408        let future = async {
409            output.set(Some(future.await));
410        };
411        let mut future = std::pin::pin!(future);
412
413        // In async GPUI tests, we must allow foreground tasks scheduled by the test itself
414        // (which are associated with the test session) to make progress while we block.
415        // Otherwise, awaiting futures that depend on same-session foreground work can deadlock.
416        scheduler.block(None, future.as_mut(), None);
417
418        output.take().expect("block_test future did not complete")
419    }
420
421    /// Block the current thread until the given future resolves.
422    /// Consider using `block_with_timeout` instead.
423    #[cfg(not(target_family = "wasm"))]
424    pub fn block_on<R>(&self, future: impl Future<Output = R>) -> R {
425        self.inner.block_on(future)
426    }
427
428    /// Block the current thread until the given future resolves or the timeout elapses.
429    #[cfg(not(target_family = "wasm"))]
430    pub fn block_with_timeout<R, Fut: Future<Output = R>>(
431        &self,
432        duration: Duration,
433        future: Fut,
434    ) -> Result<R, impl Future<Output = R> + use<R, Fut>> {
435        self.inner.block_with_timeout(duration, future)
436    }
437
438    #[doc(hidden)]
439    pub fn dispatcher(&self) -> &Arc<dyn PlatformDispatcher> {
440        &self.dispatcher
441    }
442
443    #[doc(hidden)]
444    pub fn scheduler_executor(&self) -> SchedulerLocalExecutor {
445        self.inner.clone()
446    }
447}
448
449/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
450#[cfg(not(target_family = "wasm"))]
451pub struct Scope<'a> {
452    executor: BackgroundExecutor,
453    priority: Priority,
454    futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
455    tx: Option<mpsc::Sender<()>>,
456    rx: mpsc::Receiver<()>,
457    lifetime: PhantomData<&'a ()>,
458}
459
460#[cfg(not(target_family = "wasm"))]
461impl<'a> Scope<'a> {
462    fn new(executor: BackgroundExecutor, priority: Priority) -> Self {
463        let (tx, rx) = mpsc::channel(1);
464        Self {
465            executor,
466            priority,
467            tx: Some(tx),
468            rx,
469            futures: Default::default(),
470            lifetime: PhantomData,
471        }
472    }
473
474    /// How many CPUs are available to the dispatcher.
475    pub fn num_cpus(&self) -> usize {
476        self.executor.num_cpus()
477    }
478
479    /// Spawn a future into this scope.
480    #[track_caller]
481    pub fn spawn<F>(&mut self, f: F)
482    where
483        F: Future<Output = ()> + Send + 'a,
484    {
485        let tx = self.tx.clone().unwrap();
486
487        // SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because
488        // dropping this `Scope` blocks until all of the futures have resolved.
489        let f = unsafe {
490            mem::transmute::<
491                Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
492                Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
493            >(Box::pin(async move {
494                f.await;
495                drop(tx);
496            }))
497        };
498        self.futures.push(f);
499    }
500}
501
502#[cfg(not(target_family = "wasm"))]
503impl Drop for Scope<'_> {
504    fn drop(&mut self) {
505        self.tx.take().unwrap();
506
507        // Wait until the channel is closed, which means that all of the spawned
508        // futures have resolved.
509        let future = async {
510            self.rx.next().await;
511        };
512        let mut future = std::pin::pin!(future);
513        self.executor
514            .inner
515            .scheduler()
516            .block(None, future.as_mut(), None);
517    }
518}