Skip to content

Commit

Permalink
Web: implement WaitUntilStrategy (#3739)
Browse files Browse the repository at this point in the history
  • Loading branch information
daxpedda authored and kchibisov committed Jun 21, 2024
1 parent a974640 commit d8f4d8f
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 14 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,19 @@ jobs:
command: check
log-level: error
arguments: --all-features --target ${{ matrix.platform.target }}

swc:
name: Minimize JavaScript
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install SWC
run: sudo npm i -g @swc/cli
- name: Run SWC
run: |
swc src/platform_impl/web/web_sys/worker.js -o src/platform_impl/web/web_sys/worker.min.js
- name: Check for diff
run: |
[[ -z $(git status -s) ]]
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,5 @@ target/
rls/
.vscode/
*~
*.wasm
*.ts
*.js
#*#
.DS_Store
12 changes: 12 additions & 0 deletions .swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"minify": true,
"jsc": {
"target": "es2022",
"minify": {
"compress": {
"unused": true
},
"mangle": true
}
}
}
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ features = [
'AbortController',
'AbortSignal',
'Blob',
'BlobPropertyBag',
'console',
'CssStyleDeclaration',
'Document',
Expand Down Expand Up @@ -320,6 +321,7 @@ features = [
'VisibilityState',
'Window',
'WheelEvent',
'Worker',
'Url',
]

Expand Down
69 changes: 69 additions & 0 deletions src/platform/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,20 @@ pub trait EventLoopExtWebSys {
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn poll_strategy(&self) -> PollStrategy;

/// Sets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);

/// Gets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn wait_until_strategy(&self) -> WaitUntilStrategy;
}

impl<T> EventLoopExtWebSys for EventLoop<T> {
Expand All @@ -229,6 +243,14 @@ impl<T> EventLoopExtWebSys for EventLoop<T> {
fn poll_strategy(&self) -> PollStrategy {
self.event_loop.poll_strategy()
}

fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.event_loop.set_wait_until_strategy(strategy);
}

fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.event_loop.wait_until_strategy()
}
}

pub trait ActiveEventLoopExtWebSys {
Expand All @@ -246,6 +268,20 @@ pub trait ActiveEventLoopExtWebSys {
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn poll_strategy(&self) -> PollStrategy;

/// Sets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);

/// Gets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn wait_until_strategy(&self) -> WaitUntilStrategy;

/// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
/// cursor has completely finished loading.
fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture;
Expand All @@ -266,6 +302,16 @@ impl ActiveEventLoopExtWebSys for ActiveEventLoop {
fn poll_strategy(&self) -> PollStrategy {
self.p.poll_strategy()
}

#[inline]
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.p.set_wait_until_strategy(strategy);
}

#[inline]
fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.p.wait_until_strategy()
}
}

/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll].
Expand Down Expand Up @@ -294,6 +340,29 @@ pub enum PollStrategy {
Scheduler,
}

/// Strategy used for [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum WaitUntilStrategy {
/// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available
/// this will fallback to [`setTimeout()`].
///
/// This strategy is commonly not affected by browser throttling unless the window is not
/// focused.
///
/// This is the default strategy.
///
/// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
/// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
#[default]
Scheduler,
/// Equal to [`Scheduler`][Self::Scheduler] but wakes up the event loop from a [worker].
///
/// This strategy is commonly not affected by browser throttling regardless of window focus.
///
/// [worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
Worker,
}

pub trait CustomCursorExtWebSys {
/// Returns if this cursor is an animation.
fn is_animation(&self) -> bool;
Expand Down
10 changes: 9 additions & 1 deletion src/platform_impl/web/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::mpsc::{self, Receiver, Sender};
use crate::error::EventLoopError;
use crate::event::Event;
use crate::event_loop::ActiveEventLoop as RootActiveEventLoop;
use crate::platform::web::{ActiveEventLoopExtWebSys, PollStrategy};
use crate::platform::web::{ActiveEventLoopExtWebSys, PollStrategy, WaitUntilStrategy};

use super::{backend, device, window};

Expand Down Expand Up @@ -108,4 +108,12 @@ impl<T> EventLoop<T> {
pub fn poll_strategy(&self) -> PollStrategy {
self.elw.poll_strategy()
}

pub fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.elw.set_wait_until_strategy(strategy);
}

pub fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.elw.wait_until_strategy()
}
}
13 changes: 12 additions & 1 deletion src/platform_impl/web/event_loop/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::event::{
WindowEvent,
};
use crate::event_loop::{ControlFlow, DeviceEvents};
use crate::platform::web::PollStrategy;
use crate::platform::web::{PollStrategy, WaitUntilStrategy};
use crate::platform_impl::platform::backend::EventListenerHandle;
use crate::platform_impl::platform::r#async::{DispatchRunner, Waker, WakerSpawner};
use crate::platform_impl::platform::window::Inner;
Expand Down Expand Up @@ -43,6 +43,7 @@ pub struct Execution {
proxy_spawner: WakerSpawner<Weak<Self>>,
control_flow: Cell<ControlFlow>,
poll_strategy: Cell<PollStrategy>,
wait_until_strategy: Cell<WaitUntilStrategy>,
exit: Cell<bool>,
runner: RefCell<RunnerEnum>,
suspended: Cell<bool>,
Expand Down Expand Up @@ -149,6 +150,7 @@ impl Shared {
proxy_spawner,
control_flow: Cell::new(ControlFlow::default()),
poll_strategy: Cell::new(PollStrategy::default()),
wait_until_strategy: Cell::new(WaitUntilStrategy::default()),
exit: Cell::new(false),
runner: RefCell::new(RunnerEnum::Pending),
suspended: Cell::new(false),
Expand Down Expand Up @@ -696,6 +698,7 @@ impl Shared {
start,
end,
_timeout: backend::Schedule::new_with_duration(
self.wait_until_strategy(),
self.window(),
move || cloned.resume_time_reached(start, end),
delay,
Expand Down Expand Up @@ -808,6 +811,14 @@ impl Shared {
self.0.poll_strategy.get()
}

pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.0.wait_until_strategy.set(strategy)
}

pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.0.wait_until_strategy.get()
}

pub(crate) fn waker(&self) -> Waker<Weak<Execution>> {
self.0.proxy_spawner.waker()
}
Expand Down
10 changes: 9 additions & 1 deletion src/platform_impl/web/event_loop/window_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::event::{
};
use crate::event_loop::{ControlFlow, DeviceEvents};
use crate::keyboard::ModifiersState;
use crate::platform::web::{CustomCursorFuture, PollStrategy};
use crate::platform::web::{CustomCursorFuture, PollStrategy, WaitUntilStrategy};
use crate::platform_impl::platform::cursor::CustomCursor;
use crate::platform_impl::platform::r#async::Waker;
use crate::window::{
Expand Down Expand Up @@ -682,6 +682,14 @@ impl ActiveEventLoop {
self.runner.poll_strategy()
}

pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.runner.set_wait_until_strategy(strategy)
}

pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.runner.wait_until_strategy()
}

pub(crate) fn waker(&self) -> Waker<Weak<Execution>> {
self.runner.waker()
}
Expand Down
94 changes: 86 additions & 8 deletions src/platform_impl/web/web_sys/schedule.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use js_sys::{Function, Object, Promise, Reflect};
use js_sys::{Array, Function, Object, Promise, Reflect};
use std::cell::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};
use web_sys::{
AbortController, AbortSignal, Blob, BlobPropertyBag, MessageChannel, MessagePort, Url, Worker,
};

use crate::platform::web::PollStrategy;
use crate::platform::web::{PollStrategy, WaitUntilStrategy};

#[derive(Debug)]
pub struct Schedule {
Expand All @@ -29,6 +31,7 @@ enum Inner {
port: MessagePort,
_timeout_closure: Closure<dyn FnMut()>,
},
Worker(MessagePort),
}

impl Schedule {
Expand All @@ -45,14 +48,24 @@ impl Schedule {
}
}

pub fn new_with_duration<F>(window: &web_sys::Window, f: F, duration: Duration) -> Schedule
pub fn new_with_duration<F>(
strategy: WaitUntilStrategy,
window: &web_sys::Window,
f: F,
duration: Duration,
) -> Schedule
where
F: 'static + FnMut(),
{
if has_scheduler_support(window) {
Self::new_scheduler(window, f, Some(duration))
} else {
Self::new_timeout(window.clone(), f, Some(duration))
match strategy {
WaitUntilStrategy::Scheduler => {
if has_scheduler_support(window) {
Self::new_scheduler(window, f, Some(duration))
} else {
Self::new_timeout(window.clone(), f, Some(duration))
}
},
WaitUntilStrategy::Worker => Self::new_worker(f, duration),
}
}

Expand Down Expand Up @@ -153,6 +166,44 @@ impl Schedule {
},
}
}

fn new_worker<F>(f: F, duration: Duration) -> Schedule
where
F: 'static + FnMut(),
{
thread_local! {
static URL: ScriptUrl = ScriptUrl::new(include_str!("worker.min.js"));
static WORKER: Worker = URL.with(|url| Worker::new(&url.0)).expect("`new Worker()` is not expected to fail with a local script");
}

let channel = MessageChannel::new().unwrap();
let closure = Closure::new(f);
let port_1 = channel.port1();
port_1.set_onmessage(Some(closure.as_ref().unchecked_ref()));
port_1.start();

// `Duration::as_millis()` always rounds down (because of truncation), we want to round
// up instead. This makes sure that the we never wake up **before** the given time.
let duration = duration
.as_secs()
.try_into()
.ok()
.and_then(|secs: u32| secs.checked_mul(1000))
.and_then(|secs| secs.checked_add(duration_millis_ceil(duration)))
.unwrap_or(u32::MAX);

WORKER
.with(|worker| {
let port_2 = channel.port2();
worker.post_message_with_transfer(
&Array::of2(&port_2, &duration.into()),
&Array::of1(&port_2).into(),
)
})
.expect("`Worker.postMessage()` is not expected to fail");

Schedule { _closure: closure, inner: Inner::Worker(port_1) }
}
}

impl Drop for Schedule {
Expand All @@ -165,6 +216,10 @@ impl Drop for Schedule {
port.close();
port.set_onmessage(None);
},
Inner::Worker(port) => {
port.close();
port.set_onmessage(None);
},
}
}
}
Expand Down Expand Up @@ -226,6 +281,29 @@ fn has_idle_callback_support(window: &web_sys::Window) -> bool {
})
}

struct ScriptUrl(String);

impl ScriptUrl {
fn new(script: &str) -> Self {
let sequence = Array::of1(&script.into());
let mut property = BlobPropertyBag::new();
property.type_("text/javascript");
let blob = Blob::new_with_str_sequence_and_options(&sequence, &property)
.expect("`new Blob()` should never throw");

let url = Url::create_object_url_with_blob(&blob)
.expect("`URL.createObjectURL()` should never throw");

Self(url)
}
}

impl Drop for ScriptUrl {
fn drop(&mut self) {
Url::revoke_object_url(&self.0).expect("`URL.revokeObjectURL()` should never throw");
}
}

#[wasm_bindgen]
extern "C" {
type WindowSupportExt;
Expand Down
Loading

0 comments on commit d8f4d8f

Please sign in to comment.