mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 00:01:34 +00:00
fix(performance): support User Timing in workers
This commit is contained in:
@@ -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"}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user