From 3e6092b8edf8dfddef3a777b922be1b9ae6a824c Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 20 Jun 2024 23:07:42 +0200 Subject: [PATCH] Web: implement `WaitUntilStrategy` (#3739) --- .github/workflows/ci.yml | 16 ++++ .gitignore | 3 - .swcrc | 12 +++ Cargo.toml | 2 + src/changelog/unreleased.md | 5 + src/platform/web.rs | 69 ++++++++++++++ src/platform_impl/web/event_loop/mod.rs | 10 +- src/platform_impl/web/event_loop/runner.rs | 13 ++- .../web/event_loop/window_target.rs | 10 +- src/platform_impl/web/web_sys/schedule.rs | 94 +++++++++++++++++-- src/platform_impl/web/web_sys/worker.js | 10 ++ src/platform_impl/web/web_sys/worker.min.js | 1 + 12 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 .swcrc create mode 100644 src/platform_impl/web/web_sys/worker.js create mode 100644 src/platform_impl/web/web_sys/worker.min.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9f43a94b1..a40ca4231d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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) ]] diff --git a/.gitignore b/.gitignore index ebed8e3651..2caffd8242 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,5 @@ target/ rls/ .vscode/ *~ -*.wasm -*.ts -*.js #*# .DS_Store diff --git a/.swcrc b/.swcrc new file mode 100644 index 0000000000..1d7eccb99d --- /dev/null +++ b/.swcrc @@ -0,0 +1,12 @@ +{ + "minify": true, + "jsc": { + "target": "es2022", + "minify": { + "compress": { + "unused": true + }, + "mangle": true + } + } +} diff --git a/Cargo.toml b/Cargo.toml index 39a6293570..7e1d337a22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -285,6 +285,7 @@ features = [ 'AbortController', 'AbortSignal', 'Blob', + 'BlobPropertyBag', 'console', 'CssStyleDeclaration', 'Document', @@ -320,6 +321,7 @@ features = [ 'VisibilityState', 'Window', 'WheelEvent', + 'Worker', 'Url', ] diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index c75a3fd6ba..5748772bcb 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -44,6 +44,11 @@ changelog entry. - On Web, add `EventLoopExtWebSys::(set_)poll_strategy()` to allow setting control flow strategies before starting the event loop. +- On Web, add `WaitUntilStrategy`, which allows to set different strategies for + `ControlFlow::WaitUntil`. By default the Prioritized Task Scheduling API is + used, with a fallback to `setTimeout()` with a trick to circumvent throttling + to 4ms. But an option to use a Web worker to schedule the timer is available + as well, which commonly prevents any throttling when the window is not focused. ### Changed diff --git a/src/platform/web.rs b/src/platform/web.rs index fad81e7591..261808269e 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -197,6 +197,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 EventLoopExtWebSys for EventLoop { @@ -213,6 +227,14 @@ impl EventLoopExtWebSys for EventLoop { 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 { @@ -230,6 +252,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; @@ -250,6 +286,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]. @@ -278,6 +324,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; diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 67f373cd12..b13c2ee957 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -5,7 +5,7 @@ use crate::application::ApplicationHandler; 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}; @@ -85,6 +85,14 @@ impl EventLoop { 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() + } } fn handle_event>( diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index bd2c3bbe1f..66028cc9fd 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -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; @@ -43,6 +43,7 @@ pub struct Execution { proxy_spawner: WakerSpawner>, control_flow: Cell, poll_strategy: Cell, + wait_until_strategy: Cell, exit: Cell, runner: RefCell, suspended: Cell, @@ -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), @@ -688,6 +690,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, @@ -800,6 +803,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> { self.0.proxy_spawner.waker() } diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 142466ab0b..e27d024051 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -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::{ @@ -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> { self.runner.waker() } diff --git a/src/platform_impl/web/web_sys/schedule.rs b/src/platform_impl/web/web_sys/schedule.rs index a2ef874d33..935e9b7978 100644 --- a/src/platform_impl/web/web_sys/schedule.rs +++ b/src/platform_impl/web/web_sys/schedule.rs @@ -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 { @@ -29,6 +31,7 @@ enum Inner { port: MessagePort, _timeout_closure: Closure, }, + Worker(MessagePort), } impl Schedule { @@ -45,14 +48,24 @@ impl Schedule { } } - pub fn new_with_duration(window: &web_sys::Window, f: F, duration: Duration) -> Schedule + pub fn new_with_duration( + 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), } } @@ -155,6 +168,44 @@ impl Schedule { }, } } + + fn new_worker(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.subsec_micros().div_ceil(1000))) + .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 { @@ -167,6 +218,10 @@ impl Drop for Schedule { port.close(); port.set_onmessage(None); }, + Inner::Worker(port) => { + port.close(); + port.set_onmessage(None); + }, } } } @@ -214,6 +269,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; diff --git a/src/platform_impl/web/web_sys/worker.js b/src/platform_impl/web/web_sys/worker.js new file mode 100644 index 0000000000..5a8411ef83 --- /dev/null +++ b/src/platform_impl/web/web_sys/worker.js @@ -0,0 +1,10 @@ +onmessage = event => { + const [port, timeout] = event.data + const f = () => port.postMessage(undefined) + + if ('scheduler' in this) { + scheduler.postTask(f, { delay: timeout }) + } else { + setTimeout(f, timeout) + } +} diff --git a/src/platform_impl/web/web_sys/worker.min.js b/src/platform_impl/web/web_sys/worker.min.js new file mode 100644 index 0000000000..fd394a732f --- /dev/null +++ b/src/platform_impl/web/web_sys/worker.min.js @@ -0,0 +1 @@ +onmessage=e=>{let[s,t]=e.data,a=()=>s.postMessage(void 0);"scheduler"in this?scheduler.postTask(a,{delay:t}):setTimeout(a,t)};