From 1e7b796d78d2119e9681c5e464ff441a4e2708e5 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 13 Oct 2026 15:11:54 +0800 Subject: [PATCH] feat(performance): share native User Timing and observers with workers --- moli-renderer-v8/src/context_bootstrap.rs | 87 ++++++- .../exposed_interfaces/finalize.rs | 24 +- .../exposed_interfaces/materialize.rs | 8 +- .../exposed_interfaces/metadata.rs | 6 + .../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 | 173 ++++++++++---- .../context_bootstrap/performance_runtime.rs | 16 +- .../performance_runtime/install.rs | 98 +++++++- .../performance_runtime/window_state.rs | 10 +- .../src/context_bootstrap/shared/slots.rs | 4 + .../window_runtime/performance.rs | 9 +- .../src/worker/global_scope/bootstrap.rs | 2 +- .../src/worker/global_scope/mod.rs | 2 +- .../src/worker/global_scope/performance.rs | 48 +--- .../src/worker/thread/tests/lazy_storage.rs | 7 +- .../src/worker/thread/tests/postmessage.rs | 216 +++++++++++++++++- 18 files changed, 673 insertions(+), 181 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 9b2886f995..c37126d967 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -347,13 +347,13 @@ pub(crate) use self::media_queries::{ simple_object_event_target_add_listener, simple_object_event_target_remove_listener, simple_object_has_event_listeners, }; -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, }; @@ -806,6 +806,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 { @@ -821,6 +832,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>, @@ -860,11 +940,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 538dcc5d48..c9677dd73a 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/finalize.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result, anyhow}; +use super::metadata::RealmKind; use crate::context_bootstrap::{ crypto::finalize_crypto_realm_bindings, events::finalize_pointer_event_realm_bindings, navigator_runtime::finalize_navigator_credentials_realm_bindings, @@ -20,6 +21,7 @@ enum RealmDependentFinalizer { XmlHttpRequestEventTargetState, NotificationPermission, PointerEventSecureContextSurface, + PerformanceWindowAccessors, PerformanceObserverSupportedEntryTypes, } @@ -78,6 +80,10 @@ const REALM_DEPENDENT_FINALIZER_ALLOWLIST: &[(&str, RealmDependentFinalizer)] = "MathMLElement", RealmDependentFinalizer::GlobalEventHandlersSecureContextSurface, ), + ( + "Performance", + RealmDependentFinalizer::PerformanceWindowAccessors, + ), ( "PerformanceObserver", RealmDependentFinalizer::PerformanceObserverSupportedEntryTypes, @@ -102,6 +108,7 @@ pub(super) fn finalize_materialized_interface<'s>( interface_name: &str, constructor: v8::Local<'s, v8::Function>, prototype: v8::Local<'s, v8::Object>, + realm_kind: RealmKind, ) -> Result<()> { let Some(finalizer) = realm_dependent_finalizer(interface_name) else { return Ok(()); @@ -139,8 +146,22 @@ pub(super) fn finalize_materialized_interface<'s>( RealmDependentFinalizer::PointerEventSecureContextSurface => { finalize_pointer_event_realm_bindings(scope, prototype) } + RealmDependentFinalizer::PerformanceWindowAccessors => { + if realm_kind == RealmKind::Window { + crate::context_bootstrap::performance_runtime::finalize_window_performance_realm_bindings( + scope, prototype, + ) + } else { + Ok(()) + } + } RealmDependentFinalizer::PerformanceObserverSupportedEntryTypes => { - finalize_performance_observer_realm_bindings(scope, constructor.into()) + 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 + }; + finalize_performance_observer_realm_bindings(scope, constructor.into(), supported_entry_types) } } .with_context(|| format!("failed to finalize intrinsic interface `{interface_name}`")) @@ -189,6 +210,7 @@ mod tests { "HTMLElement", "SVGElement", "MathMLElement", + "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 160d58dc84..03600c76c7 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,13 @@ fn finish_materialized_interface<'s>( }; // Like Blink's ConstructorForTypeSlowCase, finish every fallible binding // installation before publishing the interface in the per-context cache. - finalize_materialized_interface(scope, metadata.name, constructor, constructor_prototype)?; + finalize_materialized_interface( + scope, + metadata.name, + constructor, + constructor_prototype, + realm.realm_kind(), + )?; realm.publish_ready( scope, id, 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 61b870a9aa..7e8449997d 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs @@ -99,6 +99,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/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 e895516f75..9ca00a295e 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 47ae589b8f..a7bed1aeee 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; @@ -88,14 +87,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, @@ -167,7 +192,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; } @@ -181,15 +206,15 @@ pub(in crate::context_bootstrap) fn performance_observer_observe_callback<'s>( } entry_types } else { - performance_entry_types_array_from_strings( - scope, - init.entry_types - .map(|entry_types| entry_types.0) - .unwrap_or_default() - .into_iter() - .map(|entry_type| entry_type.0) - .filter(|entry_type| performance_observer_entry_type_supported(entry_type)), - ) + let supported_entry_types = init + .entry_types + .map(|entry_types| entry_types.0) + .unwrap_or_default() + .into_iter() + .map(|entry_type| entry_type.0) + .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(); @@ -218,27 +243,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; } @@ -260,6 +291,8 @@ pub(in crate::context_bootstrap) fn performance_observer_disconnect_callback<'s> && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { crate::observer_runtime::deactivate_performance_observer_callback(host_ptr, callback_id); + } else if crate::worker::get_worker_state(scope).is_some() { + remove_worker_observer_root(scope, args.this()); } let pending = v8::Array::new(scope, 0); set_performance_observer_pending(scope, args.this(), pending); @@ -309,8 +342,18 @@ fn performance_observer_mode<'s>( } } -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( @@ -365,6 +408,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>, @@ -500,3 +572,24 @@ fn set_performance_observer_bool_slot( let value = v8::Boolean::new(scope, value); set_performance_observer_slot_value(scope, observer, slot, value.into()); } + +fn remove_worker_observer_root<'s>( + scope: &mut v8::PinScope<'s, '_>, + observer: v8::Local<'s, v8::Object>, +) { + let Some(registry) = global_queue_array(scope, PERFORMANCE_OBSERVER_REGISTRY_SLOT) else { + return; + }; + let retained: Vec<_> = (0..registry.length()) + .filter_map(|index| registry.get_index(scope, index)) + .filter(|value| !value.strict_equals(observer.into())) + .collect(); + let registry = v8::Array::new_with_elements(scope, &retained); + let global = scope.get_current_context().global(scope); + set_private_value( + scope, + global, + PERFORMANCE_OBSERVER_REGISTRY_SLOT, + registry.into(), + ); +} diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs index fc9db31c09..1a566272d9 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) } 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 6f4c843197..cbf055d8c3 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, receiver)] -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, receiver = web_api_interfaces::Performance::is_instance)] +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], ) -> anyhow::Result<()> { - PerformanceObserverConstructorDeclaration::new(PERFORMANCE_OBSERVER_SUPPORTED_ENTRY_TYPES) + PerformanceObserverConstructorDeclaration::new(supported_entry_types) .initialize(scope, constructor) .map_err(|error| anyhow::anyhow!("failed to finalize PerformanceObserver: {error}")) } +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", @@ -801,9 +874,11 @@ fn navigation_performance_entry<'s>( 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) @@ -819,7 +894,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>( @@ -1041,6 +1115,10 @@ 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) { match super::lazy_subobjects::ensure_performance_subobject(scope, args.this(), subobject) { Ok(value) => rv.set(value), 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 7b3802ddd2..2032bd8f1d 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared/slots.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared/slots.rs @@ -129,8 +129,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/worker/global_scope/bootstrap.rs b/moli-renderer-v8/src/worker/global_scope/bootstrap.rs index 2a205b070f..e587f61eb2 100644 --- a/moli-renderer-v8/src/worker/global_scope/bootstrap.rs +++ b/moli-renderer-v8/src/worker/global_scope/bootstrap.rs @@ -142,7 +142,6 @@ pub(in crate::worker) 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( @@ -178,6 +177,7 @@ pub(in crate::worker) 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, diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index ccf70b7c2f..037d45be26 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -220,7 +220,7 @@ use network_state::{ }; pub(crate) use origin::worker_global_origin; use origin::{WorkerGlobalOriginDeclaration, WorkerGlobalOriginPrototypeDeclaration}; -use performance::{install_worker_performance, monotonic_unix_epoch_millis}; +use performance::monotonic_unix_epoch_millis; pub(super) use service_worker_results::{ PendingServiceWorkerClientFocus, PendingServiceWorkerClientNavigate, PendingServiceWorkerClientQuery, PendingServiceWorkerClientQueryType, diff --git a/moli-renderer-v8/src/worker/global_scope/performance.rs b/moli-renderer-v8/src/worker/global_scope/performance.rs index a0dd296a11..642817ddeb 100644 --- a/moli-renderer-v8/src/worker/global_scope/performance.rs +++ b/moli-renderer-v8/src/worker/global_scope/performance.rs @@ -1,53 +1,7 @@ -//! Worker performance clock and time origin. +//! Worker monotonic clock. Performance API uses the shared realm implementation. use super::*; -#[derive(WebApiObject)] -#[webapi(plain)] -struct WorkerGlobalPerformanceDeclaration<'scope> { - #[webapi(data_property)] - performance: v8::Local<'scope, v8::Object>, -} - -#[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: (), -} - -pub(super) 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 3d8ca9cf4a..54f1cab864 100644 --- a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs @@ -2110,18 +2110,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(); "# @@ -2135,7 +2160,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"}"# ); }