Skip to main content

gpui/
executor.rs

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