Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ redox_syscall = "0.3"
package = "web-sys"
version = "0.3.64"
features = [
'AbortController',
'AbortSignal',
'console',
'CssStyleDeclaration',
'Document',
Expand All @@ -179,6 +181,8 @@ features = [
'IntersectionObserverEntry',
'KeyboardEvent',
'MediaQueryList',
'MessageChannel',
'MessagePort',
'Node',
'PageTransitionEvent',
'PointerEvent',
Expand Down
12 changes: 7 additions & 5 deletions src/platform_impl/web/event_loop/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
),
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/platform_impl/web/event_loop/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ use web_time::Instant;
pub enum State {
Init,
WaitUntil {
timeout: backend::Timeout,
timeout: backend::Schedule,
start: Instant,
end: Instant,
},
Wait {
start: Instant,
},
Poll {
request: backend::IdleCallback,
request: backend::Schedule,
},
Exit,
}
Expand Down
4 changes: 2 additions & 2 deletions src/platform_impl/web/web_sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
182 changes: 182 additions & 0 deletions src/platform_impl/web/web_sys/schedule.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
124 changes: 0 additions & 124 deletions src/platform_impl/web/web_sys/timeout.rs

This file was deleted.