Skip to content

Commit

Permalink
Use vendored performance API to handle timing on WASM
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
9SMTM6 committed Sep 6, 2024
1 parent db00f3f commit fe148f2
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 15 deletions.
5 changes: 3 additions & 2 deletions embassy-time/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down
106 changes: 93 additions & 13 deletions embassy-time/src/driver_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -37,33 +36,31 @@ struct TimeDriver {

once: Once,
alarms: UninitCell<Mutex<[AlarmState; ALARM_COUNT]>>,
zero_instant: UninitCell<StdInstant>,
}

const ALARM_NEW: AlarmState = AlarmState::new();
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<AlarmHandle> {
let id = self.alarm_count.fetch_update(Ordering::AcqRel, Ordering::Acquire, |x| {
Expand All @@ -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 || {
Expand All @@ -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 {
Expand Down Expand Up @@ -139,3 +136,86 @@ impl<T: Copy> UninitCell<T> {
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)
}

0 comments on commit fe148f2

Please sign in to comment.