Skip to main content

gpui/
executor.rs

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