diff --git a/cli/src/main.rs b/cli/src/main.rs index 0e3aa22869e..d8ea307e81f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -559,6 +559,7 @@ fn add_runtime(printer: SharedExternalPrinterLogger, context: &mut Context) { boa_runtime::register( ( boa_runtime::extensions::ConsoleExtension(printer), + boa_runtime::extensions::PerformanceExtension, #[cfg(feature = "fetch")] boa_runtime::extensions::FetchExtension( boa_runtime::fetch::BlockingReqwestFetcher::default(), diff --git a/core/runtime/src/extensions.rs b/core/runtime/src/extensions.rs index c8d803beb7e..c31a96b0d51 100644 --- a/core/runtime/src/extensions.rs +++ b/core/runtime/src/extensions.rs @@ -87,6 +87,16 @@ impl RuntimeExtension for ConsoleExtension { } } +/// Register the `Performance` JavaScript object. +#[derive(Copy, Clone, Debug)] +pub struct PerformanceExtension; + +impl RuntimeExtension for PerformanceExtension { + fn register(self, _realm: Option, context: &mut Context) -> JsResult<()> { + crate::performance::Performance::register(context) + } +} + /// Register the `fetch` JavaScript API with the specified [`crate::fetch::Fetcher`]. #[cfg(feature = "fetch")] #[derive(Debug)] diff --git a/core/runtime/src/lib.rs b/core/runtime/src/lib.rs index 8758e2bbd23..e441b1e0e24 100644 --- a/core/runtime/src/lib.rs +++ b/core/runtime/src/lib.rs @@ -116,11 +116,15 @@ pub mod fetch; pub mod interval; pub mod message; pub mod microtask; +pub mod performance; pub mod store; pub mod text; #[cfg(feature = "url")] pub mod url; +#[doc(inline)] +pub use performance::Performance; + pub mod extensions; use crate::extensions::{ diff --git a/core/runtime/src/performance/mod.rs b/core/runtime/src/performance/mod.rs new file mode 100644 index 00000000000..23e3081c4d6 --- /dev/null +++ b/core/runtime/src/performance/mod.rs @@ -0,0 +1,152 @@ +//! Boa's implementation of the `performance` Web API object. +//! +//! The `performance` object provides access to performance-related information. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [W3C High Resolution Time specification][spec] +//! +//! [spec]: https://w3c.github.io/hr-time/ +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Performance + +use boa_engine::{ + Context, JsData, JsNativeError, JsResult, JsValue, NativeFunction, + context::time::JsInstant, + js_string, + object::{FunctionObjectBuilder, ObjectInitializer}, + property::Attribute, +}; +use boa_gc::{Finalize, Trace}; + +#[cfg(test)] +mod tests; + +/// The `Performance` object. +#[derive(Debug, Trace, Finalize, JsData)] +pub struct Performance { + #[unsafe_ignore_trace] + time_origin: JsInstant, +} + +impl Performance { + /// Create a new `Performance` object. + #[must_use] + pub fn new(context: &Context) -> Self { + Self { + time_origin: context.clock().now(), + } + } + + /// Register the `Performance` object in the context. + /// + /// # Errors + /// Returns an error if the `performance` property already exists in the global object. + pub fn register(context: &mut Context) -> JsResult<()> { + let performance = Self::new(context); + + let get_time_origin = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_fn_ptr(Self::get_time_origin), + ) + .name(js_string!("get timeOrigin")) + .length(0) + .build(); + + let performance_obj = ObjectInitializer::with_native_data(performance, context) + .function(NativeFunction::from_fn_ptr(Self::now), js_string!("now"), 0) + .accessor( + js_string!("timeOrigin"), + Some(get_time_origin), + None, + Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .build(); + + context.register_global_property( + js_string!("performance"), + performance_obj, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + )?; + + Ok(()) + } + + /// `Performance.timeOrigin` getter + /// + /// The `timeOrigin` read-only property returns the high resolution timestamp + /// that is used as the baseline for performance-related timestamps. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [W3C specification][spec] + /// + /// [spec]: https://w3c.github.io/hr-time/#dom-performance-timeorigin + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin + fn get_time_origin( + this: &JsValue, + _args: &[JsValue], + _context: &mut Context, + ) -> JsResult { + // The timeOrigin attribute MUST return the number of milliseconds in the duration returned by get + // time origin timestamp for the relevant global object of this. + // + // The time values returned when getting Performance.timeOrigin MUST use the same monotonic clock + // that is shared by time origins, and whose reference point is the [ECMA-262] time definition - + // see 9. Security Considerations. + + let obj = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("'this' is not a Performance object") + })?; + + let performance = obj.downcast_ref::().ok_or_else(|| { + JsNativeError::typ().with_message("'this' is not a Performance object") + })?; + + #[allow(clippy::cast_precision_loss)] + let time_origin_millis = performance.time_origin.nanos_since_epoch() as f64 / 1_000_000.0; + Ok(JsValue::from(time_origin_millis)) + } + + /// `performance.now()` + /// + /// The `now()` method returns a high resolution timestamp in milliseconds. + /// It represents the time elapsed since `time_origin`. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [W3C specification][spec] + /// + /// [spec]: https://w3c.github.io/hr-time/#dom-performance-now + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now + fn now(this: &JsValue, _args: &[JsValue], context: &mut Context) -> JsResult { + // The now() method MUST return the number of milliseconds in the current high resolution time + // given this's relevant global object (a duration). + // + // The time values returned when calling the now() method on Performance objects with the + // same time origin MUST use the same monotonic clock. The difference between any two + // chronologically recorded time values returned from the now() method MUST never be + // negative if the two time values have the same time origin. + + let obj = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("'this' is not a Performance object") + })?; + + let performance = obj.downcast_ref::().ok_or_else(|| { + JsNativeError::typ().with_message("'this' is not a Performance object") + })?; + + // Step 1: Get time origin from the Performance object + let time_origin = performance.time_origin; + + // Step 2: Get current high resolution time + let now = context.clock().now(); + + // Step 3: Calculate duration from time_origin to now in milliseconds + let elapsed = now - time_origin; + + #[allow(clippy::cast_precision_loss)] + let milliseconds = elapsed.as_nanos() as f64 / 1_000_000.0; + + Ok(JsValue::from(milliseconds)) + } +} diff --git a/core/runtime/src/performance/tests.rs b/core/runtime/src/performance/tests.rs new file mode 100644 index 00000000000..a24e9f4e99c --- /dev/null +++ b/core/runtime/src/performance/tests.rs @@ -0,0 +1,52 @@ +use crate::Performance; +use crate::test::{TestAction, run_test_actions}; +use indoc::indoc; + +const TEST_HARNESS: &str = r#" +function assert_true(condition, message) { + if (!condition) { + throw new Error(`Assertion failed: ${message || ''}`); + } +} +"#; + +#[test] +fn performance_now_returns_number() { + run_test_actions([ + TestAction::inspect_context(|ctx| { + Performance::register(ctx).unwrap(); + }), + TestAction::run(TEST_HARNESS), + TestAction::run(indoc! {r#" + assert_true(typeof performance.now() === 'number'); + "#}), + ]); +} + +#[test] +fn performance_now_increases() { + run_test_actions([ + TestAction::inspect_context(|ctx| { + Performance::register(ctx).unwrap(); + }), + TestAction::run(TEST_HARNESS), + TestAction::run(indoc! {r#" + const t1 = performance.now(); + const t2 = performance.now(); + assert_true(t2 >= t1, 'time should increase'); + "#}), + ]); +} + +#[test] +fn performance_now_is_non_negative() { + run_test_actions([ + TestAction::inspect_context(|ctx| { + Performance::register(ctx).unwrap(); + }), + TestAction::run(TEST_HARNESS), + TestAction::run(indoc! {r#" + assert_true(performance.now() >= 0, 'time should be non-negative'); + "#}), + ]); +}