From fe148f26e2f183a68987963797e861f7fb1fa380 Mon Sep 17 00:00:00 2001 From: Samuel Maier Date: Fri, 6 Sep 2024 17:15:44 +0200 Subject: [PATCH 1/4] Use vendored performance API to handle timing on WASM Before that this used wasm_timer, but only for Instant::now. But that paniced on web_workers, because it got there by going over the window, which isnt present in web workers. This still doesn't work on all browsers though, thus I added an assertion against usage on web-workers. --- embassy-time/Cargo.toml | 5 +- embassy-time/src/driver_wasm.rs | 106 ++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/embassy-time/Cargo.toml b/embassy-time/Cargo.toml index c3b4e4e3a2..e3d8951e73 100644 --- a/embassy-time/Cargo.toml +++ b/embassy-time/Cargo.toml @@ -24,8 +24,10 @@ target = "x86_64-unknown-linux-gnu" features = ["defmt", "std"] [features] +default = ["panic_on_webworker"] std = ["tick-hz-1_000_000", "critical-section/std"] -wasm = ["dep:wasm-bindgen", "dep:js-sys", "dep:wasm-timer", "tick-hz-1_000_000"] +wasm = ["dep:wasm-bindgen", "dep:js-sys", "tick-hz-1_000_000"] +panic_on_webworker = [] ## Display the time since startup next to defmt log messages. ## At most 1 `defmt-timestamp-uptime-*` feature can be used. @@ -426,7 +428,6 @@ document-features = "0.2.7" # WASM dependencies wasm-bindgen = { version = "0.2.81", optional = true } js-sys = { version = "0.3", optional = true } -wasm-timer = { version = "0.2.5", optional = true } [dev-dependencies] serial_test = "0.9" diff --git a/embassy-time/src/driver_wasm.rs b/embassy-time/src/driver_wasm.rs index ad884f060f..492a9ee2ab 100644 --- a/embassy-time/src/driver_wasm.rs +++ b/embassy-time/src/driver_wasm.rs @@ -6,7 +6,6 @@ use std::sync::{Mutex, Once}; use embassy_time_driver::{AlarmHandle, Driver}; use wasm_bindgen::prelude::*; -use wasm_timer::Instant as StdInstant; const ALARM_COUNT: usize = 4; @@ -37,7 +36,6 @@ struct TimeDriver { once: Once, alarms: UninitCell>, - zero_instant: UninitCell, } const ALARM_NEW: AlarmState = AlarmState::new(); @@ -45,25 +43,24 @@ embassy_time_driver::time_driver_impl!(static DRIVER: TimeDriver = TimeDriver { alarm_count: AtomicU8::new(0), once: Once::new(), alarms: UninitCell::uninit(), - zero_instant: UninitCell::uninit(), }); impl TimeDriver { - fn init(&self) { + fn ensure_init(&self) { self.once.call_once(|| unsafe { self.alarms.write(Mutex::new([ALARM_NEW; ALARM_COUNT])); - self.zero_instant.write(StdInstant::now()); + #[cfg(feature = "panic_on_webworker")] + assert!(!is_web_worker_thread(), "Timer currently has issues on Web Workers: https://github.com/embassy-rs/embassy/issues/3313"); }); } } impl Driver for TimeDriver { - fn now(&self) -> u64 { - self.init(); - - let zero = unsafe { self.zero_instant.read() }; - StdInstant::now().duration_since(zero).as_micros() as u64 - } + fn now(&self) -> u64 { + self.ensure_init(); + // this is calibrated with timeOrigin. + now_as_calibrated_timestamp().as_micros() as u64 + } unsafe fn allocate_alarm(&self) -> Option { let id = self.alarm_count.fetch_update(Ordering::AcqRel, Ordering::Acquire, |x| { @@ -81,7 +78,7 @@ impl Driver for TimeDriver { } fn set_alarm_callback(&self, alarm: AlarmHandle, callback: fn(*mut ()), ctx: *mut ()) { - self.init(); + self.ensure_init(); let mut alarms = unsafe { self.alarms.as_ref() }.lock().unwrap(); let alarm = &mut alarms[alarm.id() as usize]; alarm.closure.replace(Closure::new(move || { @@ -90,7 +87,7 @@ impl Driver for TimeDriver { } fn set_alarm(&self, alarm: AlarmHandle, timestamp: u64) -> bool { - self.init(); + self.ensure_init(); let mut alarms = unsafe { self.alarms.as_ref() }.lock().unwrap(); let alarm = &mut alarms[alarm.id() as usize]; if let Some(token) = alarm.token { @@ -139,3 +136,86 @@ impl UninitCell { ptr::read(self.as_mut_ptr()) } } + +fn is_web_worker_thread() -> bool { + js_sys::eval("typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope").unwrap().is_truthy() +} + +// ---------------- taken from web-time/js.rs +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; + +#[wasm_bindgen] +extern "C" { + /// Type for the [global object](https://developer.mozilla.org/en-US/docs/Glossary/Global_object). + type Global; + + /// Returns the [`Performance`](https://developer.mozilla.org/en-US/docs/Web/API/Performance) object. + #[wasm_bindgen(method, getter)] + fn performance(this: &Global) -> JsValue; + + /// Type for the [`Performance` object](https://developer.mozilla.org/en-US/docs/Web/API/Performance). + pub(super) type Performance; + + /// Binding to [`Performance.now()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now). + #[wasm_bindgen(method)] + pub(super) fn now(this: &Performance) -> f64; + + /// Binding to [`Performance.timeOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin). + #[cfg(target_feature = "atomics")] + #[wasm_bindgen(method, getter, js_name = timeOrigin)] + pub(super) fn time_origin(this: &Performance) -> f64; +} + +thread_local! { + pub(super) static PERFORMANCE: Performance = { + let global: Global = js_sys::global().unchecked_into(); + let performance = global.performance(); + + if performance.is_undefined() { + panic!("`Performance` object not found") + } else { + performance.unchecked_into() + } + }; +} + + +// ---------------- taken from web-time/instant.rs + +thread_local! { + static ORIGIN: f64 = PERFORMANCE.with(Performance::time_origin); +} + +/// This will get a Duration from a synchronized start point, whether in webworkers or the main browser thread. +/// +/// # Panics +/// +/// This call will panic if the [`Performance` object] was not found, e.g. +/// calling from a [worklet]. +/// +/// [`Performance` object]: https://developer.mozilla.org/en-US/docs/Web/API/performance_property +/// [worklet]: https://developer.mozilla.org/en-US/docs/Web/API/Worklet +#[must_use] +pub fn now_as_calibrated_timestamp() -> core::time::Duration { + let now = PERFORMANCE.with(|performance| { + return ORIGIN.with(|origin| performance.now() + origin); + }); + time_stamp_to_duration(now) +} + +/// Converts a `DOMHighResTimeStamp` to a [`Duration`]. +/// +/// # Note +/// +/// Keep in mind that like [`Duration::from_secs_f64()`] this doesn't do perfect +/// rounding. +#[allow( + clippy::as_conversions, + clippy::cast_possible_truncation, + clippy::cast_sign_loss +)] +fn time_stamp_to_duration(time_stamp: f64) -> core::time::Duration { + core::time::Duration::from_millis(time_stamp.trunc() as u64) + + core::time::Duration::from_nanos((time_stamp.fract() * 1.0e6).round() as u64) +} From c9974b4cca64ae5df10360089df638be5ffcee85 Mon Sep 17 00:00:00 2001 From: Samuel Maier Date: Fri, 6 Sep 2024 18:03:25 +0200 Subject: [PATCH 2/4] actually warn on web workers --- embassy-time/src/driver_wasm.rs | 111 +++++++++++++++++--------------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/embassy-time/src/driver_wasm.rs b/embassy-time/src/driver_wasm.rs index 492a9ee2ab..3fbf5422bc 100644 --- a/embassy-time/src/driver_wasm.rs +++ b/embassy-time/src/driver_wasm.rs @@ -45,22 +45,34 @@ embassy_time_driver::time_driver_impl!(static DRIVER: TimeDriver = TimeDriver { alarms: UninitCell::uninit(), }); +#[cfg(feature = "panic_on_webworker")] +thread_local! { + static CHECK_THREAD: Once = Once::new(); +} + impl TimeDriver { fn ensure_init(&self) { self.once.call_once(|| unsafe { self.alarms.write(Mutex::new([ALARM_NEW; ALARM_COUNT])); - #[cfg(feature = "panic_on_webworker")] - assert!(!is_web_worker_thread(), "Timer currently has issues on Web Workers: https://github.com/embassy-rs/embassy/issues/3313"); + }); + #[cfg(feature = "panic_on_webworker")] + CHECK_THREAD.with(|val| { + val.call_once(|| { + assert!( + !is_web_worker_thread(), + "Timer currently has issues on Web Workers: https://github.com/embassy-rs/embassy/issues/3313" + ); + }) }); } } impl Driver for TimeDriver { - fn now(&self) -> u64 { - self.ensure_init(); - // this is calibrated with timeOrigin. - now_as_calibrated_timestamp().as_micros() as u64 - } + fn now(&self) -> u64 { + self.ensure_init(); + // this is calibrated with timeOrigin. + now_as_calibrated_timestamp().as_micros() as u64 + } unsafe fn allocate_alarm(&self) -> Option { let id = self.alarm_count.fetch_update(Ordering::AcqRel, Ordering::Acquire, |x| { @@ -138,7 +150,9 @@ impl UninitCell { } fn is_web_worker_thread() -> bool { - js_sys::eval("typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope").unwrap().is_truthy() + js_sys::eval("typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope") + .unwrap() + .is_truthy() } // ---------------- taken from web-time/js.rs @@ -147,48 +161,47 @@ use wasm_bindgen::{JsCast, JsValue}; #[wasm_bindgen] extern "C" { - /// Type for the [global object](https://developer.mozilla.org/en-US/docs/Glossary/Global_object). - type Global; - - /// Returns the [`Performance`](https://developer.mozilla.org/en-US/docs/Web/API/Performance) object. - #[wasm_bindgen(method, getter)] - fn performance(this: &Global) -> JsValue; - - /// Type for the [`Performance` object](https://developer.mozilla.org/en-US/docs/Web/API/Performance). - pub(super) type Performance; - - /// Binding to [`Performance.now()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now). - #[wasm_bindgen(method)] - pub(super) fn now(this: &Performance) -> f64; - - /// Binding to [`Performance.timeOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin). - #[cfg(target_feature = "atomics")] - #[wasm_bindgen(method, getter, js_name = timeOrigin)] - pub(super) fn time_origin(this: &Performance) -> f64; + /// Type for the [global object](https://developer.mozilla.org/en-US/docs/Glossary/Global_object). + type Global; + + /// Returns the [`Performance`](https://developer.mozilla.org/en-US/docs/Web/API/Performance) object. + #[wasm_bindgen(method, getter)] + fn performance(this: &Global) -> JsValue; + + /// Type for the [`Performance` object](https://developer.mozilla.org/en-US/docs/Web/API/Performance). + pub(super) type Performance; + + /// Binding to [`Performance.now()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now). + #[wasm_bindgen(method)] + pub(super) fn now(this: &Performance) -> f64; + + /// Binding to [`Performance.timeOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin). + #[cfg(target_feature = "atomics")] + #[wasm_bindgen(method, getter, js_name = timeOrigin)] + pub(super) fn time_origin(this: &Performance) -> f64; } thread_local! { - pub(super) static PERFORMANCE: Performance = { - let global: Global = js_sys::global().unchecked_into(); - let performance = global.performance(); - - if performance.is_undefined() { - panic!("`Performance` object not found") - } else { - performance.unchecked_into() - } - }; -} + pub(super) static PERFORMANCE: Performance = { + let global: Global = js_sys::global().unchecked_into(); + let performance = global.performance(); + if performance.is_undefined() { + panic!("`Performance` object not found") + } else { + performance.unchecked_into() + } + }; +} // ---------------- taken from web-time/instant.rs thread_local! { - static ORIGIN: f64 = PERFORMANCE.with(Performance::time_origin); + static ORIGIN: f64 = PERFORMANCE.with(Performance::time_origin); } -/// This will get a Duration from a synchronized start point, whether in webworkers or the main browser thread. -/// +/// This will get a Duration from a synchronized start point, whether in webworkers or the main browser thread. +/// /// # Panics /// /// This call will panic if the [`Performance` object] was not found, e.g. @@ -198,10 +211,10 @@ thread_local! { /// [worklet]: https://developer.mozilla.org/en-US/docs/Web/API/Worklet #[must_use] pub fn now_as_calibrated_timestamp() -> core::time::Duration { - let now = PERFORMANCE.with(|performance| { - return ORIGIN.with(|origin| performance.now() + origin); - }); - time_stamp_to_duration(now) + let now = PERFORMANCE.with(|performance| { + return ORIGIN.with(|origin| performance.now() + origin); + }); + time_stamp_to_duration(now) } /// Converts a `DOMHighResTimeStamp` to a [`Duration`]. @@ -210,12 +223,8 @@ pub fn now_as_calibrated_timestamp() -> core::time::Duration { /// /// Keep in mind that like [`Duration::from_secs_f64()`] this doesn't do perfect /// rounding. -#[allow( - clippy::as_conversions, - clippy::cast_possible_truncation, - clippy::cast_sign_loss -)] +#[allow(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn time_stamp_to_duration(time_stamp: f64) -> core::time::Duration { - core::time::Duration::from_millis(time_stamp.trunc() as u64) - + core::time::Duration::from_nanos((time_stamp.fract() * 1.0e6).round() as u64) + core::time::Duration::from_millis(time_stamp.trunc() as u64) + + core::time::Duration::from_nanos((time_stamp.fract() * 1.0e6).round() as u64) } From 7e9f6f91d548add739189f6b6e1232d5c2606222 Mon Sep 17 00:00:00 2001 From: Samuel Maier Date: Sat, 7 Sep 2024 08:20:43 +0200 Subject: [PATCH 3/4] Fixing build on stable --- embassy-time/src/driver_wasm.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/embassy-time/src/driver_wasm.rs b/embassy-time/src/driver_wasm.rs index 3fbf5422bc..946d9601cf 100644 --- a/embassy-time/src/driver_wasm.rs +++ b/embassy-time/src/driver_wasm.rs @@ -176,7 +176,6 @@ extern "C" { pub(super) fn now(this: &Performance) -> f64; /// Binding to [`Performance.timeOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin). - #[cfg(target_feature = "atomics")] #[wasm_bindgen(method, getter, js_name = timeOrigin)] pub(super) fn time_origin(this: &Performance) -> f64; } From 817b928670e6f90d263cf15f8cbe8f58fe464eac Mon Sep 17 00:00:00 2001 From: Samuel Maier Date: Sat, 7 Sep 2024 08:30:27 +0200 Subject: [PATCH 4/4] Remove dead code --- embassy-time/src/driver_wasm.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/embassy-time/src/driver_wasm.rs b/embassy-time/src/driver_wasm.rs index 946d9601cf..a455ffe187 100644 --- a/embassy-time/src/driver_wasm.rs +++ b/embassy-time/src/driver_wasm.rs @@ -143,12 +143,6 @@ impl UninitCell { } } -impl UninitCell { - pub unsafe fn read(&self) -> T { - ptr::read(self.as_mut_ptr()) - } -} - fn is_web_worker_thread() -> bool { js_sys::eval("typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope") .unwrap()