Skip to content

Commit

Permalink
Use setTimeout() trick instead of Window.requestIdleCallback() (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
daxpedda authored Aug 25, 2023
1 parent 68ef9f7 commit 48abf52
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 133 deletions.
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.

0 comments on commit 48abf52

Please sign in to comment.