Skip to main content

gpui/
executor.rs

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