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) +}