From b3936c133b23ced3110ea2dcfb8a1b7a24debcea Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 15 Jul 2026 14:57:08 +0800 Subject: [PATCH] fix(performance): support User Timing in workers --- moli-renderer-v8/src/context_bootstrap.rs | 87 ++++++- .../exposed_interfaces/finalize.rs | 21 ++ .../exposed_interfaces/materialize.rs | 2 +- .../exposed_interfaces/metadata.rs | 6 + .../exposed_interfaces/realm_registry.rs | 4 + .../src/context_bootstrap/file_api/install.rs | 2 - .../src/context_bootstrap/message_ports.rs | 2 +- .../performance_observer_runtime/delivery.rs | 140 +++++++----- .../performance_observer_runtime/observer.rs | 146 +++++++++--- .../context_bootstrap/performance_runtime.rs | 17 +- .../performance_runtime/install.rs | 109 +++++++-- .../performance_runtime/window_state.rs | 10 +- .../src/context_bootstrap/shared/slots.rs | 4 + .../window_runtime/performance.rs | 9 +- .../script_vm/tests/browser_api/navigation.rs | 161 +------------ .../src/worker/global_scope/mod.rs | 48 +--- .../src/worker/thread/tests/lazy_storage.rs | 7 +- .../src/worker/thread/tests/postmessage.rs | 216 +++++++++++++++++- 18 files changed, 660 insertions(+), 331 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 3902addad9..cec044df5e 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -316,13 +316,13 @@ pub(crate) use self::media_queries::{ simple_object_event_listeners_snapshot, simple_object_event_set_ordered_handler, simple_object_event_target_add_listener, simple_object_event_target_remove_listener, }; -use self::message_ports::schedule_host_callback; pub(crate) use self::message_ports::{ MessagePortDeliveryRunResult, MessagePortRealmBinding, detach_message_port_owner_for_transfer, detach_transferred_message_port, dispatch_message_port_events_for_port_collecting_errors, dispatch_one_authorized_message_port_event, ensure_message_port_wrapper_for_id, ensure_message_port_wrapper_for_id_in_realm, message_port_id_from_object, }; +use self::message_ports::{schedule_host_callback, schedule_scope_callback}; pub(crate) use self::microtask_checkpoint::{ install_agent_microtask_checkpoint_tasks, run_end_of_microtask_checkpoint_tasks, }; @@ -776,6 +776,17 @@ struct WorkerAbortControllerTemplateDeclaration { abort: (), } +#[derive(Default, WebApiObject)] +#[webapi(fragment, prototype = "WorkerGlobalScope", enumerable)] +struct WorkerGlobalScopePerformancePrototypeDeclaration { + #[webapi( + accessor_property, + getter = worker_performance_getter_callback, + setter = worker_performance_setter_callback + )] + performance: (), +} + #[derive(Default, WebApiObject)] #[webapi(fragment, prototype = "WorkerGlobalScope", enumerable)] struct WorkerGlobalScopeCryptoPrototypeDeclaration { @@ -791,6 +802,75 @@ pub(crate) fn initialize_worker_fetch_realm_state<'s>( Ok(()) } +pub(crate) fn initialize_worker_performance_realm_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + global: v8::Local<'s, v8::Object>, +) -> Result<()> { + self::performance_runtime::install_worker_performance_runtime_state(scope, global)?; + install_worker_performance_global_attribute(scope, global) +} + +fn install_worker_performance_global_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + global: v8::Local<'s, v8::Object>, +) -> Result<()> { + let performance = get_own_static_property(scope, global, WINDOW_PERFORMANCE_SLOT) + .filter(|value| !value.is_undefined()) + .ok_or_else(|| anyhow!("worker performance runtime state did not install performance"))?; + let Some(prototype) = global_constructor_prototype(scope, "WorkerGlobalScope") else { + return define_global_value(scope, global, "performance", performance); + }; + WorkerGlobalScopePerformancePrototypeDeclaration::default() + .initialize(scope, prototype) + .map_err(|error| anyhow!("failed to initialize WorkerGlobalScope performance: {error}")) +} + +fn worker_performance_getter_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if !require_current_worker_global_receiver(scope, args.this()) { + return; + } + let global = scope.get_current_context().global(scope); + match get_own_static_property(scope, global, WINDOW_PERFORMANCE_SLOT) + .filter(|value| !value.is_undefined()) + { + Some(value) => rv.set(value), + None => rv.set_undefined(), + } +} + +fn worker_performance_setter_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let receiver = args.this(); + if !require_current_worker_global_receiver(scope, receiver) { + return; + } + let _ = receiver.define_own_property( + scope, + v8str(scope, "performance").into(), + args.get(0), + v8::PropertyAttribute::NONE, + ); +} + +fn require_current_worker_global_receiver( + scope: &mut v8::PinScope<'_, '_>, + receiver: v8::Local<'_, v8::Object>, +) -> bool { + let global = scope.get_current_context().global(scope); + if receiver.strict_equals(global.into()) { + return true; + } + throw_type_error(scope, "Illegal invocation"); + false +} + pub(crate) fn initialize_worker_crypto_realm_state<'s>( scope: &mut v8::PinScope<'s, '_>, global: v8::Local<'s, v8::Object>, @@ -830,11 +910,10 @@ fn worker_crypto_getter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let global = scope.get_current_context().global(scope); - if !args.this().strict_equals(global.into()) { - throw_type_error(scope, "Illegal invocation"); + if !require_current_worker_global_receiver(scope, args.this()) { return; } + let global = scope.get_current_context().global(scope); match self::crypto::ensure_worker_crypto_for_global(scope, global) { Ok(crypto) => rv.set(crypto.into()), Err(error) => throw_error( diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs index 71389fde62..6d1d4fbc55 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs @@ -3,6 +3,7 @@ use anyhow::Result; use super::materialize::{ ensure_intrinsic_interface_constructor, ensure_intrinsic_interface_prototype, }; +use super::metadata::RealmKind; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum RealmDependentFinalizer { @@ -11,6 +12,7 @@ enum RealmDependentFinalizer { XmlHttpRequestEventTargetState, NotificationPermission, PointerEventSecureContextSurface, + PerformanceWindowAccessors, PerformanceObserverSupportedEntryTypes, } @@ -49,6 +51,10 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] = "PointerEvent", RealmDependentFinalizer::PointerEventSecureContextSurface, ), + ( + "Performance", + RealmDependentFinalizer::PerformanceWindowAccessors, + ), ( "PerformanceObserver", RealmDependentFinalizer::PerformanceObserverSupportedEntryTypes, @@ -70,6 +76,7 @@ fn realm_dependent_finalizer(interface_name: &str) -> Option, interface_name: &str, + realm_kind: RealmKind, ) -> Result<()> { let prototype = ensure_intrinsic_interface_prototype(scope, interface_name)?; @@ -98,11 +105,24 @@ pub(super) fn finalize_materialized_interface( scope, prototype, )?; } + RealmDependentFinalizer::PerformanceWindowAccessors => { + if realm_kind == RealmKind::Window { + crate::context_bootstrap::performance_runtime::finalize_window_performance_realm_bindings( + scope, prototype, + )?; + } + } RealmDependentFinalizer::PerformanceObserverSupportedEntryTypes => { let constructor = ensure_intrinsic_interface_constructor(scope, "PerformanceObserver")?; + let supported_entry_types = if realm_kind == RealmKind::Window { + crate::context_bootstrap::performance_runtime::WINDOW_PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES + } else { + crate::context_bootstrap::performance_runtime::WORKER_PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES + }; crate::context_bootstrap::performance_runtime::finalize_performance_observer_realm_bindings( scope, constructor.into(), + supported_entry_types, ); } } @@ -144,6 +164,7 @@ mod tests { "XMLHttpRequestEventTarget", "Notification", "PointerEvent", + "Performance", "PerformanceObserver", ] ); diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs index eba493a6ae..d88c01af08 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs @@ -292,7 +292,7 @@ fn materialize_uninitialized_interface<'s>( public_interface, )?; realm.set_state(id, RealmInterfaceState::Finalizing)?; - finalize_materialized_interface(scope, metadata.name)?; + finalize_materialized_interface(scope, metadata.name, realm.realm_kind())?; realm.set_state(id, RealmInterfaceState::Ready)?; registry.record_materialization(id); Ok(public_interface.into()) diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs index b425c75eec..a30b5e4ea5 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs @@ -75,6 +75,12 @@ pub(in crate::context_bootstrap) const WORKER_SHARED_INTERFACE_NAMES: &[&str] = "FileReaderSync", "DOMException", "QuotaExceededError", + "Performance", + "PerformanceObserver", + "PerformanceObserverEntryList", + "PerformanceEntry", + "PerformanceMark", + "PerformanceMeasure", "URL", "URLSearchParams", "FormData", diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs index 3408b5aa97..5b78673030 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs @@ -88,6 +88,10 @@ impl IntrinsicInterfaceRegistry { Ok(registry) } + pub(super) const fn realm_kind(&self) -> RealmKind { + self.realm_kind + } + pub(super) fn state(&self, id: InterfaceId) -> Option { self.states.borrow().get(id.index()).copied() } diff --git a/moli-renderer-v8/src/context_bootstrap/file_api/install.rs b/moli-renderer-v8/src/context_bootstrap/file_api/install.rs index 91cb1f7e44..5f166dcbd6 100644 --- a/moli-renderer-v8/src/context_bootstrap/file_api/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/file_api/install.rs @@ -23,8 +23,6 @@ struct FileApiRuntimeQueuesDeclaration { resize_observer_queue: (), #[webapi(slot = RESIZE_OBSERVER_REGISTRY_SLOT, init = "array")] resize_observer_registry: (), - #[webapi(slot = PERFORMANCE_OBSERVER_QUEUE_SLOT, init = "array")] - performance_observer_queue: (), } pub(in crate::context_bootstrap) fn install_file_api_template_bindings<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/message_ports.rs b/moli-renderer-v8/src/context_bootstrap/message_ports.rs index a7eed2a931..8ac0ca9af6 100644 --- a/moli-renderer-v8/src/context_bootstrap/message_ports.rs +++ b/moli-renderer-v8/src/context_bootstrap/message_ports.rs @@ -18,7 +18,7 @@ pub(crate) use delivery::{ pub(super) use methods::{ message_port_close_callback, message_port_post_message_callback, message_port_start_callback, }; -pub(super) use scheduling::schedule_host_callback; +pub(super) use scheduling::{schedule_host_callback, schedule_scope_callback}; pub(in crate::context_bootstrap) use state::install_message_port_template_bindings; pub(crate) use state::{ MessagePortRealmBinding, detach_message_port_owner_for_transfer, diff --git a/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/delivery.rs b/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/delivery.rs index 6045ef70da..d6ae25f04e 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/delivery.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/delivery.rs @@ -2,8 +2,8 @@ use super::observer::{ array_contains_string, performance_observer_active, performance_observer_callback_id, performance_observer_callback_residence, performance_observer_entry_types, performance_observer_observed_type, performance_observer_pending, - performance_observer_scheduled, set_performance_observer_pending, - set_performance_observer_scheduled, + performance_observer_scheduled, prepare_worker_performance_observer_callback, + set_performance_observer_pending, set_performance_observer_scheduled, }; use super::*; use crate::host::report_event_callback_exception; @@ -37,19 +37,6 @@ fn performance_observer_flush_callback<'s>( if pending.length() == 0 { return; } - let Some(callback_residence) = performance_observer_callback_residence(scope, observer) else { - return; - }; - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - return; - }; - let Some(callback) = - crate::observer_runtime::prepare_callback(scope, host_ptr, callback_residence) - else { - let pending = v8::Array::new(scope, 0); - set_performance_observer_pending(scope, observer, pending); - return; - }; let list = PerformanceObserverEntryListObjectDeclaration::new(pending) .bind(scope) .expect("PerformanceObserverEntryList declaration should bind"); @@ -61,26 +48,61 @@ fn performance_observer_flush_callback<'s>( // droppedEntriesCount when a drop count exists. let options = v8::Object::new(scope); let observer_value: v8::Local<'_, v8::Value> = observer.into(); - match callback.invoke( - scope, - host_ptr, - "PerformanceObserver callback", - observer_value, - &[list.into(), observer_value, options.into()], - ) { - WindowWebIdlCallbackFunctionOutcome::Threw(report) => { - report_event_callback_exception( - scope, - host_ptr, - "performanceobserver", - callback.relevant_identity(), - None, - &report, - ); + let callback_arguments = [list.into(), observer_value, options.into()]; + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + let Some(callback_residence) = performance_observer_callback_residence(scope, observer) + else { + return; + }; + let Some(callback) = + crate::observer_runtime::prepare_callback(scope, host_ptr, callback_residence) + else { + return; + }; + match callback.invoke( + scope, + host_ptr, + "PerformanceObserver callback", + observer_value, + &callback_arguments, + ) { + WindowWebIdlCallbackFunctionOutcome::Threw(report) => { + report_event_callback_exception( + scope, + host_ptr, + "performanceobserver", + callback.relevant_identity(), + None, + &report, + ); + } + WindowWebIdlCallbackFunctionOutcome::Returned + | WindowWebIdlCallbackFunctionOutcome::Retired => {} } - WindowWebIdlCallbackFunctionOutcome::Returned - | WindowWebIdlCallbackFunctionOutcome::Retired => {} + return; } + + if crate::worker::get_worker_state(scope).is_none() { + return; + } + let Some(callback) = prepare_worker_performance_observer_callback(scope, observer) else { + return; + }; + let _ = moli_webidl_callback::invoke_webidl_callback_function( + scope, + &callback, + observer_value, + &callback_arguments, + |scope, callback, receiver, arguments| { + crate::exception_reporting::invoke_callback( + scope, + "PerformanceObserver callback", + callback, + receiver, + arguments, + ) + }, + ); } pub(super) fn enqueue_buffered_performance_entries<'s>( @@ -117,18 +139,30 @@ pub(in crate::context_bootstrap) fn queue_matching_performance_observers<'s>( entry: v8::Local<'s, v8::Object>, entry_type: &str, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let host_ptr = context_host_ptr_from_global_bridge(scope); + let observers = if let Some(host_ptr) = host_ptr { + crate::observer_runtime::active_performance_observer_callbacks(scope, host_ptr) + } else if crate::worker::get_worker_state(scope).is_some() { + let Some(registry) = global_queue_array(scope, PERFORMANCE_OBSERVER_REGISTRY_SLOT) else { + return; + }; + (0..registry.length()) + .filter_map(|index| registry.get_index(scope, index)) + .filter_map(|value| v8::Local::::try_from(value).ok()) + .collect() + } else { return; }; - let observers = crate::observer_runtime::active_performance_observer_callbacks(scope, host_ptr); for observer in observers { - let Some(callback_id) = performance_observer_callback_id(scope, observer) else { - continue; - }; - if !crate::observer_runtime::callback_is_current(host_ptr, callback_id) { - let pending = v8::Array::new(scope, 0); - set_performance_observer_pending(scope, observer, pending); - continue; + if let Some(host_ptr) = host_ptr { + let Some(callback_id) = performance_observer_callback_id(scope, observer) else { + continue; + }; + if !crate::observer_runtime::callback_is_current(host_ptr, callback_id) { + let pending = v8::Array::new(scope, 0); + set_performance_observer_pending(scope, observer, pending); + continue; + } } if !performance_observer_active(scope, observer) { continue; @@ -151,21 +185,21 @@ pub(super) fn queue_performance_observer_delivery<'s>( if performance_observer_scheduled(scope, observer) { return; } - let Some(callback_id) = performance_observer_callback_id(scope, observer) else { - return; - }; - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - return; - }; - if !crate::observer_runtime::callback_is_current(host_ptr, callback_id) { - let pending = v8::Array::new(scope, 0); - set_performance_observer_pending(scope, observer, pending); + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + let Some(callback_id) = performance_observer_callback_id(scope, observer) else { + return; + }; + if !crate::observer_runtime::callback_is_current(host_ptr, callback_id) { + let pending = v8::Array::new(scope, 0); + set_performance_observer_pending(scope, observer, pending); + return; + } + } else if crate::worker::get_worker_state(scope).is_none() { return; } set_performance_observer_scheduled(scope, observer, true); push_object_to_global_queue(scope, PERFORMANCE_OBSERVER_QUEUE_SLOT, observer); - let host = unsafe { &mut *host_ptr }; - schedule_host_callback(scope, host, performance_observer_flush_callback); + schedule_scope_callback(scope, performance_observer_flush_callback, None); } fn performance_observer_matches_entry_type<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/observer.rs b/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/observer.rs index b20d886546..edd1003c04 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/observer.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_observer_runtime/observer.rs @@ -1,4 +1,3 @@ -use super::super::performance_runtime::PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES; use super::delivery::{enqueue_buffered_performance_entries, queue_performance_observer_delivery}; use super::*; use crate::observer_runtime::ObserverCallbackId; @@ -77,14 +76,40 @@ pub(in crate::context_bootstrap) fn performance_observer_constructor_callback<'s else { return; }; - let host_ptr = context_host_ptr_from_global_bridge(scope) - .expect("PerformanceObserver constructor must execute in a Window realm"); - let registered_callback = - crate::observer_runtime::register_callback(scope, host_ptr, args.this(), parsed.callback); let (callback_id, callback, relevant_global, incumbent_global) = - registered_callback.into_parts(); + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + let registered_callback = crate::observer_runtime::register_callback( + scope, + host_ptr, + args.this(), + parsed.callback, + ); + let (callback_id, callback, relevant_global, incumbent_global) = + registered_callback.into_parts(); + ( + callback_id.as_u32(), + callback, + relevant_global, + incumbent_global, + ) + } else { + if crate::worker::get_worker_state(scope).is_none() { + throw_type_error( + scope, + "PerformanceObserver is unavailable outside a Window or Worker realm.", + ); + return; + } + let callback = v8::Local::::try_from(parsed.callback.value(scope)) + .expect("a Web IDL callback function must be an object"); + let relevant_global = parsed.callback.relevant_context(scope).global(scope); + let incumbent_global = parsed.callback.incumbent_context(scope).global(scope); + // Worker observers do not need a Window execution-context binding. + // Zero is deliberately outside ObserverCallbackId's valid range. + (0, callback, relevant_global, incumbent_global) + }; PerformanceObserverObjectDeclaration::new( - callback_id.as_u32(), + callback_id, callback, relevant_global, incumbent_global, @@ -149,7 +174,7 @@ pub(in crate::context_bootstrap) fn performance_observer_observe_callback<'s>( } let observed_entry_types = if let Some(observed_type) = init.observed_type.as_deref() { - if !performance_observer_entry_type_supported(observed_type) { + if !performance_observer_entry_type_supported(scope, observed_type) { rv.set_undefined(); return; } @@ -163,13 +188,13 @@ pub(in crate::context_bootstrap) fn performance_observer_observe_callback<'s>( } entry_types } else { - performance_entry_types_array_from_strings( - scope, - init.entry_types - .unwrap_or_default() - .into_iter() - .filter(|entry_type| performance_observer_entry_type_supported(entry_type)), - ) + let supported_entry_types = init + .entry_types + .unwrap_or_default() + .into_iter() + .filter(|entry_type| performance_observer_entry_type_supported(scope, entry_type)) + .collect::>(); + performance_entry_types_array_from_strings(scope, supported_entry_types) }; if observed_entry_types.length() == 0 { rv.set_undefined(); @@ -198,27 +223,33 @@ pub(in crate::context_bootstrap) fn performance_observer_observe_callback<'s>( .unwrap_or_else(|| v8::null(scope).into()), ); set_performance_observer_entry_types(scope, args.this(), observed_entry_types); - let Some(callback_id) = performance_observer_callback_id(scope, args.this()) else { - rv.set_undefined(); - return; - }; - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_undefined(); - return; - }; - // Blink keeps a PerformanceObserver alive only while it is registered - // (`HasPendingActivity() == is_registered_`). The exact callback binding - // owns the same active root so disconnect and Realm retirement can release - // it without a parallel global JS registry. - if !crate::observer_runtime::activate_performance_observer_callback( - scope, - host_ptr, - callback_id, - args.this(), - ) { - set_performance_observer_active(scope, args.this(), false); - let pending = v8::Array::new(scope, 0); - set_performance_observer_pending(scope, args.this(), pending); + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + let Some(callback_id) = performance_observer_callback_id(scope, args.this()) else { + rv.set_undefined(); + return; + }; + // Blink keeps a PerformanceObserver alive only while it is registered + // (`HasPendingActivity() == is_registered_`). The exact callback binding + // owns the same active root so disconnect and Realm retirement can release + // it without a parallel global JS registry. + if !crate::observer_runtime::activate_performance_observer_callback( + scope, + host_ptr, + callback_id, + args.this(), + ) { + set_performance_observer_active(scope, args.this(), false); + let pending = v8::Array::new(scope, 0); + set_performance_observer_pending(scope, args.this(), pending); + rv.set_undefined(); + return; + } + } else if crate::worker::get_worker_state(scope).is_some() { + // A worker has one Realm for the lifetime of its isolate run, so its + // active observer set can live on that Realm without Window generation + // authorization. + push_object_to_global_registry(scope, PERFORMANCE_OBSERVER_REGISTRY_SLOT, args.this()); + } else { rv.set_undefined(); return; } @@ -295,8 +326,18 @@ fn performance_observer_type_slot_is_set<'s>( .is_some_and(|value| !value.is_null_or_undefined()) } -fn performance_observer_entry_type_supported(entry_type: &str) -> bool { - PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES.contains(&entry_type) +fn performance_observer_entry_type_supported( + scope: &mut v8::PinScope<'_, '_>, + entry_type: &str, +) -> bool { + let global = scope.get_current_context().global(scope); + get_private_value( + scope, + global, + PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok()) + .is_some_and(|entry_types| array_contains_string(scope, entry_types, entry_type)) } pub(super) fn array_contains_string( @@ -351,6 +392,35 @@ pub(super) fn performance_observer_callback_residence<'s>( ) } +pub(super) fn prepare_worker_performance_observer_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + observer: v8::Local<'s, v8::Object>, +) -> Option { + let callback = + performance_observer_slot_value(scope, observer, PERFORMANCE_OBSERVER_CALLBACK_VALUE_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let relevant_global = performance_observer_slot_value( + scope, + observer, + PERFORMANCE_OBSERVER_CALLBACK_RELEVANT_GLOBAL_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let incumbent_global = performance_observer_slot_value( + scope, + observer, + PERFORMANCE_OBSERVER_CALLBACK_INCUMBENT_GLOBAL_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let relevant_context = relevant_global.get_creation_context(scope)?; + let incumbent_context = incumbent_global.get_creation_context(scope)?; + moli_webidl_callback::PreparedWebIdlCallbackFunction::try_new( + scope, + callback, + relevant_context, + incumbent_context, + ) +} + pub(super) fn performance_observer_pending<'s>( scope: &mut v8::PinScope<'s, '_>, observer: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs index 458c01a690..1fb254870b 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs @@ -86,8 +86,10 @@ pub(super) const PERFORMANCE_LIFECYCLE_TIMESTAMPS_SLOT: &str = "__moliPerformanceLifecycleTimestamps"; pub(super) const PERFORMANCE_PENDING_EVENT_COUNTS_SLOT: &str = "__moliPerformancePendingEventCounts"; -pub(in crate::context_bootstrap) const PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES: &[&str] = - &["mark", "measure", "navigation", "resource"]; +pub(in crate::context_bootstrap) const WINDOW_PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES: + &[&str] = &["mark", "measure", "navigation", "resource"]; +pub(in crate::context_bootstrap) const WORKER_PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES: + &[&str] = &["mark", "measure", "resource"]; pub(super) use super::performance_observer_runtime::{ performance_entry_list_get_entries_by_name_callback, @@ -108,9 +110,13 @@ pub(in crate::context_bootstrap) use entries::{ performance_entry_slot_number, performance_entry_slot_string, performance_entry_slot_value, set_performance_entry_slot_number, }; -pub(crate) use install::finalize_performance_observer_realm_bindings; pub(crate) use install::increment_performance_event_count; -pub(super) use install::install_performance_template_bindings; +pub(crate) use install::{ + finalize_performance_observer_realm_bindings, finalize_window_performance_realm_bindings, +}; +pub(super) use install::{ + install_performance_template_bindings, install_worker_performance_runtime_state, +}; pub(crate) use install::{ record_performance_dom_content_loaded_event_end, record_performance_dom_content_loaded_event_start, record_performance_load_event_end, @@ -176,7 +182,7 @@ pub(in crate::context_bootstrap) fn current_window_navigation_timing_snapshot( pub(in crate::context_bootstrap) fn ensure_navigation_performance_entry_for_api<'s>( scope: &mut v8::PinScope<'s, '_>, performance: v8::Local<'s, v8::Object>, -) -> v8::Local<'s, v8::Object> { +) { install::ensure_navigation_performance_entry(scope, performance) } @@ -231,6 +237,7 @@ fn record_performance_load_event_for_window( let scope = &mut v8::ContextScope::new(scope, relevant_context); record(scope); } + pub(crate) struct ResourcePerformanceEntry { name: String, initiator_type: String, diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs index d977a5d281..6acc19b1e6 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs @@ -10,6 +10,7 @@ use crate::webidl_iterator::{ SnapshotWebIdlIteratorKind, invoke_webidl_collection_for_each_callback, new_snapshot_webidl_iterator, prepare_webidl_collection_for_each_callback, }; +use anyhow::{Result, anyhow}; use moli_webapi_declare::{ObjectLiteralDeclaration, WebApiFunctionTemplate, WebApiObject}; pub(super) const MIN_LIFECYCLE_TIMING_DELTA_MILLIS: f64 = 0.001; @@ -77,6 +78,16 @@ struct PerformanceObjectDeclaration<'scope> { event_counts: v8::Local<'scope, v8::Value>, } +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::Performance)] +struct WorkerPerformanceObjectDeclaration { + #[webapi(slot = PERFORMANCE_TIME_ORIGIN_SLOT)] + time_origin: f64, + + #[webapi(slot = PERFORMANCE_ENTRIES_SLOT, init = "array")] + entries: (), +} + #[derive(WebApiObject)] #[webapi(plain, data_properties, enumerable)] struct PerformanceJsonSnapshotDeclaration<'scope> { @@ -92,6 +103,16 @@ struct PerformanceObserverConstructorDeclaration { supported_entry_types: &'static [&'static str], } +#[derive(Default, WebApiObject)] +#[webapi(fragment)] +struct PerformanceObserverRuntimeStateDeclaration { + #[webapi(slot = PERFORMANCE_OBSERVER_REGISTRY_SLOT, init = "array")] + registry: (), + + #[webapi(slot = PERFORMANCE_OBSERVER_QUEUE_SLOT, init = "array")] + queue: (), +} + #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::PerformanceNavigation)] struct PerformanceNavigationObjectDeclaration { @@ -161,16 +182,20 @@ struct PerformancePrototypeMethodsDeclaration { #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::Performance, enumerable)] -struct PerformancePrototypeAccessorsDeclaration { - #[webapi(accessor_property, getter = super::memory::performance_memory_getter)] - memory: (), - +struct PerformancePrototypeCommonAccessorsDeclaration { #[webapi( accessor_property, getter = performance_attribute_getter_callback, data = callback_data_index_value(scope, 0) )] time_origin: (), +} + +#[derive(Default, WebApiObject)] +#[webapi(fragment, prototype = "Performance", enumerable)] +struct PerformancePrototypeWindowAccessorsDeclaration { + #[webapi(accessor_property, getter = super::memory::performance_memory_getter)] + memory: (), #[webapi( accessor_property, @@ -299,7 +324,7 @@ pub(in crate::context_bootstrap) fn install_performance_template_bindings<'s>( match interface_name { "Performance" => { PerformancePrototypeMethodsDeclaration::initialize_prototype_template(scope, prototype); - PerformancePrototypeAccessorsDeclaration::initialize_prototype_template( + PerformancePrototypeCommonAccessorsDeclaration::initialize_prototype_template( scope, prototype, ); super::resource_buffer::install_resource_timing_buffer_template_bindings( @@ -327,6 +352,46 @@ pub(in crate::context_bootstrap) fn install_performance_template_bindings<'s>( } } +pub(in crate::context_bootstrap) fn install_worker_performance_runtime_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + global: v8::Local<'s, v8::Object>, +) -> Result<()> { + install_performance_observer_runtime_state( + scope, + global, + WORKER_PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES, + )?; + super::super::exposed_interfaces::ensure_intrinsic_interface_constructor(scope, "Performance")?; + + let performance = WorkerPerformanceObjectDeclaration::new(unix_epoch_millis()) + .bind(scope) + .map_err(|error| anyhow!("failed to create worker performance: {error}"))?; + install_simple_event_target_methods( + scope, + performance, + PERFORMANCE_EVENT_LISTENERS_SLOT, + false, + ); + define_global_value(scope, global, WINDOW_PERFORMANCE_SLOT, performance.into()) +} + +pub(super) fn install_performance_observer_runtime_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + global: v8::Local<'s, v8::Object>, + supported_entry_types: &'static [&'static str], +) -> Result<()> { + PerformanceObserverRuntimeStateDeclaration::default().initialize(scope, global)?; + let supported_entry_types_array = serialize_v8_array(scope, supported_entry_types) + .unwrap_or_else(|| v8::Array::new(scope, 0)); + set_private_value( + scope, + global, + PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES_SLOT, + supported_entry_types_array.into(), + ); + Ok(()) +} + pub(super) fn create_performance_object<'s>( scope: &mut v8::PinScope<'s, '_>, window: Option>, @@ -439,12 +504,20 @@ pub(super) fn apply_pending_window_performance_state<'s>( pub(crate) fn finalize_performance_observer_realm_bindings<'s>( scope: &mut v8::PinScope<'s, '_>, constructor: v8::Local<'s, v8::Object>, + supported_entry_types: &'static [&'static str], ) { - PerformanceObserverConstructorDeclaration::new(PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES) + PerformanceObserverConstructorDeclaration::new(supported_entry_types) .initialize(scope, constructor) .expect("PerformanceObserver constructor declaration should initialize"); } +pub(crate) fn finalize_window_performance_realm_bindings<'s>( + scope: &mut v8::PinScope<'s, '_>, + prototype: v8::Local<'s, v8::Object>, +) -> Result<()> { + PerformancePrototypeWindowAccessorsDeclaration::default().initialize(scope, prototype)?; + Ok(()) +} fn performance_navigation_timing_type(navigation_type: &str) -> &'static str { match navigation_type { "reload" => "reload", @@ -813,12 +886,21 @@ fn navigation_performance_entry<'s>( .and_then(|value| v8::Local::::try_from(value).ok()) } +pub(super) fn is_window_performance<'s>( + scope: &mut v8::PinScope<'s, '_>, + performance: v8::Local<'s, v8::Object>, +) -> bool { + get_private_value(scope, performance, PERFORMANCE_NAVIGATION_TYPE_SEED_SLOT).is_some() +} + pub(super) fn ensure_navigation_performance_entry<'s>( scope: &mut v8::PinScope<'s, '_>, performance: v8::Local<'s, v8::Object>, -) -> v8::Local<'s, v8::Object> { - if let Some(entry) = navigation_performance_entry(scope, performance) { - return entry; +) { + if navigation_performance_entry(scope, performance).is_some() + || !is_window_performance(scope, performance) + { + return; } let navigation_type = performance_navigation_type_seed(scope, performance); let name = get_private_value(scope, performance, PERFORMANCE_NAVIGATION_NAME_SEED_SLOT) @@ -834,7 +916,6 @@ pub(super) fn ensure_navigation_performance_entry<'s>( ); apply_lifecycle_to_navigation_entry(scope, performance, entry); append_performance_entry(scope, performance, entry); - entry } fn apply_lifecycle_to_navigation_entry<'s>( @@ -1056,11 +1137,11 @@ fn performance_attribute_getter_callback<'s>( rv.set_undefined(); return; }; + if performance_slot_number(scope, args.this(), PERFORMANCE_TIME_ORIGIN_SLOT).is_none() { + throw_type_error(scope, "Illegal invocation"); + return; + } if let Some(subobject) = super::lazy_subobjects::PerformanceSubobject::from_slot(slot) { - if performance_slot_value(scope, args.this(), PERFORMANCE_TIME_ORIGIN_SLOT).is_none() { - rv.set_undefined(); - return; - } match super::lazy_subobjects::ensure_performance_subobject(scope, args.this(), subobject) { Ok(value) => rv.set(value), Err(error) => throw_type_error(scope, &error.to_string()), diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime/window_state.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime/window_state.rs index 2e2dbab9b7..fc9c1ccce5 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime/window_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime/window_state.rs @@ -1,4 +1,7 @@ -use super::install::create_performance_object; +use super::{ + WINDOW_PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES, + install::{create_performance_object, install_performance_observer_runtime_state}, +}; use crate::{ context_bootstrap::{ WindowLazySurface, ensure_window_lazy_surface_object, @@ -85,6 +88,11 @@ pub(in crate::context_bootstrap) fn install_default_window_performance_seed<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, ) -> Result<()> { + install_performance_observer_runtime_state( + scope, + window, + WINDOW_PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES, + )?; bind_window_performance_seed(scope, window, "navigate", unix_epoch_millis()) } diff --git a/moli-renderer-v8/src/context_bootstrap/shared/slots.rs b/moli-renderer-v8/src/context_bootstrap/shared/slots.rs index 3478314a36..1b89373602 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared/slots.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared/slots.rs @@ -126,8 +126,12 @@ pub(in crate::context_bootstrap) const PERFORMANCE_OBSERVER_ACTIVE_SLOT: &str = "__moliPerformanceObserverActive"; pub(in crate::context_bootstrap) const PERFORMANCE_OBSERVER_SCHEDULED_SLOT: &str = "__moliPerformanceObserverScheduled"; +pub(in crate::context_bootstrap) const PERFORMANCE_OBSERVER_REGISTRY_SLOT: &str = + "__moliPerformanceObserverRegistry"; pub(in crate::context_bootstrap) const PERFORMANCE_OBSERVER_QUEUE_SLOT: &str = "__moliPerformanceObserverQueue"; +pub(in crate::context_bootstrap) const PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES_SLOT: &str = + "__moliPerformanceObserverSupportedEntryTypes"; pub(in crate::context_bootstrap) const PERFORMANCE_ENTRY_LIST_ENTRIES_SLOT: &str = "__moliPerformanceEntryListEntries"; pub(in crate::context_bootstrap) const PERFORMANCE_ENTRIES_SLOT: &str = "__moliPerformanceEntries"; diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/performance.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/performance.rs index 6b37a0a974..edab152d0c 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/performance.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/performance.rs @@ -1,6 +1,7 @@ use crate::context_bootstrap::{ PERFORMANCE_TIME_ORIGIN_SLOT, dom_time_since_origin_millis, performance_slot_number, }; +use crate::util::throw_type_error; use moli_webapi_declare::v8; pub(in crate::context_bootstrap) fn performance_now_callback<'s>( @@ -8,8 +9,12 @@ pub(in crate::context_bootstrap) fn performance_now_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let time_origin = - performance_slot_number(scope, args.this(), PERFORMANCE_TIME_ORIGIN_SLOT).unwrap_or(0.0); + let Some(time_origin) = + performance_slot_number(scope, args.this(), PERFORMANCE_TIME_ORIGIN_SLOT) + else { + throw_type_error(scope, "Illegal invocation"); + return; + }; let now = dom_time_since_origin_millis(time_origin); rv.set(v8::Number::new(scope, now).into()); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index 28ca0cebe3..2dec739864 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -815,154 +815,6 @@ fn performance_mark_constructor_creates_detached_structured_entries() { ); } -#[test] -fn performance_user_timing_enforces_mark_and_measure_boundaries() { - let mut vm = new_storage_test_vm("https://performance-user-timing-boundaries.test/"); - - let result = vm - .eval( - r#" - (() => { - const capture = callback => { - try { - callback(); - return "none"; - } catch (error) { - return `${error.name}:${error.code}`; - } - }; - performance.mark("later", { startTime: 5 }); - const sourceDetail = { value: 1 }; - const cloned = performance.measure("cloned", { - start: 1, - duration: 2, - detail: sourceDetail - }); - sourceDetail.value = 9; - const negative = performance.measure( - "negative-duration", - "later", - "navigationStart" - ); - const legacyPendingNames = [ - "unloadEventStart", - "unloadEventEnd", - "redirectStart", - "redirectEnd", - "secureConnectionStart", - "domInteractive", - "domContentLoadedEventStart", - "domContentLoadedEventEnd", - "domComplete", - "loadEventStart", - "loadEventEnd" - ]; - return JSON.stringify({ - reservedMark: capture(() => performance.mark("navigationStart")), - missingMark: capture(() => performance.measure("missing", "does-not-exist")), - numericLegacyMark: capture(() => performance.measure("number", 51.15, "later")), - unavailableTiming: capture(() => performance.measure("pending", "redirectStart")), - detailOnlyOptions: capture(() => performance.measure("detail-only", { detail: 1 })), - overSpecifiedOptions: capture(() => performance.measure("all", { - start: 1, - duration: 2, - end: 3 - })), - negativeMark: capture(() => performance.mark("bad-mark", { startTime: -1 })), - infiniteMark: capture(() => performance.mark("bad-infinite", { startTime: Infinity })), - negativeBoundary: capture(() => performance.measure("bad-boundary", { start: -1 })), - negativeDuration: negative.duration, - navigationStartTime: performance.measure("from-navigation", "navigationStart").startTime, - pendingTimingStartsAtZero: legacyPendingNames.every(name => performance.timing[name] === 0), - clonedDetail: `${cloned.detail.value}:${cloned.detail === sourceDetail}`, - navigationEntryIsNotMark: capture(() => performance.measure("entry-name", location.href)) - }); - })() - "#, - ) - .expect("User Timing boundary probe should evaluate"); - - assert_eq!( - result, - r#"{"reservedMark":"SyntaxError:12","missingMark":"SyntaxError:12","numericLegacyMark":"SyntaxError:12","unavailableTiming":"InvalidAccessError:15","detailOnlyOptions":"TypeError:undefined","overSpecifiedOptions":"TypeError:undefined","negativeMark":"TypeError:undefined","infiniteMark":"TypeError:undefined","negativeBoundary":"TypeError:undefined","negativeDuration":-5,"navigationStartTime":0,"pendingTimingStartsAtZero":true,"clonedDetail":"1:false","navigationEntryIsNotMark":"SyntaxError:12"}"# - ); -} - -#[test] -fn performance_mark_constructor_creates_detached_structured_entries() { - let mut vm = new_storage_test_vm("https://performance-mark-constructor.test/"); - - let result = vm - .eval( - r#" - (() => { - const capture = callback => { - try { - callback(); - return "none"; - } catch (error) { - return `${error.name}:${error.code}`; - } - }; - const sourceDetail = { state: "before" }; - const entry = new PerformanceMark("detached", { - startTime: 2, - detail: sourceDetail - }); - sourceDetail.state = "after"; - class DerivedPerformanceMark extends PerformanceMark {} - const derived = new DerivedPerformanceMark("derived", { startTime: 3 }); - return JSON.stringify({ - shape: [ - entry instanceof PerformanceEntry, - entry instanceof PerformanceMark, - Object.prototype.toString.call(entry), - entry.name, - entry.entryType, - entry.startTime, - entry.duration - ].join(":"), - detail: `${entry.detail.state}:${entry.detail === entry.detail}:${entry.detail === sourceDetail}`, - timelineEntries: performance.getEntriesByName("detached", "mark").length, - derived: [ - derived instanceof DerivedPerformanceMark, - derived instanceof PerformanceMark, - Object.getPrototypeOf(derived) === DerivedPerformanceMark.prototype, - derived.name, - derived.startTime - ].join(":"), - constructorInheritance: - Object.getPrototypeOf(PerformanceMark) === PerformanceEntry - && Object.getPrototypeOf(PerformanceMeasure) === PerformanceEntry, - detailBrand: capture(() => - Object.getOwnPropertyDescriptor(PerformanceMark.prototype, "detail") - .get.call(PerformanceMark.prototype)), - methodBrands: [ - performance.mark, - performance.clearMarks, - performance.measure, - performance.clearMeasures - ].map(method => capture(() => method.call(null, "unbound"))).join("|"), - withoutNew: capture(() => PerformanceMark("call")), - missingName: capture(() => new PerformanceMark()), - negativeStart: capture(() => new PerformanceMark("negative", { startTime: -1 })), - infiniteStart: capture(() => new PerformanceMark("infinite", { startTime: Infinity })), - reservedName: capture(() => new PerformanceMark("navigationStart")), - cloneError: capture(() => new PerformanceMark("clone", { - detail: { value: Symbol() } - })) - }); - })() - "#, - ) - .expect("PerformanceMark constructor probe should evaluate"); - - assert_eq!( - result, - r#"{"shape":"true:true:[object PerformanceMark]:detached:mark:2:0","detail":"before:true:false","timelineEntries":0,"derived":"true:true:true:derived:3","constructorInheritance":true,"detailBrand":"TypeError:undefined","methodBrands":"TypeError:undefined|TypeError:undefined|TypeError:undefined|TypeError:undefined","withoutNew":"TypeError:undefined","missingName":"TypeError:undefined","negativeStart":"TypeError:undefined","infiniteStart":"TypeError:undefined","reservedName":"SyntaxError:12","cloneError":"DataCloneError:25"}"# - ); -} - #[test] fn performance_entry_to_json_returns_native_base_snapshot() { let mut vm = new_storage_test_vm("https://performance-entry-json.test/"); @@ -1146,6 +998,13 @@ fn performance_root_slots_ignore_reflection_and_spoofing() { && descriptor.configurable === true && !Object.prototype.hasOwnProperty.call(receiver, name); }; + const capture = callback => { + try { + return String(callback()); + } catch (error) { + return error.name; + } + }; const timing = performance.timing; const navigation = performance.navigation; const eventCounts = performance.eventCounts; @@ -1209,8 +1068,8 @@ fn performance_root_slots_ignore_reflection_and_spoofing() { eventCountsFirstEntry: `${firstEntry[0]}:${firstEntry[1]}`, jsonTimeOriginStable: json.timeOrigin === timeOrigin, jsonNavigationType: json.navigation.type, - fakeTimeOrigin: String(timeOriginGetter.call(fakePerformance)), - fakeTiming: String(timingGetter.call(fakePerformance)), + fakeTimeOrigin: capture(() => timeOriginGetter.call(fakePerformance)), + fakeTiming: capture(() => timingGetter.call(fakePerformance)), fakeNavigationType: String(navigationTypeGetter.call(fakeNavigation)), fakeEventCountsGet: String(eventCountsPrototype.get.call(fakeEventCounts, "click")), fakeEventCountsValue: String(eventCountsPrototype.values.call(fakeEventCounts).next().value) @@ -1222,7 +1081,7 @@ fn performance_root_slots_ignore_reflection_and_spoofing() { assert_eq!( result, - r#"{"initialPerformanceNames":[],"initialNavigationNames":[],"initialEventCountsNames":[],"timeOriginSpoofIgnored":true,"timingStable":true,"navigationStable":true,"eventCountsStable":true,"performanceDescriptorsStable":true,"entriesSpoofIgnored":1,"navigationType":0,"navigationRedirectCount":0,"navigationDescriptorsStable":true,"eventCountsClick":0,"eventCountsFirstValue":0,"eventCountsFirstEntry":"auxclick:0","jsonTimeOriginStable":true,"jsonNavigationType":0,"fakeTimeOrigin":"undefined","fakeTiming":"undefined","fakeNavigationType":"undefined","fakeEventCountsGet":"0","fakeEventCountsValue":"0"}"# + r#"{"initialPerformanceNames":[],"initialNavigationNames":[],"initialEventCountsNames":[],"timeOriginSpoofIgnored":true,"timingStable":true,"navigationStable":true,"eventCountsStable":true,"performanceDescriptorsStable":true,"entriesSpoofIgnored":1,"navigationType":0,"navigationRedirectCount":0,"navigationDescriptorsStable":true,"eventCountsClick":0,"eventCountsFirstValue":0,"eventCountsFirstEntry":"auxclick:0","jsonTimeOriginStable":true,"jsonNavigationType":0,"fakeTimeOrigin":"TypeError","fakeTiming":"TypeError","fakeNavigationType":"undefined","fakeEventCountsGet":"0","fakeEventCountsValue":"0"}"# ); } diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index b0d2e6bb16..0ce40584bc 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -228,13 +228,6 @@ struct WorkerGlobalConsoleDeclaration<'scope> { console: v8::Local<'scope, v8::Object>, } -#[derive(WebApiObject)] -#[webapi(plain)] -struct WorkerGlobalPerformanceDeclaration<'scope> { - #[webapi(data_property)] - performance: v8::Local<'scope, v8::Object>, -} - #[derive(Default, WebApiObject)] #[webapi(plain)] struct WorkerConsoleObjectDeclaration { @@ -282,15 +275,6 @@ struct WorkerConsoleObjectDeclaration { profile_end: (), } -#[derive(WebApiObject)] -#[webapi(prototype = "Object", interface = web_api_interfaces::Performance)] -struct WorkerPerformanceObjectDeclaration { - #[webapi(data_property, readonly)] - time_origin: f64, - #[webapi(method, callback = worker_performance_now_callback, data = self.time_origin)] - now: (), -} - #[derive(Default, WebApiObject)] #[webapi(plain, enumerable)] struct WorkerGlobalCommonEventHandlersDeclaration { @@ -3003,7 +2987,6 @@ pub(super) fn install_worker_global_scope<'s>( ) .initialize(scope, global) .map_err(|error| anyhow!("failed to initialize worker global bootstrap properties: {error}"))?; - install_worker_performance(scope, global)?; install_worker_global_scope_constructors(scope, global, &global_kind, worker_templates)?; let (_, realm_kind) = worker_global_scope_interface(&global_kind); crate::context_bootstrap::install_worker_lazy_exposed_interfaces( @@ -3028,6 +3011,7 @@ pub(super) fn install_worker_global_scope<'s>( install_service_worker_extendable_event_constructors(scope, global)?; } crate::context_bootstrap::initialize_worker_fetch_realm_state(scope, global)?; + crate::context_bootstrap::initialize_worker_performance_realm_state(scope, global)?; let subtle_crypto_available = secure_context; crate::context_bootstrap::initialize_worker_crypto_realm_state( scope, @@ -6636,36 +6620,6 @@ fn install_console<'s>( .map_err(|error| anyhow!("failed to initialize worker console: {error}")) } -fn install_worker_performance<'s>( - scope: &mut v8::PinScope<'s, '_>, - global: v8::Local<'s, v8::Object>, -) -> Result<()> { - let time_origin = monotonic_unix_epoch_millis(); - let performance = WorkerPerformanceObjectDeclaration::new(time_origin) - .bind(scope) - .map_err(|error| anyhow!("failed to create worker performance: {error}"))?; - WorkerGlobalPerformanceDeclaration::new(performance) - .initialize(scope, global) - .map_err(|error| anyhow!("failed to initialize worker performance: {error}")) -} - -fn worker_performance_now_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let time_origin = args.data().number_value(scope).unwrap_or(0.0); - rv.set( - v8::Number::new( - scope, - moli_time::coarsened_dom_time_millis( - (monotonic_unix_epoch_millis() - time_origin).max(0.0), - ), - ) - .into(), - ); -} - fn unix_epoch_millis() -> f64 { SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs b/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs index 8e083e7323..b695c55f14 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs @@ -296,9 +296,10 @@ async fn worker_storage_surfaces_materialize_in_independent_stages() { ); let diagnostics = lazy_diagnostics(&handle).await; assert_eq!(diagnostics.storage_constructor_materializations, 0); - assert!( - diagnostics.materialized_interfaces.is_empty(), - "blank worker bootstrap must not materialize Navigator or Crypto constructors" + assert_eq!( + diagnostics.materialized_interfaces, + vec![("Performance", 1), ("EventTarget", 1)], + "only constructors required by the eagerly exposed performance object may materialize during worker bootstrap" ); assert!(!diagnostics.storage_manager_materialized); assert!(!diagnostics.storage_bucket_manager_materialized); diff --git a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs index da05687198..8863d02590 100644 --- a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs @@ -2016,18 +2016,43 @@ async fn worker_performance_now_uses_readonly_monotonic_time_origin() { ensure_v8(); let mut handle = spawn_worker( r#" - const descriptor = Object.getOwnPropertyDescriptor(performance, "timeOrigin"); - const before = performance.timeOrigin; - try { performance.timeOrigin = before + 1000000; } catch (_) {} - const after = performance.timeOrigin; - const first = performance.now(); - const second = performance.now(); + const original = performance; + const globalDescriptor = Object.getOwnPropertyDescriptor( + WorkerGlobalScope.prototype, + "performance" + ); + const timeOriginDescriptor = Object.getOwnPropertyDescriptor( + Performance.prototype, + "timeOrigin" + ); + const before = original.timeOrigin; + try { original.timeOrigin = before + 1000000; } catch (_) {} + const after = original.timeOrigin; + const first = original.now(); + const second = original.now(); + const replacement = {}; + self.performance = replacement; postMessage({ - readonly: descriptor && descriptor.writable === false, + interfaceShape: + original instanceof Performance + && original instanceof EventTarget + && Object.getPrototypeOf(original) === Performance.prototype, + globalAccessor: + typeof globalDescriptor?.get === "function" + && typeof globalDescriptor?.set === "function" + && globalDescriptor.enumerable === true, + timeOriginAccessor: + typeof timeOriginDescriptor?.get === "function" + && timeOriginDescriptor?.set === undefined + && timeOriginDescriptor.enumerable === true, unchanged: after === before, numeric: typeof first === "number" && typeof second === "number", monotonic: second >= first, - noLegacyMemory: !("memory" in performance) && !("MemoryInfo" in self) + replaceable: + performance === replacement + && Object.prototype.hasOwnProperty.call(self, "performance"), + originalStillWorks: typeof original.now() === "number", + noLegacyMemory: !("memory" in original) && !("MemoryInfo" in self) }); close(); "# @@ -2041,7 +2066,180 @@ async fn worker_performance_now_uses_readonly_monotonic_time_origin() { .expect("channel closed"); assert_eq!( expect_post_json(msg), - r#"{"readonly":true,"unchanged":true,"numeric":true,"monotonic":true,"noLegacyMemory":true}"# + r#"{"interfaceShape":true,"globalAccessor":true,"timeOriginAccessor":true,"unchanged":true,"numeric":true,"monotonic":true,"replaceable":true,"originalStillWorks":true,"noLegacyMemory":true}"# + ); +} + +#[tokio::test] +async fn worker_user_timing_uses_shared_mark_measure_runtime() { + ensure_v8(); + let mut handle = spawn_worker( + r#" + const capture = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error.name; + } + }; + const timingNames = [ + "navigationStart", + "unloadEventStart", + "unloadEventEnd", + "redirectStart", + "redirectEnd", + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "secureConnectionStart", + "requestStart", + "responseStart", + "responseEnd", + "domLoading", + "domInteractive", + "domContentLoadedEventStart", + "domContentLoadedEventEnd", + "domComplete", + "loadEventStart", + "loadEventEnd" + ]; + const timingNamesAccepted = timingNames.every(name => { + const mark = performance.mark(name, { startTime: 1 }); + performance.clearMarks(name); + const measure = performance.measure(name); + performance.clearMeasures(name); + return mark.name === name && measure.name === name; + }); + + const sourceDetail = { state: "before" }; + const mark = performance.mark("worker-mark", { + startTime: 2, + detail: sourceDetail + }); + sourceDetail.state = "after"; + const measure = performance.measure("worker-measure", { + start: "worker-mark", + end: 5, + detail: { kind: "measure" } + }); + const detached = new PerformanceMark("detached", { + startTime: 3, + detail: { kind: "detached" } + }); + const reservedBoundary = capture(() => { + performance.mark("navigationStart", { startTime: 4 }); + performance.measure("reserved", "navigationStart", "navigationStart"); + }); + + postMessage({ + timingNamesAccepted, + interfaces: + performance instanceof Performance + && mark instanceof PerformanceMark + && mark instanceof PerformanceEntry + && measure instanceof PerformanceMeasure + && measure instanceof PerformanceEntry + && detached instanceof PerformanceMark, + inheritance: + Object.getPrototypeOf(Performance) === EventTarget + && Object.getPrototypeOf(PerformanceMark) === PerformanceEntry + && Object.getPrototypeOf(PerformanceMeasure) === PerformanceEntry, + mark: [ + mark.name, + mark.entryType, + mark.startTime, + mark.duration, + mark.detail.state, + mark.detail === sourceDetail + ].join(":"), + measure: [ + measure.name, + measure.entryType, + measure.startTime, + measure.duration, + measure.detail.kind + ].join(":"), + detachedTimelineCount: + performance.getEntriesByName("detached", "mark").length, + bufferedTypes: performance.getEntries() + .map(entry => entry.entryType) + .join(","), + reservedBoundary, + missingBoundary: capture(() => + performance.measure("missing", "not-a-mark")), + markBrand: capture(() => performance.mark.call({}, "bad")), + nowBrand: capture(() => performance.now.call({})) + }); + close(); + "# + .into(), + "test://worker_user_timing".into(), + ); + + let msg = timeout(TIMEOUT, handle.recv()) + .await + .expect("timed out") + .expect("channel closed"); + assert_eq!( + expect_post_json(msg), + r#"{"timingNamesAccepted":true,"interfaces":true,"inheritance":true,"mark":"worker-mark:mark:2:0:before:false","measure":"worker-measure:measure:2:3:measure","detachedTimelineCount":0,"bufferedTypes":"mark,measure,mark","reservedBoundary":"TypeError","missingBoundary":"SyntaxError","markBrand":"TypeError","nowBrand":"TypeError"}"# + ); +} + +#[tokio::test] +async fn worker_performance_observer_delivers_live_and_buffered_user_timing_entries() { + ensure_v8(); + let mut handle = spawn_worker( + r#" + const firstObserver = new PerformanceObserver((list, observer) => { + const liveEntries = list.getEntries(); + observer.disconnect(); + const bufferedObserver = new PerformanceObserver(bufferedList => { + const bufferedEntries = bufferedList.getEntries(); + postMessage({ + interfaces: + firstObserver instanceof PerformanceObserver + && list instanceof PerformanceObserverEntryList, + prototypeMethods: + typeof list.getEntries === "function" + && typeof list.getEntriesByType === "function" + && typeof list.getEntriesByName === "function", + supportedEntryTypes: PerformanceObserver.supportedEntryTypes.join(","), + live: liveEntries + .map(entry => `${entry.name}:${entry.entryType}`) + .join(","), + liveMarks: list.getEntriesByType("mark").length, + namedMeasure: list.getEntriesByName("worker-observed-measure", "measure").length, + buffered: bufferedEntries + .map(entry => `${entry.name}:${entry.entryType}`) + .join(",") + }); + bufferedObserver.disconnect(); + close(); + }); + bufferedObserver.observe({ type: "mark", buffered: true }); + }); + firstObserver.observe({ entryTypes: ["mark", "measure"] }); + performance.mark("worker-observed-mark", { startTime: 1 }); + performance.measure("worker-observed-measure", { + start: 1, + end: 3 + }); + "# + .into(), + "test://worker_performance_observer".into(), + ); + + let msg = timeout(TIMEOUT, handle.recv()) + .await + .expect("timed out") + .expect("channel closed"); + assert_eq!( + expect_post_json(msg), + r#"{"interfaces":true,"prototypeMethods":true,"supportedEntryTypes":"mark,measure,resource","live":"worker-observed-mark:mark,worker-observed-measure:measure","liveMarks":1,"namedMeasure":1,"buffered":"worker-observed-mark:mark"}"# ); }