Skip to content

Commit

Permalink
Use the wasi functions for time retrieval, and yield less often to th…
Browse files Browse the repository at this point in the history
…e JS engine (#481)

* Use the wasi functions for time retrieval

* Docfix

* Minor change

* Not sure Date.now() is an integer

* Remove obsolete comment

* Yield less often to the JS engine

* Whoops, it's in seconds and not milliseconds
  • Loading branch information
tomaka authored Apr 27, 2023
1 parent 163e0a1 commit b42882d
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 148 deletions.
1 change: 1 addition & 0 deletions wasm-node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- As NodeJS v14 reaches its end of life on April 30th 2023, the minimum NodeJS version required to run smoldot is now v16. The smoldot Wasm binary now has SIMD enabled, meaning that the minimum Deno version required to run smoldot is now v1.9.
- When receiving an identify request through the libp2p protocol, smoldot now sends back `smoldot-light-wasm vX.X.X` (with proper version numbers) as its agent name and version, instead of previously just `smoldot`. ([#417](https://github.com/smol-dot/smoldot/pull/417))
- Yielding to the browser using `setTimeout` and `setImmediate` is now done less frequently in order to reduce the overhead of doing so. ([#481](https://github.com/smol-dot/smoldot/pull/481))

### Fixed

Expand Down
11 changes: 0 additions & 11 deletions wasm-node/javascript/src/instance/bindings-smoldot-light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ export interface Config {
*/
bufferIndices: Array<Uint8Array>,

/**
* Returns the number of milliseconds since an arbitrary epoch.
*/
performanceNow: () => number,

/**
* Tries to open a new connection using the given configuration.
*
Expand Down Expand Up @@ -307,12 +302,6 @@ export default function (config: Config): { imports: WebAssembly.ModuleImports,
}
},

// Must return the UNIX time in milliseconds.
unix_time_ms: () => Date.now(),

// Must return the value of a monotonic clock in milliseconds.
monotonic_clock_ms: () => config.performanceNow(),

// Must call `timer_finished` after the given number of milliseconds has elapsed.
start_timer: (id: number, ms: number) => {
if (killedTracked.killed) return;
Expand Down
41 changes: 41 additions & 0 deletions wasm-node/javascript/src/instance/bindings-wasi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export interface Config {
*/
getRandomValues: (buffer: Uint8Array) => void,

/**
* Returns the number of milliseconds since an arbitrary epoch.
*/
performanceNow: () => number,

/**
* List of environment variables to feed to the Rust program. An array of strings.
* Example: `["RUST_BACKTRACE=1", "RUST_LOG=foo"];`
Expand Down Expand Up @@ -75,6 +80,42 @@ export default (config: Config): WebAssembly.ModuleImports => {
return 0;
},

clock_time_get: (clockId: number, _precision: bigint, outPtr: number): number => {
// See <https://github.com/rust-lang/rust/blob/master/library/std/src/sys/wasi/time.rs>
// and <docs.rs/wasi/> for help.

const instance = config.instance!;
const mem = new Uint8Array(instance.exports.memory.buffer);
outPtr >>>= 0;

// We ignore the precision, as it can't be implemented anyway.

switch (clockId) {
case 0: {
// Realtime clock.
const now = BigInt(Math.floor(Date.now())) * BigInt(1_000_000);
buffer.writeUInt64LE(mem, outPtr, now)

// Success.
return 0;
}
case 1: {
// Monotonic clock.
const nowMs = config.performanceNow();
const nowMsInt = Math.floor(nowMs);
const now = BigInt(nowMsInt) * BigInt(1_000_000) +
BigInt(Math.floor(((nowMs - nowMsInt) * 1_000_000)));
buffer.writeUInt64LE(mem, outPtr, now)

// Success.
return 0;
}
default:
// Return an `EINVAL` error.
return 28
}
},

// Writing to a file descriptor is used in order to write to stdout/stderr.
fd_write: (fd: number, addr: number, num: number, outPtr: number) => {
const instance = config.instance!;
Expand Down
12 changes: 12 additions & 0 deletions wasm-node/javascript/src/instance/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ export function writeUInt32LE(buffer: Uint8Array, offset: number, value: number)
buffer[offset] = value & 0xff
}

export function writeUInt64LE(buffer: Uint8Array, offset: number, value: bigint) {
checkRange(buffer, offset, 8);
buffer[offset + 7] = Number((value >> BigInt(56)) & BigInt(0xff))
buffer[offset + 6] = Number((value >> BigInt(48)) & BigInt(0xff))
buffer[offset + 5] = Number((value >> BigInt(40)) & BigInt(0xff))
buffer[offset + 4] = Number((value >> BigInt(32)) & BigInt(0xff))
buffer[offset + 3] = Number((value >> BigInt(24)) & BigInt(0xff))
buffer[offset + 2] = Number((value >> BigInt(16)) & BigInt(0xff))
buffer[offset + 1] = Number((value >> BigInt(8)) & BigInt(0xff))
buffer[offset] = Number(value & BigInt(0xff))
}

function checkRange(buffer: Uint8Array, offset: number, length: number) {
if (!Number.isInteger(offset) || offset < 0)
throw new RangeError()
Expand Down
2 changes: 1 addition & 1 deletion wasm-node/javascript/src/instance/raw-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ export async function startInstance(config: Config, platformBindings: PlatformBi
// Used to bind with the smoldot-light bindings. See the `bindings-smoldot-light.js` file.
const smoldotJsConfig: SmoldotBindingsConfig = {
bufferIndices,
performanceNow: platformBindings.performanceNow,
connect: platformBindings.connect,
onPanic: (message) => {
killAll();
Expand All @@ -119,6 +118,7 @@ export async function startInstance(config: Config, platformBindings: PlatformBi
const wasiConfig: WasiConfig = {
envVars: [],
getRandomValues: platformBindings.getRandomValues,
performanceNow: platformBindings.performanceNow,
onProcExit: (retCode) => {
killAll();
config.onWasmPanic(`proc_exit called: ${retCode}`)
Expand Down
36 changes: 2 additions & 34 deletions wasm-node/rust/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,47 +130,15 @@ extern "C" {
/// virtual machine at offset `ptr` and with length `len`.
pub fn log(level: u32, target_ptr: u32, target_len: u32, message_ptr: u32, message_len: u32);

/// Must return the number of milliseconds that have passed since the UNIX epoch, ignoring
/// leap seconds.
///
/// Must never return `NaN` or infinite.
///
/// This is typically implemented by calling `Date.now()`.
///
/// See <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now>
///
/// > **Note**: Ideally this function isn't needed. The wasi target supports clocks through
/// > the `clock_time_get` syscall. However, since `clock_time_get` uses `u64s`, and
/// > browsers don't support `u64s`, using it causes an unbypassable exception. See
/// > also <https://github.com/dcodeIO/webassembly/issues/26#issuecomment-410157370>.
pub fn unix_time_ms() -> f64;

/// Must return the number of milliseconds that have passed since an arbitrary point in time.
///
/// Must never return `NaN` or infinite.
///
/// Contrary to [`unix_time_ms`], the returned value must never be inferior to a value
/// previously returned. Consequently, this must not be implemented using `Date.now()`, whose
/// value can decrease if the user adjusts their machine's clock, but rather with
/// `Performance.now()` or similar.
///
/// See <https://developer.mozilla.org/fr/docs/Web/API/Performance/now>
///
/// > **Note**: Ideally this function isn't needed. The wasi target supports clocks through
/// > the `clock_time_get` syscall. However, since `clock_time_get` uses `u64s`, and
/// > browsers don't support `u64s`, using it causes an unbypassable exception. See
/// > also <https://github.com/dcodeIO/webassembly/issues/26#issuecomment-410157370>.
pub fn monotonic_clock_ms() -> f64;

/// After at least `milliseconds` milliseconds have passed, must call [`timer_finished`] with
/// the `id` passed as parameter.
///
/// It is not a logic error to call [`timer_finished`] *before* `milliseconds` milliseconds
/// have passed, and this will likely cause smoldot to restart a new timer for the remainder
/// of the duration.
///
/// When [`timer_finished`] is called, the value of [`monotonic_clock_ms`] must have increased
/// by at least the given number of `milliseconds`.
/// When [`timer_finished`] is called, the value of the monotonic clock (in the WASI bindings)
/// must have increased by at least the given number of `milliseconds`.
///
/// If `milliseconds` is 0, [`timer_finished`] should be called as soon as possible.
///
Expand Down
29 changes: 19 additions & 10 deletions wasm-node/rust/src/cpu_rate_limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use core::{
time::Duration,
};

use std::time::Instant;

/// Wraps around a `Future` and enforces an upper bound to the CPU consumed by the polling of
/// this `Future`.
///
Expand All @@ -34,6 +36,10 @@ pub struct CpuRateLimiter<T> {
inner: T,
max_divided_by_rate_limit_minus_one: f64,

/// Instead of sleeping regularly for short amounts of time, we instead continue running only
/// until the time we have to sleep reaches a certain threshold.
sleep_deprevation_sec: f64,

/// Prevent `self.inner.poll` from being called before this `Delay` is ready.
#[pin]
prevent_poll_until: crate::timers::Delay,
Expand All @@ -55,6 +61,7 @@ impl<T> CpuRateLimiter<T> {
CpuRateLimiter {
inner,
max_divided_by_rate_limit_minus_one,
sleep_deprevation_sec: 0.0,
prevent_poll_until: crate::timers::Delay::new(Duration::new(0, 0)),
}
}
Expand All @@ -75,30 +82,32 @@ impl<T: Future> Future for CpuRateLimiter<T> {
return Poll::Pending;
}

let before_polling = crate::Instant::now();
let before_polling = Instant::now();

match this.inner.poll(cx) {
Poll::Ready(value) => Poll::Ready(value),
Poll::Pending => {
let after_polling = crate::Instant::now();
let after_polling = Instant::now();

// Time it took to execute `poll`.
let poll_duration = after_polling - before_polling;

// In order to enforce the rate limiting, we prevent `poll` from executing
// for a certain amount of time.
// The base equation here is: `(after_poll_sleep + poll_duration) * rate_limit == poll_duration * u32::max_value()`.
// Because `Duration::mul_f64` and `Duration::from_secs_f64` panic in case of
// overflow, we need to do the operation of multiplying and checking the bounds
// manually.
let after_poll_sleep =
poll_duration.as_secs_f64() * *this.max_divided_by_rate_limit_minus_one;
debug_assert!(after_poll_sleep >= 0.0 && !after_poll_sleep.is_nan());

this.prevent_poll_until.set(crate::timers::Delay::new_at(
after_polling
+ Duration::try_from_secs_f64(after_poll_sleep).unwrap_or(Duration::MAX),
));
*this.sleep_deprevation_sec += after_poll_sleep;

if *this.sleep_deprevation_sec > 0.005 {
this.prevent_poll_until.set(crate::timers::Delay::new_at(
after_polling
+ Duration::try_from_secs_f64(*this.sleep_deprevation_sec)
.unwrap_or(Duration::MAX),
));
*this.sleep_deprevation_sec = 0.0;
}

Poll::Pending
}
Expand Down
81 changes: 1 addition & 80 deletions wasm-node/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,7 @@
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(unused_crate_dependencies)]

use core::{
cmp::Ordering,
num::NonZeroU32,
ops::{Add, Sub},
pin::Pin,
str,
sync::atomic,
time::Duration,
};
use core::{num::NonZeroU32, pin::Pin, str, sync::atomic, time::Duration};
use futures_util::{stream, FutureExt as _, Stream as _, StreamExt as _};
use smoldot_light::HandleRpcError;
use std::{
Expand All @@ -54,77 +46,6 @@ fn start_timer_wrap(duration: Duration, closure: impl FnOnce() + 'static) {
unsafe { bindings::start_timer(timer_id, duration.as_secs_f64() * 1000.0) }
}

#[derive(Debug, Copy, Clone)]
pub struct Instant {
/// Milliseconds.
inner: f64,
}

impl PartialEq for Instant {
fn eq(&self, other: &Instant) -> bool {
debug_assert!(self.inner.is_finite());
self.inner == other.inner
}
}

// This trait is ok to implement because `self.inner` is always finite.
impl Eq for Instant {}

impl PartialOrd for Instant {
fn partial_cmp(&self, other: &Instant) -> Option<Ordering> {
self.inner.partial_cmp(&other.inner)
}
}

impl Ord for Instant {
fn cmp(&self, other: &Self) -> Ordering {
debug_assert!(self.inner.is_finite());
self.inner.partial_cmp(&other.inner).unwrap()
}
}

impl Instant {
pub fn now() -> Instant {
let value = unsafe { bindings::monotonic_clock_ms() };
debug_assert!(value.is_finite());
Instant { inner: value }
}
}

impl Add<Duration> for Instant {
type Output = Instant;

fn add(self, other: Duration) -> Instant {
let new_val = self.inner + other.as_millis() as f64;
// Just like the `Add` implementation of the actual `Instant`, we panic if the value can't
// be represented.
assert!(new_val.is_finite());
Instant { inner: new_val }
}
}

impl Sub<Duration> for Instant {
type Output = Instant;

fn sub(self, other: Duration) -> Instant {
let new_val = self.inner - other.as_millis() as f64;
// Just like the `Sub` implementation of the actual `Instant`, we panic if the value can't
// be represented.
assert!(new_val.is_finite());
Instant { inner: new_val }
}
}

impl Sub<Instant> for Instant {
type Output = Duration;

fn sub(self, other: Instant) -> Duration {
let ms = self.inner - other.inner;
assert!(ms >= 0.0);
Duration::from_millis(ms as u64)
}
}

static CLIENT: Mutex<Option<init::Client<platform::Platform, ()>>> = Mutex::new(None);

fn init(
Expand Down
15 changes: 6 additions & 9 deletions wasm-node/rust/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use std::{
atomic::{AtomicU64, Ordering},
Mutex,
},
time::{Instant, SystemTime, UNIX_EPOCH},
};

/// Total number of bytes that all the connections created through [`Platform`] combined have
Expand Down Expand Up @@ -57,7 +58,7 @@ impl Platform {
impl smoldot_light::platform::PlatformRef for Platform {
type Delay = Delay;
type Yield = Yield;
type Instant = crate::Instant;
type Instant = Instant;
type Connection = ConnectionWrapper; // Entry in the ̀`CONNECTIONS` map.
type Stream = StreamWrapper; // Entry in the ̀`STREAMS` map and a read buffer.
type ConnectFuture = future::BoxFuture<
Expand All @@ -77,19 +78,15 @@ impl smoldot_light::platform::PlatformRef for Platform {
>;

fn now_from_unix_epoch(&self) -> Duration {
let value = unsafe { bindings::unix_time_ms() };
debug_assert!(value.is_finite());
// The documentation of `now_from_unix_epoch()` mentions that it's ok to panic if we're
// before the UNIX epoch.
assert!(
value >= 0.0,
"running before the UNIX epoch isn't supported"
);
Duration::from_secs_f64(value / 1000.0)
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| panic!())
}

fn now(&self) -> Self::Instant {
crate::Instant::now()
Instant::now()
}

fn sleep(&self, duration: Duration) -> Self::Delay {
Expand Down
4 changes: 1 addition & 3 deletions wasm-node/rust/src/timers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use core::{
time::Duration,
};
use futures_util::future;
use std::{collections::BinaryHeap, sync::Mutex};
use std::{collections::BinaryHeap, sync::Mutex, time::Instant};

pub(crate) fn timer_finished(timer_id: u32) {
let callback = {
Expand All @@ -41,8 +41,6 @@ pub(crate) fn timer_finished(timer_id: u32) {
callback();
}

use super::Instant;

/// `Future` that automatically wakes up after a certain amount of time has elapsed.
pub struct Delay {
/// Index in `TIMERS::timers`. Guaranteed to have `is_obsolete` equal to `false`.
Expand Down

0 comments on commit b42882d

Please sign in to comment.