Skip to content
Open
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
1 change: 1 addition & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
10 changes: 10 additions & 0 deletions core/runtime/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ impl<L: Logger + Debug + 'static> RuntimeExtension for ConsoleExtension<L> {
}
}

/// Register the `Performance` JavaScript object.
#[derive(Copy, Clone, Debug)]
pub struct PerformanceExtension;

impl RuntimeExtension for PerformanceExtension {
fn register(self, _realm: Option<Realm>, 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)]
Expand Down
4 changes: 4 additions & 0 deletions core/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
152 changes: 152 additions & 0 deletions core/runtime/src/performance/mod.rs
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason you don't use the boa_class proc macro here? It would significantly reduce the code.

/// 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<JsValue> {
// 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::<Self>().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<JsValue> {
// 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::<Self>().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))
}
}
52 changes: 52 additions & 0 deletions core/runtime/src/performance/tests.rs
Original file line number Diff line number Diff line change
@@ -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');
"#}),
]);
}
Loading