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    #[doc(hidden)]
285    pub fn dispatcher(&self) -> &Arc<dyn PlatformDispatcher> {
286        &self.dispatcher
287    }
288}
289
290impl ForegroundExecutor {
291    /// Creates a new ForegroundExecutor from the given PlatformDispatcher.
292    pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
293        #[cfg(any(test, feature = "test-support"))]
294        let (scheduler, session_id): (Arc<dyn Scheduler>, _) =
295            if let Some(test_dispatcher) = dispatcher.as_test() {
296                (
297                    test_dispatcher.scheduler().clone(),
298                    test_dispatcher.session_id(),
299                )
300            } else {
301                let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
302                let inner = platform_scheduler.foreground_executor();
303                return Self {
304                    inner,
305                    dispatcher,
306                    #[cfg(feature = "profiler")]
307                    foreground_runnables: Some(platform_scheduler.foreground_runnable_counter()),
308                    not_send: PhantomData,
309                };
310            };
311
312        #[cfg(not(any(test, feature = "test-support")))]
313        let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
314        #[cfg(not(any(test, feature = "test-support")))]
315        let inner = platform_scheduler.foreground_executor();
316        #[cfg(all(not(any(test, feature = "test-support")), feature = "profiler"))]
317        let foreground_runnables = Some(platform_scheduler.foreground_runnable_counter());
318
319        #[cfg(any(test, feature = "test-support"))]
320        let inner = {
321            let scheduler_for_dispatch = Arc::downgrade(&scheduler);
322            scheduler::LocalExecutor::new(session_id, scheduler, move |runnable| {
323                if let Some(scheduler) = scheduler_for_dispatch.upgrade() {
324                    scheduler.schedule_local(session_id, runnable);
325                }
326            })
327        };
328
329        #[cfg(all(any(test, feature = "test-support"), feature = "profiler"))]
330        // The deterministic test scheduler does not invoke GPUI's task profiler
331        // hooks, so an increment here would have no matching decrement.
332        let foreground_runnables = None;
333
334        Self {
335            inner,
336            dispatcher,
337            #[cfg(feature = "profiler")]
338            foreground_runnables,
339            not_send: PhantomData,
340        }
341    }
342
343    /// Enqueues the given Task to run on the main thread.
344    #[track_caller]
345    pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
346    where
347        R: 'static,
348    {
349        self.inner.spawn(future.boxed_local())
350    }
351
352    /// Enqueues the given Task to run on the main thread with the given priority.
353    #[track_caller]
354    pub fn spawn_with_priority<R>(
355        &self,
356        _priority: Priority,
357        future: impl Future<Output = R> + 'static,
358    ) -> Task<R>
359    where
360        R: 'static,
361    {
362        // Priority is ignored for foreground tasks - they run in order on the main thread
363        self.inner.spawn(future)
364    }
365
366    /// On platforms with dedicated support, enqueues the given future to run
367    /// on the main thread during platform idle time. Without a `timeout`,
368    /// polls may be deferred indefinitely while the platform stays busy;
369    /// with one, a poll still waiting after that long runs as ordinary main-thread work.
370    /// Each poll occupies part of one idle slice, so long synchronous stretches
371    /// should bound themselves against [`Self::idle_time_remaining`] and yield.
372    ///
373    /// On platforms without dedicated support, schedules the given future to run
374    /// with a low priority, ignoring `timeout`.
375    #[track_caller]
376    pub fn spawn_when_idle<R>(
377        &self,
378        timeout: Option<Duration>,
379        future: impl Future<Output = R> + 'static,
380    ) -> Task<R>
381    where
382        R: 'static,
383    {
384        let dispatcher = self.dispatcher.clone();
385        #[cfg(feature = "profiler")]
386        let foreground_runnables = self.foreground_runnables.clone();
387        self.inner
388            .spawn_with_dispatch(future.boxed_local(), move |runnable| {
389                #[cfg(feature = "profiler")]
390                if let Some(foreground_runnables) = &foreground_runnables {
391                    foreground_runnables.queued();
392                }
393                dispatcher.dispatch_on_main_thread_when_idle(runnable, timeout);
394            })
395    }
396
397    /// The time remaining in the current idle slice, when called from a task
398    /// spawned via [`Self::spawn_when_idle`] on a platform that meters idle
399    /// time. `None` when idle time is unmetered (or the caller is not inside
400    /// an idle slice); work that must bound itself should then fall back to a
401    /// budget of its own.
402    pub fn idle_time_remaining(&self) -> Option<Duration> {
403        self.dispatcher.idle_time_remaining()
404    }
405
406    /// Used by the test harness to run an async test in a synchronous fashion.
407    #[cfg(all(not(target_family = "wasm"), any(test, feature = "test-support")))]
408    #[track_caller]
409    pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
410        use std::cell::Cell;
411
412        let scheduler = self.inner.scheduler();
413
414        let output = Cell::new(None);
415        let future = async {
416            output.set(Some(future.await));
417        };
418        let mut future = std::pin::pin!(future);
419
420        // In async GPUI tests, we must allow foreground tasks scheduled by the test itself
421        // (which are associated with the test session) to make progress while we block.
422        // Otherwise, awaiting futures that depend on same-session foreground work can deadlock.
423        scheduler.block(None, future.as_mut(), None);
424
425        output.take().expect("block_test future did not complete")
426    }
427
428    /// Block the current thread until the given future resolves.
429    /// Consider using `block_with_timeout` instead.
430    #[cfg(not(target_family = "wasm"))]
431    pub fn block_on<R>(&self, future: impl Future<Output = R>) -> R {
432        self.inner.block_on(future)
433    }
434
435    /// Block the current thread until the given future resolves or the timeout elapses.
436    #[cfg(not(target_family = "wasm"))]
437    pub fn block_with_timeout<R, Fut: Future<Output = R>>(
438        &self,
439        duration: Duration,
440        future: Fut,
441    ) -> Result<R, impl Future<Output = R> + use<R, Fut>> {
442        self.inner.block_with_timeout(duration, future)
443    }
444
445    #[doc(hidden)]
446    pub fn dispatcher(&self) -> &Arc<dyn PlatformDispatcher> {
447        &self.dispatcher
448    }
449
450    #[doc(hidden)]
451    pub fn scheduler_executor(&self) -> SchedulerLocalExecutor {
452        self.inner.clone()
453    }
454}
455
456/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
457#[cfg(not(target_family = "wasm"))]
458pub struct Scope<'a> {
459    executor: BackgroundExecutor,
460    priority: Priority,
461    futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
462    tx: Option<mpsc::Sender<()>>,
463    rx: mpsc::Receiver<()>,
464    lifetime: PhantomData<&'a ()>,
465}
466
467#[cfg(not(target_family = "wasm"))]
468impl<'a> Scope<'a> {
469    fn new(executor: BackgroundExecutor, priority: Priority) -> Self {
470        let (tx, rx) = mpsc::channel(1);
471        Self {
472            executor,
473            priority,
474            tx: Some(tx),
475            rx,
476            futures: Default::default(),
477            lifetime: PhantomData,
478        }
479    }
480
481    /// How many CPUs are available to the dispatcher.
482    pub fn num_cpus(&self) -> usize {
483        self.executor.num_cpus()
484    }
485
486    /// Spawn a future into this scope.
487    #[track_caller]
488    pub fn spawn<F>(&mut self, f: F)
489    where
490        F: Future<Output = ()> + Send + 'a,
491    {
492        let tx = self.tx.clone().unwrap();
493
494        // SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because
495        // dropping this `Scope` blocks until all of the futures have resolved.
496        let f = unsafe {
497            mem::transmute::<
498                Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
499                Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
500            >(Box::pin(async move {
501                f.await;
502                drop(tx);
503            }))
504        };
505        self.futures.push(f);
506    }
507}
508
509#[cfg(not(target_family = "wasm"))]
510impl Drop for Scope<'_> {
511    fn drop(&mut self) {
512        self.tx.take().unwrap();
513
514        // Wait until the channel is closed, which means that all of the spawned
515        // futures have resolved.
516        let future = async {
517            self.rx.next().await;
518        };
519        let mut future = std::pin::pin!(future);
520        self.executor
521            .inner
522            .scheduler()
523            .block(None, future.as_mut(), None);
524    }
525}
526
527#[cfg(test)]
528mod test {
529    use super::*;
530    use crate::{App, TestDispatcher, TestPlatform};
531    use std::cell::RefCell;
532
533    /// Helper to create test infrastructure.
534    /// Returns (dispatcher, background_executor, app).
535    fn create_test_app() -> (TestDispatcher, BackgroundExecutor, Rc<crate::AppCell>) {
536        let dispatcher = TestDispatcher::new(0);
537        let arc_dispatcher = Arc::new(dispatcher.clone());
538        let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
539        let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
540
541        let platform = TestPlatform::new(background_executor.clone(), foreground_executor);
542        let asset_source = Arc::new(());
543        let http_client = http_client::FakeHttpClient::with_404_response();
544
545        let app = App::new_app(platform, asset_source, http_client);
546        (dispatcher, background_executor, app)
547    }
548
549    #[test]
550    fn sanity_test_tasks_run() {
551        let (dispatcher, _background_executor, app) = create_test_app();
552        let foreground_executor = app.borrow().foreground_executor.clone();
553
554        let task_ran = Rc::new(RefCell::new(false));
555
556        foreground_executor
557            .spawn({
558                let task_ran = Rc::clone(&task_ran);
559                async move {
560                    *task_ran.borrow_mut() = true;
561                }
562            })
563            .detach();
564
565        // Run dispatcher while app is still alive
566        dispatcher.run_until_parked();
567
568        // Task should have run
569        assert!(
570            *task_ran.borrow(),
571            "Task should run normally when app is alive"
572        );
573    }
574}