fix(performance): support User Timing in workers

This commit is contained in:
ldm0
2026-09-27 19:22:01 +08:00
parent dced77cc24
commit b3936c133b
18 changed files with 660 additions and 331 deletions
+83 -4
View File
@@ -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(
@@ -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<RealmDependentFinal
pub(super) fn finalize_materialized_interface(
scope: &mut v8::PinScope<'_, '_>,
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",
]
);
@@ -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())
@@ -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",
@@ -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<RealmInterfaceState> {
self.states.borrow().get(id.index()).copied()
}
@@ -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>(
@@ -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,
@@ -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::<v8::Object>::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>(
@@ -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::<v8::Object>::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::<Vec<_>>();
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::<v8::Array>::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<moli_webidl_callback::PreparedWebIdlCallbackFunction> {
let callback =
performance_observer_slot_value(scope, observer, PERFORMANCE_OBSERVER_CALLBACK_VALUE_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())?;
let relevant_global = performance_observer_slot_value(
scope,
observer,
PERFORMANCE_OBSERVER_CALLBACK_RELEVANT_GLOBAL_SLOT,
)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())?;
let incumbent_global = performance_observer_slot_value(
scope,
observer,
PERFORMANCE_OBSERVER_CALLBACK_INCUMBENT_GLOBAL_SLOT,
)
.and_then(|value| v8::Local::<v8::Object>::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>,
@@ -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,
@@ -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<v8::Local<'s, v8::Object>>,
@@ -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::<v8::Object>::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()),
@@ -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())
}
@@ -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";
@@ -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());
}
@@ -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"}"#
);
}
@@ -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)
@@ -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);
@@ -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"}"#
);
}