Use setTimeout() trick instead of Window.requestIdleCallback() (#3044)

This commit is contained in:
daxpedda 2023-08-25 21:40:21 +02:00 committed by GitHub
parent 68ef9f707e
commit 48abf52aac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 198 additions and 133 deletions

View file

@ -9,6 +9,7 @@ And please only add new entries to the top of this list, right below the `# Unre
# Unreleased
- Fix window size sometimes being invalid when resizing on macOS.
- On Web, `ControlFlow::Poll` and `ControlFlow::WaitUntil` are now using the Prioritized Task Scheduling API. `setTimeout()` with a trick to circumvent throttling to 4ms is used as a fallback.
# 0.29.1-beta

View file

@ -164,6 +164,8 @@ redox_syscall = "0.3"
package = "web-sys"
version = "0.3.64"
features = [
'AbortController',
'AbortSignal',
'console',
'CssStyleDeclaration',
'Document',
@ -179,6 +181,8 @@ features = [
'IntersectionObserverEntry',
'KeyboardEvent',
'MediaQueryList',
'MessageChannel',
'MessagePort',
'Node',
'PageTransitionEvent',
'PointerEvent',

View file

@ -627,9 +627,11 @@ impl<T: 'static> Shared<T> {
ControlFlow::Poll => {
let cloned = self.clone();
State::Poll {
request: backend::IdleCallback::new(self.window().clone(), move || {
cloned.poll()
}),
request: backend::Schedule::new(
self.window().clone(),
move || cloned.poll(),
None,
),
}
}
ControlFlow::Wait => State::Wait {
@ -649,10 +651,10 @@ impl<T: 'static> Shared<T> {
State::WaitUntil {
start,
end,
timeout: backend::Timeout::new(
timeout: backend::Schedule::new(
self.window().clone(),
move || cloned.resume_time_reached(start, end),
delay,
Some(delay),
),
}
}

View file

@ -7,7 +7,7 @@ use web_time::Instant;
pub enum State {
Init,
WaitUntil {
timeout: backend::Timeout,
timeout: backend::Schedule,
start: Instant,
end: Instant,
},
@ -15,7 +15,7 @@ pub enum State {
start: Instant,
},
Poll {
request: backend::IdleCallback,
request: backend::Schedule,
},
Exit,
}

View file

@ -7,13 +7,13 @@ mod intersection_handle;
mod media_query_handle;
mod pointer;
mod resize_scaling;
mod timeout;
mod schedule;
pub use self::canvas::Canvas;
pub use self::event::ButtonsState;
pub use self::event_handle::EventListenerHandle;
pub use self::resize_scaling::ResizeScaleHandle;
pub use self::timeout::{IdleCallback, Timeout};
pub use self::schedule::Schedule;
use crate::dpi::{LogicalPosition, LogicalSize};
use wasm_bindgen::closure::Closure;

View file

@ -0,0 +1,182 @@
use js_sys::{Function, Object, Promise, Reflect};
use once_cell::unsync::{Lazy, OnceCell};
use std::time::Duration;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort};
#[derive(Debug)]
pub struct Schedule(Inner);
#[derive(Debug)]
enum Inner {
Scheduler {
controller: AbortController,
_closure: Closure<dyn FnMut()>,
},
Timeout {
window: web_sys::Window,
handle: i32,
port: MessagePort,
_message_closure: Closure<dyn FnMut()>,
_timeout_closure: Closure<dyn FnMut()>,
},
}
impl Schedule {
pub fn new<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
where
F: 'static + FnMut(),
{
if has_scheduler_support(&window) {
Self::new_scheduler(window, f, duration)
} else {
Self::new_timeout(window, f, duration)
}
}
fn new_scheduler<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
where
F: 'static + FnMut(),
{
let window: WindowSupportExt = window.unchecked_into();
let scheduler = window.scheduler();
let closure = Closure::new(f);
let mut options = SchedulerPostTaskOptions::new();
let controller = AbortController::new().expect("Failed to create `AbortController`");
options.signal(&controller.signal());
if let Some(duration) = duration {
options.delay(duration.as_millis() as f64);
}
thread_local! {
static REJECT_HANDLER: Lazy<Closure<dyn FnMut(JsValue)>> = Lazy::new(|| Closure::new(|_| ()));
}
REJECT_HANDLER.with(|handler| {
let _ = scheduler
.post_task_with_options(closure.as_ref().unchecked_ref(), &options)
.catch(handler);
});
Schedule(Inner::Scheduler {
controller,
_closure: closure,
})
}
fn new_timeout<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
where
F: 'static + FnMut(),
{
let channel = MessageChannel::new().unwrap();
let message_closure = Closure::new(f);
let port_1 = channel.port1();
port_1
.add_event_listener_with_callback("message", message_closure.as_ref().unchecked_ref())
.expect("Failed to set message handler");
port_1.start();
let port_2 = channel.port2();
let timeout_closure = Closure::new(move || {
port_2
.post_message(&JsValue::UNDEFINED)
.expect("Failed to send message")
});
let handle = if let Some(duration) = duration {
window.set_timeout_with_callback_and_timeout_and_arguments_0(
timeout_closure.as_ref().unchecked_ref(),
duration.as_millis() as i32,
)
} else {
window.set_timeout_with_callback(timeout_closure.as_ref().unchecked_ref())
}
.expect("Failed to set timeout");
Schedule(Inner::Timeout {
window,
handle,
port: port_1,
_message_closure: message_closure,
_timeout_closure: timeout_closure,
})
}
}
impl Drop for Schedule {
fn drop(&mut self) {
match &self.0 {
Inner::Scheduler { controller, .. } => controller.abort(),
Inner::Timeout {
window,
handle,
port,
..
} => {
window.clear_timeout_with_handle(*handle);
port.close();
}
}
}
}
fn has_scheduler_support(window: &web_sys::Window) -> bool {
thread_local! {
static SCHEDULER_SUPPORT: OnceCell<bool> = OnceCell::new();
}
SCHEDULER_SUPPORT.with(|support| {
*support.get_or_init(|| {
#[wasm_bindgen]
extern "C" {
type SchedulerSupport;
#[wasm_bindgen(method, getter, js_name = scheduler)]
fn has_scheduler(this: &SchedulerSupport) -> JsValue;
}
let support: &SchedulerSupport = window.unchecked_ref();
!support.has_scheduler().is_undefined()
})
})
}
#[wasm_bindgen]
extern "C" {
type WindowSupportExt;
#[wasm_bindgen(method, getter)]
fn scheduler(this: &WindowSupportExt) -> Scheduler;
type Scheduler;
#[wasm_bindgen(method, js_name = postTask)]
fn post_task_with_options(
this: &Scheduler,
callback: &Function,
options: &SchedulerPostTaskOptions,
) -> Promise;
type SchedulerPostTaskOptions;
}
impl SchedulerPostTaskOptions {
fn new() -> Self {
Object::new().unchecked_into()
}
fn delay(&mut self, val: f64) -> &mut Self {
let r = Reflect::set(self, &JsValue::from("delay"), &val.into());
debug_assert!(r.is_ok(), "Failed to set `delay` property");
self
}
fn signal(&mut self, val: &AbortSignal) -> &mut Self {
let r = Reflect::set(self, &JsValue::from("signal"), &val.into());
debug_assert!(r.is_ok(), "Failed to set `signal` property");
self
}
}

View file

@ -1,124 +0,0 @@
use once_cell::unsync::OnceCell;
use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
#[derive(Debug)]
pub struct Timeout {
window: web_sys::Window,
handle: i32,
_closure: Closure<dyn FnMut()>,
}
impl Timeout {
pub fn new<F>(window: web_sys::Window, f: F, duration: Duration) -> Timeout
where
F: 'static + FnMut(),
{
let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
let handle = window
.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
duration.as_millis() as i32,
)
.expect("Failed to set timeout");
Timeout {
window,
handle,
_closure: closure,
}
}
}
impl Drop for Timeout {
fn drop(&mut self) {
self.window.clear_timeout_with_handle(self.handle);
}
}
#[derive(Debug)]
pub struct IdleCallback {
window: web_sys::Window,
handle: Handle,
fired: Rc<Cell<bool>>,
_closure: Closure<dyn FnMut()>,
}
#[derive(Clone, Copy, Debug)]
enum Handle {
IdleCallback(u32),
Timeout(i32),
}
impl IdleCallback {
pub fn new<F>(window: web_sys::Window, mut f: F) -> IdleCallback
where
F: 'static + FnMut(),
{
let fired = Rc::new(Cell::new(false));
let c_fired = fired.clone();
let closure = Closure::wrap(Box::new(move || {
(*c_fired).set(true);
f();
}) as Box<dyn FnMut()>);
let handle = if has_idle_callback_support(&window) {
Handle::IdleCallback(
window
.request_idle_callback(closure.as_ref().unchecked_ref())
.expect("Failed to request idle callback"),
)
} else {
Handle::Timeout(
window
.set_timeout_with_callback(closure.as_ref().unchecked_ref())
.expect("Failed to set timeout"),
)
};
IdleCallback {
window,
handle,
fired,
_closure: closure,
}
}
}
impl Drop for IdleCallback {
fn drop(&mut self) {
if !(*self.fired).get() {
match self.handle {
Handle::IdleCallback(handle) => self.window.cancel_idle_callback(handle),
Handle::Timeout(handle) => self.window.clear_timeout_with_handle(handle),
}
}
}
}
fn has_idle_callback_support(window: &web_sys::Window) -> bool {
thread_local! {
static IDLE_CALLBACK_SUPPORT: OnceCell<bool> = OnceCell::new();
}
IDLE_CALLBACK_SUPPORT.with(|support| {
*support.get_or_init(|| {
#[wasm_bindgen]
extern "C" {
type IdleCallbackSupport;
#[wasm_bindgen(method, getter, js_name = requestIdleCallback)]
fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue;
}
let support: &IdleCallbackSupport = window.unchecked_ref();
!support.has_request_idle_callback().is_undefined()
})
})
}