gpui: Add an abstraction for scheduling foreground work for idle periods, and implement it for the web (#61827)
- Add `ForegroundExecutor::spawn_when_idle(timeout, future)`, which schedules `future` to be polled on the main thread when the main thread is idle, but tries to do so no later than `timeout` after the current instant (with the same `timeout` being applied to subsequent polls of the same future) - Add `Platform::dispatch_on_main_thread_when_idle(runnable, timeout)` as the basis for `spawn_when_idle`, with a default implementation that ignores the timeout and delegates to `dispatch_on_main_thread(runnable, Priority::Low)` - Implement `dispatch_on_main_thread_when_idle` for the web platform using [`requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) Release Notes: - N/A
This commit is contained in:
1 parent
90727b44e5
commit
6ac1191f6a
5 files changed
+167
No files matched your search
@@ -332,6 +332,40 @@ impl ForegroundExecutor {
|
||||
self.inner.spawn(future)
|
||||
}
|
||||
|
||||
/// On platforms with dedicated support, enqueues the given future to run
|
||||
/// on the main thread during platform idle time. Without a `timeout`,
|
||||
/// polls may be deferred indefinitely while the platform stays busy;
|
||||
/// with one, a poll still waiting after that long runs as ordinary main-thread work.
|
||||
/// Each poll occupies part of one idle slice, so long synchronous stretches
|
||||
/// should bound themselves against [`Self::idle_time_remaining`] and yield.
|
||||
///
|
||||
/// On platforms without dedicated support, schedules the given future to run
|
||||
/// with a low priority, ignoring `timeout`.
|
||||
#[track_caller]
|
||||
pub fn spawn_when_idle<R>(
|
||||
&self,
|
||||
timeout: Option<Duration>,
|
||||
future: impl Future<Output = R> + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
self.inner
|
||||
.spawn_with_dispatch(future.boxed_local(), move |runnable| {
|
||||
dispatcher.dispatch_on_main_thread_when_idle(runnable, timeout);
|
||||
})
|
||||
}
|
||||
|
||||
/// The time remaining in the current idle slice, when called from a task
|
||||
/// spawned via [`Self::spawn_when_idle`] on a platform that meters idle
|
||||
/// time. `None` when idle time is unmetered (or the caller is not inside
|
||||
/// an idle slice); work that must bound itself should then fall back to a
|
||||
/// budget of its own.
|
||||
pub fn idle_time_remaining(&self) -> Option<Duration> {
|
||||
self.dispatcher.idle_time_remaining()
|
||||
}
|
||||
|
||||
/// Used by the test harness to run an async test in a synchronous fashion.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
|
||||
@@ -1007,6 +1007,19 @@ pub trait PlatformDispatcher: Send + Sync {
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
|
||||
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
|
||||
|
||||
fn dispatch_on_main_thread_when_idle(
|
||||
&self,
|
||||
runnable: RunnableVariant,
|
||||
timeout: Option<Duration>,
|
||||
) {
|
||||
let _ = timeout;
|
||||
self.dispatch_on_main_thread(runnable, Priority::Low);
|
||||
}
|
||||
|
||||
fn idle_time_remaining(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
|
||||
@@ -51,6 +51,8 @@ web-sys = { version = "0.3", features = [
|
||||
"HtmlCanvasElement",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
"IdleDeadline",
|
||||
"IdleRequestOptions",
|
||||
"KeyboardEvent",
|
||||
"MediaQueryList",
|
||||
"MediaQueryListEvent",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use gpui::{
|
||||
PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::time::Duration;
|
||||
@@ -39,6 +40,10 @@ enum MainThreadItem {
|
||||
runnable: RunnableVariant,
|
||||
millis: i32,
|
||||
},
|
||||
Idle {
|
||||
runnable: RunnableVariant,
|
||||
timeout: Option<Duration>,
|
||||
},
|
||||
Function(Box<dyn FnOnce() + Send>),
|
||||
// TODO-Wasm: Shouldn't these run on their own dedicated thread?
|
||||
RealtimeFunction(Box<dyn FnOnce() + Send>),
|
||||
@@ -293,6 +298,31 @@ impl PlatformDispatcher for WebDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread_when_idle(
|
||||
&self,
|
||||
runnable: RunnableVariant,
|
||||
timeout: Option<Duration>,
|
||||
) {
|
||||
if self.on_main_thread() {
|
||||
schedule_idle_runnable(&browser_window(), runnable, timeout);
|
||||
} else {
|
||||
self.main_thread_mailbox
|
||||
.post(Priority::Low, MainThreadItem::Idle { runnable, timeout });
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_time_remaining(&self) -> Option<Duration> {
|
||||
if !self.on_main_thread() {
|
||||
return None;
|
||||
}
|
||||
IDLE_DEADLINE.with(|deadline| {
|
||||
deadline
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|deadline| Duration::from_secs_f64(deadline.time_remaining() / 1000.0))
|
||||
})
|
||||
}
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
@@ -307,6 +337,9 @@ fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
|
||||
MainThreadItem::Runnable(runnable) => {
|
||||
runnable.run();
|
||||
}
|
||||
MainThreadItem::Idle { runnable, timeout } => {
|
||||
schedule_idle_runnable(window, runnable, timeout);
|
||||
}
|
||||
MainThreadItem::Delayed { runnable, millis } => {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
runnable.run();
|
||||
@@ -324,6 +357,64 @@ fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// The deadline of the idle callback currently running; read by
|
||||
/// [`PlatformDispatcher::idle_time_remaining`] from inside the runnable.
|
||||
static IDLE_DEADLINE: RefCell<Option<web_sys::IdleDeadline>> = const { RefCell::new(None) };
|
||||
/// Whether `requestIdleCallback` exists (it is absent on Safari), probed
|
||||
/// on first use.
|
||||
static IDLE_CALLBACK_SUPPORTED: Cell<Option<bool>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
/// Registers one `requestIdleCallback` per runnable, mirroring how
|
||||
/// `dispatch_after` maps each timer to its own platform alarm. The browser
|
||||
/// already provides the queue semantics a central pump would rebuild: idle
|
||||
/// callbacks run in registration order, an idle period drains as many as its
|
||||
/// deadline allows, and a callback whose `timeout` expires is posted as an
|
||||
/// ordinary task instead.
|
||||
fn schedule_idle_runnable(
|
||||
window: &web_sys::Window,
|
||||
runnable: RunnableVariant,
|
||||
timeout: Option<Duration>,
|
||||
) {
|
||||
if !idle_callback_supported(window) {
|
||||
// Safari: run idle work as ordinary macrotasks. With no metered
|
||||
// deadline, `idle_time_remaining` stays `None` and idle tasks bound
|
||||
// their own slices.
|
||||
schedule_runnable(window, runnable, Priority::Low);
|
||||
return;
|
||||
}
|
||||
let callback = Closure::once_into_js(move |deadline: web_sys::IdleDeadline| {
|
||||
IDLE_DEADLINE.with(|current| *current.borrow_mut() = Some(deadline));
|
||||
runnable.run();
|
||||
IDLE_DEADLINE.with(|current| *current.borrow_mut() = None);
|
||||
});
|
||||
let result = match timeout {
|
||||
Some(timeout) => {
|
||||
let options = web_sys::IdleRequestOptions::new();
|
||||
options.set_timeout(timeout.as_millis().min(u32::MAX as u128) as u32);
|
||||
window.request_idle_callback_with_options(callback.unchecked_ref(), &options)
|
||||
}
|
||||
None => window.request_idle_callback(callback.unchecked_ref()),
|
||||
};
|
||||
if let Err(error) = result {
|
||||
log::error!("requestIdleCallback failed: {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_callback_supported(window: &web_sys::Window) -> bool {
|
||||
IDLE_CALLBACK_SUPPORTED.with(|supported| {
|
||||
if let Some(supported) = supported.get() {
|
||||
return supported;
|
||||
}
|
||||
let probed =
|
||||
js_sys::Reflect::has(window.as_ref(), &JsValue::from_str("requestIdleCallback"))
|
||||
.unwrap_or(false);
|
||||
supported.set(Some(probed));
|
||||
probed
|
||||
})
|
||||
}
|
||||
|
||||
fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
runnable.run();
|
||||
|
||||
@@ -79,6 +79,33 @@ impl LocalExecutor {
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
/// Like [`Self::spawn`], but routes the task's runnables through the
|
||||
/// given dispatch destination instead of this executor's own. The
|
||||
/// destination must deliver runnables to this executor's thread: the
|
||||
/// future is polled and dropped on the spawning thread.
|
||||
#[track_caller]
|
||||
pub fn spawn_with_dispatch<F>(
|
||||
&self,
|
||||
future: F,
|
||||
dispatch: impl Fn(Runnable<RunnableMeta>) + Send + Sync + 'static,
|
||||
) -> Task<F::Output>
|
||||
where
|
||||
F: Future + 'static,
|
||||
F::Output: 'static,
|
||||
{
|
||||
let location = Location::caller();
|
||||
let (runnable, task) = spawn_local_with_source_location(
|
||||
future,
|
||||
dispatch,
|
||||
RunnableMeta {
|
||||
location,
|
||||
spawned: crate::SpawnTime(Instant::now()),
|
||||
},
|
||||
);
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
pub fn block_on<Fut: Future>(&self, future: Fut) -> Fut::Output {
|
||||
use std::cell::Cell;
|
||||
|
||||
|
||||
Reference in new issue
Block a user