Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an internal time type and Clock trait #4149

Merged
merged 2 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions core/engine/src/builtins/date/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ use crate::{
},
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
},
context::{
intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
HostHooks,
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
error::JsNativeError,
js_string,
object::{internal_methods::get_prototype_from_constructor, JsObject},
Expand Down Expand Up @@ -53,8 +50,8 @@ impl Date {
}

/// Creates a new `Date` from the current UTC time of the host.
pub(crate) fn utc_now(hooks: &dyn HostHooks) -> Self {
Self(hooks.utc_now() as f64)
pub(crate) fn utc_now(context: &mut Context) -> Self {
Self(context.clock().now().millis_since_epoch() as f64)
}
}

Expand Down Expand Up @@ -208,7 +205,7 @@ impl BuiltInConstructor for Date {
// 1. If NewTarget is undefined, then
if new_target.is_undefined() {
// a. Let now be the time value (UTC) identifying the current time.
let now = context.host_hooks().utc_now();
let now = context.clock().now().millis_since_epoch();

// b. Return ToDateString(now).
return Ok(JsValue::from(to_date_string_t(
Expand All @@ -222,7 +219,7 @@ impl BuiltInConstructor for Date {
// 3. If numberOfArgs = 0, then
[] => {
// a. Let dv be the time value (UTC) identifying the current time.
Self::utc_now(context.host_hooks().as_ref())
Self::utc_now(context)
}
// 4. Else if numberOfArgs = 1, then
// a. Let value be values[0].
Expand Down Expand Up @@ -326,7 +323,7 @@ impl Date {
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn now(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::new(context.host_hooks().utc_now()))
Ok(JsValue::new(context.clock().now().millis_since_epoch()))
}

/// `Date.parse()`
Expand Down
4 changes: 4 additions & 0 deletions core/engine/src/context/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ pub trait HostHooks {
/// which can cause panics if the target doesn't support [`SystemTime::now`][time].
///
/// [time]: std::time::SystemTime::now
#[deprecated(
since = "0.21.0",
note = "Use `context.clock().now().millis_since_epoch()` instead"
)]
Comment on lines +185 to +188
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh, I like this! It should make it easier for users to migrate, and maybe we can comment when the deprecated API should be removed e.g. a version after deprecation or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always update the note later with more info.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave the deprecation policy of this repo to the leadership :)

fn utc_now(&self) -> i64 {
let now = OffsetDateTime::now_utc();
now.unix_timestamp() * 1000 + i64::from(now.millisecond())
Expand Down
27 changes: 27 additions & 0 deletions core/engine/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ use crate::{

use self::intrinsics::StandardConstructor;

pub mod time;
use crate::context::time::StdClock;
pub use time::Clock;

mod hooks;
#[cfg(feature = "intl")]
pub(crate) mod icu;
Expand Down Expand Up @@ -113,6 +117,8 @@ pub struct Context {

host_hooks: Rc<dyn HostHooks>,

clock: Rc<dyn Clock>,

job_executor: Rc<dyn JobExecutor>,

module_loader: Rc<dyn ModuleLoader>,
Expand All @@ -137,6 +143,7 @@ impl std::fmt::Debug for Context {
.field("strict", &self.strict)
.field("job_executor", &"JobExecutor")
.field("hooks", &"HostHooks")
.field("clock", &"Clock")
.field("module_loader", &"ModuleLoader")
.field("optimizer_options", &self.optimizer_options);

Expand Down Expand Up @@ -553,6 +560,13 @@ impl Context {
self.host_hooks.clone()
}

/// Gets the internal clock.
#[inline]
#[must_use]
pub fn clock(&self) -> &dyn Clock {
self.clock.as_ref()
}

/// Gets the job executor.
#[inline]
#[must_use]
Expand Down Expand Up @@ -888,6 +902,7 @@ impl Context {
pub struct ContextBuilder {
interner: Option<Interner>,
host_hooks: Option<Rc<dyn HostHooks>>,
clock: Option<Rc<dyn Clock>>,
job_executor: Option<Rc<dyn JobExecutor>>,
module_loader: Option<Rc<dyn ModuleLoader>>,
can_block: bool,
Expand All @@ -904,12 +919,15 @@ impl std::fmt::Debug for ContextBuilder {
#[derive(Clone, Copy, Debug)]
struct HostHooks;
#[derive(Clone, Copy, Debug)]
struct Clock;
#[derive(Clone, Copy, Debug)]
struct ModuleLoader;

let mut out = f.debug_struct("ContextBuilder");

out.field("interner", &self.interner)
.field("host_hooks", &self.host_hooks.as_ref().map(|_| HostHooks))
.field("clock", &self.clock.as_ref().map(|_| Clock))
.field(
"job_executor",
&self.job_executor.as_ref().map(|_| JobExecutor),
Expand Down Expand Up @@ -1026,6 +1044,13 @@ impl ContextBuilder {
self
}

/// Initializes the [`Clock`] for the context.
#[must_use]
pub fn clock<C: Clock + 'static>(mut self, clock: Rc<C>) -> Self {
self.clock = Some(clock);
self
}

/// Initializes the [`JobExecutor`] for the context.
#[must_use]
pub fn job_executor<Q: JobExecutor + 'static>(mut self, job_executor: Rc<Q>) -> Self {
Expand Down Expand Up @@ -1089,6 +1114,7 @@ impl ContextBuilder {
let root_shape = RootShape::default();

let host_hooks = self.host_hooks.unwrap_or(Rc::new(DefaultHooks));
let clock = self.clock.unwrap_or_else(|| Rc::new(StdClock));
let realm = Realm::create(host_hooks.as_ref(), &root_shape)?;
let vm = Vm::new(realm);

Expand Down Expand Up @@ -1129,6 +1155,7 @@ impl ContextBuilder {
instructions_remaining: self.instructions_remaining,
kept_alive: Vec::new(),
host_hooks,
clock,
job_executor,
module_loader,
optimizer_options: OptimizerOptions::OPTIMIZE_ALL,
Expand Down
213 changes: 213 additions & 0 deletions core/engine/src/context/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//! Clock related types and functions.

/// A monotonic instant in time, in the Boa engine.
///
/// This type is guaranteed to be monotonic, i.e. if two instants
/// are compared, the later one will always be greater than the
/// earlier one. It is also always guaranteed to be greater than
/// or equal to the Unix epoch.
///
/// This should not be used to keep dates or times, but only to
/// measure the current time in the engine.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct JsInstant {
/// The duration of time since the Unix epoch.
inner: std::time::Duration,
}

impl JsInstant {
/// Creates a new `JsInstant` from the given number of seconds and nanoseconds.
#[must_use]
pub fn new(secs: u64, nanos: u32) -> Self {
let inner = std::time::Duration::new(secs, nanos);
Self::new_unchecked(inner)
}

/// Creates a new `JsInstant` from an unchecked duration since the Unix epoch.
#[must_use]
fn new_unchecked(inner: std::time::Duration) -> Self {
Self { inner }
}

/// Returns the number of milliseconds since the Unix epoch.
#[must_use]
pub fn millis_since_epoch(&self) -> u64 {
self.inner.as_millis() as u64
}

/// Returns the number of nanoseconds since the Unix epoch.
#[must_use]
pub fn nanos_since_epoch(&self) -> u128 {
self.inner.as_nanos()
}
}

/// A duration of time, inside the Boa engine.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct JsDuration {
inner: std::time::Duration,
}

impl JsDuration {
/// Creates a new `JsDuration` from the given number of milliseconds.
#[must_use]
pub fn from_millis(millis: u64) -> Self {
Self {
inner: std::time::Duration::from_millis(millis),
}
}

/// Returns the number of milliseconds in this duration.
#[must_use]
pub fn as_millis(&self) -> u64 {
self.inner.as_millis() as u64
}

/// Returns the number of seconds in this duration.
#[must_use]
pub fn as_secs(&self) -> u64 {
self.inner.as_secs()
}

/// Returns the number of nanoseconds in this duration.
#[must_use]
pub fn as_nanos(&self) -> u128 {
self.inner.as_nanos()
}
}

impl From<std::time::Duration> for JsDuration {
fn from(duration: std::time::Duration) -> Self {
Self { inner: duration }
}
}

impl From<JsDuration> for std::time::Duration {
fn from(duration: JsDuration) -> Self {
duration.inner
}
}

macro_rules! impl_duration_ops {
($($trait:ident $trait_fn:ident),*) => {
$(
impl std::ops::$trait for JsDuration {
type Output = JsDuration;

#[inline]
fn $trait_fn(self, rhs: JsDuration) -> Self::Output {
Self {
inner: std::ops::$trait::$trait_fn(self.inner, rhs.inner)
}
}
}
impl std::ops::$trait<JsDuration> for JsInstant {
type Output = JsInstant;

#[inline]
fn $trait_fn(self, rhs: JsDuration) -> Self::Output {
Self {
inner: std::ops::$trait::$trait_fn(self.inner, rhs.inner)
}
}
}
)*
};
}

impl_duration_ops!(Add add, Sub sub);

impl std::ops::Sub for JsInstant {
type Output = JsDuration;

#[inline]
fn sub(self, rhs: JsInstant) -> Self::Output {
JsDuration {
inner: self.inner - rhs.inner,
}
}
}

/// Implement a clock that can be used to measure time.
pub trait Clock {
/// Returns the current time.
fn now(&self) -> JsInstant;
}

/// A clock that uses the standard system clock.
#[derive(Debug, Clone, Copy, Default)]
pub struct StdClock;

impl Clock for StdClock {
fn now(&self) -> JsInstant {
let now = std::time::SystemTime::now();
let duration = now
.duration_since(std::time::UNIX_EPOCH)
.expect("System clock is before Unix epoch");

JsInstant::new_unchecked(duration)
}
}

/// A clock that uses a fixed time, useful for testing. The internal time is in milliseconds.
///
/// This clock will always return the same time, unless it is moved forward manually. It cannot
/// be moved backward or set to a specific time.
#[derive(Debug, Clone, Default)]
pub struct FixedClock(std::cell::RefCell<u64>);

impl FixedClock {
/// Creates a new `FixedClock` from the given number of milliseconds since the Unix epoch.
#[must_use]
pub fn from_millis(millis: u64) -> Self {
Self(std::cell::RefCell::new(millis))
}

/// Move the clock forward by the given number of milliseconds.
pub fn forward(&self, millis: u64) {
*self.0.borrow_mut() += millis;
}
}

impl Clock for FixedClock {
fn now(&self) -> JsInstant {
let millis = *self.0.borrow();
JsInstant::new_unchecked(std::time::Duration::new(
millis / 1000,
((millis % 1000) * 1_000_000) as u32,
))
}
}

#[test]
fn basic() {
let now = StdClock.now();
assert!(now.millis_since_epoch() > 0);
assert!(now.nanos_since_epoch() > 0);

let duration = JsDuration::from_millis(1000);
let later = now + duration;
assert!(later > now);

let earlier = now - duration;
assert!(earlier < now);

let diff = later - earlier;
assert_eq!(diff.as_millis(), 2000);

let fixed = FixedClock::from_millis(0);
let now2 = fixed.now();
assert_eq!(now2.millis_since_epoch(), 0);
assert!(now2 < now);

fixed.forward(1000);
let now3 = fixed.now();
assert_eq!(now3.millis_since_epoch(), 1000);
assert!(now3 > now2);

// End of time.
fixed.forward(u64::MAX - 1000);
let now4 = fixed.now();
assert_eq!(now4.millis_since_epoch(), u64::MAX);
assert!(now4 > now3);
}
8 changes: 3 additions & 5 deletions core/engine/src/object/builtins/jsdate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,9 @@ impl JsDate {
#[inline]
pub fn new(context: &mut Context) -> Self {
let prototype = context.intrinsics().constructors().date().prototype();
let inner = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(),
prototype,
Date::utc_now(context.host_hooks().as_ref()),
);
let now = Date::utc_now(context);
let inner =
JsObject::from_proto_and_data_with_shared_shape(context.root_shape(), prototype, now);

Self { inner }
}
Expand Down
Loading