diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 4de4cd68c44..97dcd2497eb 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -681,6 +681,7 @@ set(SOURCES Painting/VideoPaintable.cpp Painting/ViewportPaintable.cpp PerformanceTimeline/EntryTypes.cpp + PerformanceTimeline/EventNames.cpp PerformanceTimeline/PerformanceEntry.cpp PerformanceTimeline/PerformanceObserver.cpp PerformanceTimeline/PerformanceObserverEntryList.cpp @@ -701,6 +702,7 @@ set(SOURCES ResizeObserver/ResizeObserver.cpp ResizeObserver/ResizeObserverEntry.cpp ResizeObserver/ResizeObserverSize.cpp + ResourceTiming/PerformanceResourceTiming.cpp SecureContexts/AbstractOperations.cpp ServiceWorker/Job.cpp ServiceWorker/Registration.cpp diff --git a/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp b/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp index 97b5ff3cd0a..4d382b74974 100644 --- a/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp +++ b/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -687,13 +688,11 @@ void fetch_response_handover(JS::Realm& realm, Infrastructure::FetchParams const body_info.content_type = MimeSniff::minimise_a_supported_mime_type(mime_type.value()); } - // FIXME: 8. If fetchParams’s request’s initiator type is not null, then mark resource timing given timingInfo, - // request’s URL, request’s initiator type, global, cacheState, bodyInfo, and responseStatus. - (void)timing_info; - (void)global; - (void)cache_state; - (void)body_info; - (void)response_status; + // 8. If fetchParams’s request’s initiator type is not null, then mark resource timing given timingInfo, + // request’s URL, request’s initiator type, global, cacheState, bodyInfo, and responseStatus. + if (fetch_params.request()->initiator_type().has_value()) { + ResourceTiming::PerformanceResourceTiming::mark_resource_timing(timing_info, fetch_params.request()->url().to_string(), Infrastructure::initiator_type_to_string(fetch_params.request()->initiator_type().value()), global, cache_state, body_info, response_status); + } }); // 4. Let processResponseEndOfBodyTask be the following steps: diff --git a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.cpp b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.cpp index c6b3b030182..c1fb681660b 100644 --- a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.cpp +++ b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.cpp @@ -442,6 +442,55 @@ StringView request_mode_to_string(Request::Mode mode) VERIFY_NOT_REACHED(); } +FlyString initiator_type_to_string(Request::InitiatorType initiator_type) +{ + switch (initiator_type) { + case Request::InitiatorType::Audio: + return "audio"_fly_string; + case Request::InitiatorType::Beacon: + return "beacon"_fly_string; + case Request::InitiatorType::Body: + return "body"_fly_string; + case Request::InitiatorType::CSS: + return "css"_fly_string; + case Request::InitiatorType::EarlyHint: + return "early-hints"_fly_string; + case Request::InitiatorType::Embed: + return "embed"_fly_string; + case Request::InitiatorType::Fetch: + return "fetch"_fly_string; + case Request::InitiatorType::Font: + return "font"_fly_string; + case Request::InitiatorType::Frame: + return "frame"_fly_string; + case Request::InitiatorType::IFrame: + return "iframe"_fly_string; + case Request::InitiatorType::Image: + return "image"_fly_string; + case Request::InitiatorType::IMG: + return "img"_fly_string; + case Request::InitiatorType::Input: + return "input"_fly_string; + case Request::InitiatorType::Link: + return "link"_fly_string; + case Request::InitiatorType::Object: + return "object"_fly_string; + case Request::InitiatorType::Ping: + return "ping"_fly_string; + case Request::InitiatorType::Script: + return "script"_fly_string; + case Request::InitiatorType::Track: + return "track"_fly_string; + case Request::InitiatorType::Video: + return "video"_fly_string; + case Request::InitiatorType::XMLHttpRequest: + return "xmlhttprequest"_fly_string; + case Request::InitiatorType::Other: + return "other"_fly_string; + } + VERIFY_NOT_REACHED(); +} + Optional request_priority_from_string(StringView string) { if (string.equals_ignoring_ascii_case("high"sv)) diff --git a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.h b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.h index b113ccd343c..b9504ca4f64 100644 --- a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.h +++ b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Requests.h @@ -531,6 +531,7 @@ private: StringView request_destination_to_string(Request::Destination); StringView request_mode_to_string(Request::Mode); +FlyString initiator_type_to_string(Request::InitiatorType); Optional request_priority_from_string(StringView); diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index ec6d824c740..a4a2a106cf8 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -74,6 +74,7 @@ enum class MediaEncodingType : u8; enum class MediaKeysRequirement : u8; enum class ReadableStreamReaderMode : u8; enum class ReferrerPolicy : u8; +enum class RenderBlockingStatusType : u8; enum class RequestCache : u8; enum class RequestCredentials : u8; enum class RequestDestination : u8; @@ -756,6 +757,10 @@ namespace Web::ResizeObserver { class ResizeObserver; } +namespace Web::ResourceTiming { +class PerformanceResourceTiming; +} + namespace Web::Selection { class Selection; } diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index ce192ee89fe..88918547c88 100644 --- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -34,10 +34,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -75,6 +77,7 @@ void WindowOrWorkerGlobalScopeMixin::visit_edges(JS::Cell::Visitor& visitor) entry.value.visit_edges(visitor); visitor.visit(m_registered_event_sources); visitor.visit(m_crypto); + visitor.visit(m_resource_timing_secondary_buffer); } void WindowOrWorkerGlobalScopeMixin::finalize() @@ -472,7 +475,6 @@ void WindowOrWorkerGlobalScopeMixin::add_performance_entry(GC::Ref, FlyString const& entry_type) { auto& tuple = relevant_performance_entry_tuple(entry_type); @@ -643,6 +645,102 @@ void WindowOrWorkerGlobalScopeMixin::queue_the_performance_observer_task() })); } +// https://w3c.github.io/resource-timing/#dfn-add-a-performanceresourcetiming-entry +void WindowOrWorkerGlobalScopeMixin::add_resource_timing_entry(Badge, GC::Ref entry) +{ + // 1. If can add resource timing entry returns true and resource timing buffer full event pending flag is false, + // run the following substeps: + if (can_add_resource_timing_entry() && !m_resource_timing_buffer_full_event_pending) { + // a. Add new entry to the performance entry buffer. + // b. Increase resource timing buffer current size by 1. + add_performance_entry(entry); + + // c. Return. + return; + } + + // 2. If resource timing buffer full event pending flag is false, run the following substeps: + if (!m_resource_timing_buffer_full_event_pending) { + // a. Set resource timing buffer full event pending flag to true. + m_resource_timing_buffer_full_event_pending = true; + + // b. Queue a task on the performance timeline task source to run fire a buffer full event. + HTML::queue_a_task(HTML::Task::Source::PerformanceTimeline, nullptr, nullptr, GC::create_function(this_impl().heap(), [this] { + fire_resource_timing_buffer_full_event(); + })); + } + + // 3. Add new entry to the resource timing secondary buffer. + // 4. Increase resource timing secondary buffer current size by 1. + m_resource_timing_secondary_buffer.append(entry); +} + +// https://w3c.github.io/resource-timing/#dfn-can-add-resource-timing-entry +bool WindowOrWorkerGlobalScopeMixin::can_add_resource_timing_entry() +{ + // 1. If resource timing buffer current size is smaller than resource timing buffer size limit, return true. + // 2. Return false. + return resource_timing_buffer_current_size() < m_resource_timing_buffer_size_limit; +} + +// https://w3c.github.io/resource-timing/#dfn-resource-timing-buffer-current-size +size_t WindowOrWorkerGlobalScopeMixin::resource_timing_buffer_current_size() +{ + // A resource timing buffer current size which is initially 0. + auto resource_timing_tuple = relevant_performance_entry_tuple(PerformanceTimeline::EntryTypes::resource); + return resource_timing_tuple.performance_entry_buffer.size(); +} + +// https://w3c.github.io/resource-timing/#dfn-fire-a-buffer-full-event +void WindowOrWorkerGlobalScopeMixin::fire_resource_timing_buffer_full_event() +{ + // 1. While resource timing secondary buffer is not empty, run the following substeps: + while (!m_resource_timing_secondary_buffer.is_empty()) { + // 1. Let number of excess entries before be resource timing secondary buffer current size. + auto number_of_excess_entries_before = m_resource_timing_secondary_buffer.size(); + + // 2. If can add resource timing entry returns false, then fire an event named resourcetimingbufferfull at the Performance object. + if (!can_add_resource_timing_entry()) { + auto full_event = DOM::Event::create(this_impl().realm(), PerformanceTimeline::EventNames::resourcetimingbufferfull); + performance()->dispatch_event(full_event); + } + + // 3. Run copy secondary buffer. + copy_resource_timing_secondary_buffer(); + + // 4. Let number of excess entries after be resource timing secondary buffer current size. + auto number_of_excess_entries_after = m_resource_timing_secondary_buffer.size(); + + // 5. If number of excess entries before is lower than or equals number of excess entries after, then remove + // all entries from resource timing secondary buffer, set resource timing secondary buffer current size to + // 0, and abort these steps. + if (number_of_excess_entries_before <= number_of_excess_entries_after) { + m_resource_timing_secondary_buffer.clear(); + break; + } + } + + // 2. Set resource timing buffer full event pending flag to false. + m_resource_timing_buffer_full_event_pending = false; +} + +// https://w3c.github.io/resource-timing/#dfn-copy-secondary-buffer +void WindowOrWorkerGlobalScopeMixin::copy_resource_timing_secondary_buffer() +{ + // 1. While resource timing secondary buffer is not empty and can add resource timing entry returns true, + // run the following substeps: + while (!m_resource_timing_secondary_buffer.is_empty() && can_add_resource_timing_entry()) { + // 1. Let entry be the oldest PerformanceResourceTiming in resource timing secondary buffer. + // 2. Add entry to the end of performance entry buffer. + // 3. Increment resource timing buffer current size by 1. + // 4. Remove entry from resource timing secondary buffer. + // 5. Decrement resource timing secondary buffer current size by 1. + auto entry = m_resource_timing_secondary_buffer.take_first(); + auto& resource_tuple = relevant_performance_entry_tuple(PerformanceTimeline::EntryTypes::resource); + resource_tuple.performance_entry_buffer.append(entry); + } +} + void WindowOrWorkerGlobalScopeMixin::register_event_source(Badge, GC::Ref event_source) { m_registered_event_sources.set(event_source); diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h index 154574f992c..782b8eda58f 100644 --- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h +++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h @@ -66,6 +66,9 @@ public: void queue_the_performance_observer_task(); + void set_resource_timing_buffer_size_limit(Badge, u32 value) { m_resource_timing_buffer_size_limit = value; } + void add_resource_timing_entry(Badge, GC::Ref entry); + void register_event_source(Badge, GC::Ref); void unregister_event_source(Badge, GC::Ref); void forcibly_close_all_event_sources(); @@ -112,6 +115,11 @@ private: GC::Ref create_image_bitmap_impl(ImageBitmapSource& image, Optional sx, Optional sy, Optional sw, Optional sh, Optional& options) const; + size_t resource_timing_buffer_current_size(); + bool can_add_resource_timing_entry(); + void fire_resource_timing_buffer_full_event(); + void copy_resource_timing_secondary_buffer(); + IDAllocator m_timer_id_allocator; HashMap> m_timers; @@ -141,6 +149,24 @@ private: bool m_error_reporting_mode { false }; WebSockets::WebSocket::List m_registered_web_sockets; + + // https://w3c.github.io/resource-timing/#sec-extensions-performance-interface + // Each ECMAScript global environment has: + // https://w3c.github.io/resource-timing/#dfn-resource-timing-buffer-size-limit + // A resource timing buffer size limit which should initially be 250 or greater. + // The recommended minimum number of PerformanceResourceTiming objects is 250, though this may be changed by the + // user agent. setResourceTimingBufferSize can be called to request a change to this limit. + u32 m_resource_timing_buffer_size_limit { 250 }; + + // https://w3c.github.io/resource-timing/#dfn-resource-timing-buffer-full-event-pending-flag + // A resource timing buffer full event pending flag which is initially false. + bool m_resource_timing_buffer_full_event_pending { false }; + + // https://w3c.github.io/resource-timing/#dfn-resource-timing-secondary-buffer-current-size + // A resource timing secondary buffer current size which is initially 0. + // https://w3c.github.io/resource-timing/#dfn-resource-timing-secondary-buffer + // A resource timing secondary buffer to store PerformanceResourceTiming objects that is initially empty. + Vector> m_resource_timing_secondary_buffer; }; } diff --git a/Libraries/LibWeb/HighResolutionTime/Performance.cpp b/Libraries/LibWeb/HighResolutionTime/Performance.cpp index d9be79f28e0..7ab3df295cf 100644 --- a/Libraries/LibWeb/HighResolutionTime/Performance.cpp +++ b/Libraries/LibWeb/HighResolutionTime/Performance.cpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace Web::HighResolutionTime { @@ -330,6 +331,35 @@ void Performance::clear_measures(Optional measure_name) // 3. Return undefined. } +// https://w3c.github.io/resource-timing/#dom-performance-clearresourcetimings +void Performance::clear_resource_timings() +{ + // 1. Remove all PerformanceResourceTiming objects in the performance entry buffer. + // 2. Set resource timing buffer current size to 0. + window_or_worker().clear_performance_entry_buffer({}, PerformanceTimeline::EntryTypes::resource); +} + +// https://w3c.github.io/resource-timing/#dom-performance-setresourcetimingbuffersize +void Performance::set_resource_timing_buffer_size(u32 max_size) +{ + // 1. Set resource timing buffer size limit to the maxSize parameter. If the maxSize parameter is less than + // resource timing buffer current size, no PerformanceResourceTiming objects are to be removed from the + // performance entry buffer. + window_or_worker().set_resource_timing_buffer_size_limit({}, max_size); +} + +// https://w3c.github.io/resource-timing/#dom-performance-onresourcetimingbufferfull +void Performance::set_onresourcetimingbufferfull(WebIDL::CallbackType* event_handler) +{ + set_event_handler_attribute(PerformanceTimeline::EventNames::resourcetimingbufferfull, event_handler); +} + +// https://w3c.github.io/resource-timing/#dom-performance-onresourcetimingbufferfull +WebIDL::CallbackType* Performance::onresourcetimingbufferfull() +{ + return event_handler_attribute(PerformanceTimeline::EventNames::resourcetimingbufferfull); +} + // https://www.w3.org/TR/performance-timeline/#getentries-method WebIDL::ExceptionOr>> Performance::get_entries() const { diff --git a/Libraries/LibWeb/HighResolutionTime/Performance.h b/Libraries/LibWeb/HighResolutionTime/Performance.h index 799f61143f0..62f342c2faa 100644 --- a/Libraries/LibWeb/HighResolutionTime/Performance.h +++ b/Libraries/LibWeb/HighResolutionTime/Performance.h @@ -30,6 +30,11 @@ public: WebIDL::ExceptionOr> measure(String const& measure_name, Variant const& start_or_measure_options, Optional end_mark); void clear_measures(Optional measure_name); + void clear_resource_timings(); + void set_resource_timing_buffer_size(u32 max_size); + void set_onresourcetimingbufferfull(WebIDL::CallbackType*); + WebIDL::CallbackType* onresourcetimingbufferfull(); + WebIDL::ExceptionOr>> get_entries() const; WebIDL::ExceptionOr>> get_entries_by_type(String const& type) const; WebIDL::ExceptionOr>> get_entries_by_name(String const& name, Optional type) const; diff --git a/Libraries/LibWeb/HighResolutionTime/Performance.idl b/Libraries/LibWeb/HighResolutionTime/Performance.idl index e619f01f337..0ee1307bde7 100644 --- a/Libraries/LibWeb/HighResolutionTime/Performance.idl +++ b/Libraries/LibWeb/HighResolutionTime/Performance.idl @@ -23,6 +23,12 @@ interface Performance : EventTarget { PerformanceMeasure measure(DOMString measureName, optional (DOMString or PerformanceMeasureOptions) startOrMeasureOptions = {}, optional DOMString endMark); undefined clearMeasures(optional DOMString measureName); + // https://w3c.github.io/resource-timing/#sec-extensions-performance-interface + // "Resource Timing" extensions to the Performance interface + undefined clearResourceTimings(); + undefined setResourceTimingBufferSize(unsigned long maxSize); + attribute EventHandler onresourcetimingbufferfull; + // https://www.w3.org/TR/performance-timeline/#extensions-to-the-performance-interface // "Performance Timeline" extensions to the Performance interface PerformanceEntryList getEntries(); diff --git a/Libraries/LibWeb/HighResolutionTime/SupportedPerformanceTypes.h b/Libraries/LibWeb/HighResolutionTime/SupportedPerformanceTypes.h index 55e02ccf410..ab991130e5b 100644 --- a/Libraries/LibWeb/HighResolutionTime/SupportedPerformanceTypes.h +++ b/Libraries/LibWeb/HighResolutionTime/SupportedPerformanceTypes.h @@ -9,8 +9,9 @@ namespace Web::HighResolutionTime { // Please keep these in alphabetical order based on the entry type :^) -#define ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES \ - __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(PerformanceTimeline::EntryTypes::mark, UserTiming::PerformanceMark) \ - __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(PerformanceTimeline::EntryTypes::measure, UserTiming::PerformanceMeasure) +#define ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES \ + __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(PerformanceTimeline::EntryTypes::mark, UserTiming::PerformanceMark) \ + __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(PerformanceTimeline::EntryTypes::measure, UserTiming::PerformanceMeasure) \ + __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(PerformanceTimeline::EntryTypes::resource, ResourceTiming::PerformanceResourceTiming) } diff --git a/Libraries/LibWeb/PerformanceTimeline/EventNames.cpp b/Libraries/LibWeb/PerformanceTimeline/EventNames.cpp new file mode 100644 index 00000000000..40f882a2d13 --- /dev/null +++ b/Libraries/LibWeb/PerformanceTimeline/EventNames.cpp @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Web::PerformanceTimeline::EventNames { + +#define __ENUMERATE_PERFORMANCE_TIMELINE_EVENT(name) \ + FlyString name = #name##_fly_string; +ENUMERATE_PERFORMANCE_TIMELINE_EVENTS +#undef __ENUMERATE_PERFORMANCE_TIMELINE_EVENT + +} diff --git a/Libraries/LibWeb/PerformanceTimeline/EventNames.h b/Libraries/LibWeb/PerformanceTimeline/EventNames.h new file mode 100644 index 00000000000..82d33c41c2e --- /dev/null +++ b/Libraries/LibWeb/PerformanceTimeline/EventNames.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::PerformanceTimeline::EventNames { + +#define ENUMERATE_PERFORMANCE_TIMELINE_EVENTS \ + __ENUMERATE_PERFORMANCE_TIMELINE_EVENT(resourcetimingbufferfull) + +#define __ENUMERATE_PERFORMANCE_TIMELINE_EVENT(name) extern FlyString name; +ENUMERATE_PERFORMANCE_TIMELINE_EVENTS +#undef __ENUMERATE_PERFORMANCE_TIMELINE_EVENT + +} diff --git a/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.cpp b/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.cpp new file mode 100644 index 00000000000..bca0239883b --- /dev/null +++ b/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.cpp @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2025, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::ResourceTiming { + +GC_DEFINE_ALLOCATOR(PerformanceResourceTiming); + +PerformanceResourceTiming::PerformanceResourceTiming(JS::Realm& realm, String const& name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration, GC::Ref timing_info) + : PerformanceTimeline::PerformanceEntry(realm, name, start_time, duration) + , m_timing_info(timing_info) +{ +} + +PerformanceResourceTiming::~PerformanceResourceTiming() = default; + +// https://w3c.github.io/resource-timing/#dfn-entrytype +FlyString const& PerformanceResourceTiming::entry_type() const +{ + // entryType + // The entryType getter steps are to return the DOMString "resource". + return PerformanceTimeline::EntryTypes::resource; +} + +void PerformanceResourceTiming::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(PerformanceResourceTiming); +} + +void PerformanceResourceTiming::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_timing_info); +} + +// https://w3c.github.io/resource-timing/#dfn-convert-fetch-timestamp +HighResolutionTime::DOMHighResTimeStamp convert_fetch_timestamp(HighResolutionTime::DOMHighResTimeStamp time_stamp, JS::Object const& global) +{ + // 1. If ts is zero, return zero. + if (time_stamp == 0.0) + return 0.0; + + // 2. Otherwise, return the relative high resolution coarse time given ts and global. + return HighResolutionTime::relative_high_resolution_coarsen_time(time_stamp, global); +} + +// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing +void PerformanceResourceTiming::mark_resource_timing(GC::Ref timing_info, String const& requested_url, FlyString const& initiator_type, JS::Object& global, Optional const& cache_mode, Fetch::Infrastructure::Response::BodyInfo body_info, Fetch::Infrastructure::Status response_status, FlyString delivery_type) +{ + // 1. Create a PerformanceResourceTiming object entry in global's realm. + auto& window_or_worker = as(global); + auto& realm = window_or_worker.this_impl().realm(); + + // https://w3c.github.io/resource-timing/#dfn-name + // name + // The name getter steps are to return this's requested URL. + + // https://w3c.github.io/resource-timing/#dfn-starttime + // startTime + // The startTime getter steps are to convert fetch timestamp for this's timing info's start time and this's relevant global object. + + // https://w3c.github.io/resource-timing/#dfn-duration + // duration + // The duration getter steps are to return this's timing info's end time minus this's timing info's start time. + auto converted_start_time = convert_fetch_timestamp(timing_info->start_time(), global); + auto converted_end_time = convert_fetch_timestamp(timing_info->end_time(), global); + auto entry = realm.create(realm, requested_url, converted_start_time, converted_end_time - converted_start_time, timing_info); + + // Setup the resource timing entry for entry, given initiatorType, requestedURL, timingInfo, cacheMode, bodyInfo, responseStatus, and deliveryType. + entry->setup_the_resource_timing_entry(initiator_type, requested_url, timing_info, cache_mode, move(body_info), response_status, delivery_type); + + // 3. Queue entry. + window_or_worker.queue_performance_entry(entry); + + // 4. Add entry to global's performance entry buffer. + window_or_worker.add_resource_timing_entry({}, entry); +} + +// https://www.w3.org/TR/resource-timing/#dfn-setup-the-resource-timing-entry +void PerformanceResourceTiming::setup_the_resource_timing_entry(FlyString const& initiator_type, String const& requested_url, GC::Ref timing_info, Optional const& cache_mode, Fetch::Infrastructure::Response::BodyInfo body_info, Fetch::Infrastructure::Status response_status, FlyString delivery_type) +{ + // 2. Setup the resource timing entry for entry, given initiatorType, requestedURL, timingInfo, cacheMode, bodyInfo, responseStatus, and deliveryType. + // https://w3c.github.io/resource-timing/#dfn-setup-the-resource-timing-entry + + // 1. Assert that cacheMode is the empty string, "local", or "validated". + + // 2. Set entry's initiator type to initiatorType. + m_initiator_type = initiator_type; + + // 3. Set entry's requested URL to requestedURL. + m_requested_url = requested_url; + + // 4. Set entry's timing info to timingInfo. + m_timing_info = timing_info; + + // 5. Set entry's response body info to bodyInfo. + m_response_body_info = move(body_info); + + // 6. Set entry's cache mode to cacheMode. + m_cache_mode = cache_mode; + + // 7. Set entry's response status to responseStatus. + m_response_status = response_status; + + // 8. If deliveryType is the empty string and cacheMode is not, then set deliveryType to "cache". + if (delivery_type.is_empty() && cache_mode.has_value()) + delivery_type = "cache"_fly_string; + + // 9. Set entry's delivery type to deliveryType. + m_delivery_type = delivery_type; +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-nexthopprotocol +FlyString PerformanceResourceTiming::next_hop_protocol() const +{ + // The nextHopProtocol getter steps are to isomorphic decode this's timing info's final connection timing info's + // ALPN negotiated protocol. See Recording connection timing info for more info. + // NOTE: "final connection timing info" can be null, e.g. if this is the timing of a cross-origin resource and + // the Timing-Allow-Origin check fails. We return empty string in this case. + if (!m_timing_info->final_connection_timing_info().has_value()) + return ""_fly_string; + + return m_timing_info->final_connection_timing_info()->alpn_negotiated_protocol; +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-workerstart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::worker_start() const +{ + // The workerStart getter steps are to convert fetch timestamp for this's timing info's final service worker start + // time and the relevant global object for this. See HTTP fetch for more info. + return convert_fetch_timestamp(m_timing_info->final_service_worker_start_time(), HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-redirectstart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::redirect_start() const +{ + // The redirectStart getter steps are to convert fetch timestamp for this's timing info's redirect start time and + // the relevant global object for this. See HTTP-redirect fetch for more info. + return convert_fetch_timestamp(m_timing_info->redirect_start_time(), HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-redirectend +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::redirect_end() const +{ + // The redirectEnd getter steps are to convert fetch timestamp for this's timing info's redirect end time and the + // relevant global object for this. See HTTP-redirect fetch for more info. + return convert_fetch_timestamp(m_timing_info->redirect_end_time(), HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-fetchstart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::fetch_start() const +{ + // The fetchStart getter steps are to convert fetch timestamp for this's timing info's post-redirect start time and + // the relevant global object for this. See HTTP fetch for more info. + return convert_fetch_timestamp(m_timing_info->post_redirect_start_time(), HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-domainlookupstart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::domain_lookup_start() const +{ + // The domainLookupStart getter steps are to convert fetch timestamp for this's timing info's final connection + // timing info's domain lookup start time and the relevant global object for this. See Recording connection timing + // info for more info. + // NOTE: "final connection timing info" can be null, e.g. if this is the timing of a cross-origin resource and + // the Timing-Allow-Origin check fails. We return 0.0 in this case. + if (!m_timing_info->final_connection_timing_info().has_value()) + return 0.0; + + return convert_fetch_timestamp(m_timing_info->final_connection_timing_info()->domain_lookup_start_time, HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-domainlookupend +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::domain_lookup_end() const +{ + // The domainLookupEnd getter steps are to convert fetch timestamp for this's timing info's final connection timing + // info's domain lookup end time and the relevant global object for this. See Recording connection timing info for + // more info. + // NOTE: "final connection timing info" can be null, e.g. if this is the timing of a cross-origin resource and + // the Timing-Allow-Origin check fails. We return 0.0 in this case. + if (!m_timing_info->final_connection_timing_info().has_value()) + return 0.0; + + return convert_fetch_timestamp(m_timing_info->final_connection_timing_info()->domain_lookup_end_time, HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-connectstart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::connect_start() const +{ + // The connectStart getter steps are to convert fetch timestamp for this's timing info's final connection timing + // info's connection start time and the relevant global object for this. See Recording connection timing info for + // more info. + // NOTE: "final connection timing info" can be null, e.g. if this is the timing of a cross-origin resource and + // the Timing-Allow-Origin check fails. We return 0.0 in this case. + if (!m_timing_info->final_connection_timing_info().has_value()) + return 0.0; + + return convert_fetch_timestamp(m_timing_info->final_connection_timing_info()->connection_start_time, HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-connectend +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::connect_end() const +{ + // The connectEnd getter steps are to convert fetch timestamp for this's timing info's final connection timing + // info's connection end time and the relevant global object for this. See Recording connection timing info for + // more info. + // NOTE: "final connection timing info" can be null, e.g. if this is the timing of a cross-origin resource and + // the Timing-Allow-Origin check fails. We return 0.0 in this case. + if (!m_timing_info->final_connection_timing_info().has_value()) + return 0.0; + + return convert_fetch_timestamp(m_timing_info->final_connection_timing_info()->connection_end_time, HTML::relevant_global_object(*this)); +} + +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::secure_connection_start() const +{ + // The secureConnectionStart getter steps are to convert fetch timestamp for this's timing info's final connection + // timing info's secure connection start time and the relevant global object for this. See Recording connection + // timing info for more info. + // NOTE: "final connection timing info" can be null, e.g. if this is the timing of a cross-origin resource and + // the Timing-Allow-Origin check fails. We return 0.0 in this case. + if (!m_timing_info->final_connection_timing_info().has_value()) + return 0.0; + + return convert_fetch_timestamp(m_timing_info->final_connection_timing_info()->secure_connection_start_time, HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-requeststart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::request_start() const +{ + // The requestStart getter steps are to convert fetch timestamp for this's timing info's final network-request + // start time and the relevant global object for this. See HTTP fetch for more info. + return convert_fetch_timestamp(m_timing_info->final_network_request_start_time(), HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-finalresponseheadersstart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::final_response_headers_start() const +{ + // The finalResponseHeadersStart getter steps are to convert fetch timestamp for this's timing info's final + // network-response start time and the relevant global object for this. See HTTP fetch for more info. + return convert_fetch_timestamp(m_timing_info->final_network_response_start_time(), HTML::relevant_global_object(*this)); +} + +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::first_interim_response_start() const +{ + // The firstInterimResponseStart getter steps are to convert fetch timestamp for this's timing info's first interim + // network-response start time and the relevant global object for this. See HTTP fetch for more info. + return convert_fetch_timestamp(m_timing_info->first_interim_network_response_start_time(), HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-responsestart +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::response_start() const +{ + // The responseStart getter steps are to return this's firstInterimResponseStart if it is not 0; + // Otherwise this's finalResponseHeadersStart. + auto first_interim_response_start_time = first_interim_response_start(); + if (first_interim_response_start_time != 0.0) + return first_interim_response_start_time; + + return final_response_headers_start(); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-responseend +HighResolutionTime::DOMHighResTimeStamp PerformanceResourceTiming::response_end() const +{ + // The responseEnd getter steps are to convert fetch timestamp for this's timing info's end time and the relevant + // global object for this. See fetch for more info. + return convert_fetch_timestamp(m_timing_info->end_time(), HTML::relevant_global_object(*this)); +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-encodedbodysize +u64 PerformanceResourceTiming::encoded_body_size() const +{ + // The encodedBodySize getter steps are to return this's resource info's encoded size. + return m_response_body_info.encoded_size; +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-decodedbodysize +u64 PerformanceResourceTiming::decoded_body_size() const +{ + // The decodedBodySize getter steps are to return this's resource info's decoded size. + return m_response_body_info.decoded_size; +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-transfersize +u64 PerformanceResourceTiming::transfer_size() const +{ + if (m_cache_mode.has_value()) { + // 1. If this's cache mode is "local", then return 0. + if (m_cache_mode.value() == Fetch::Infrastructure::Response::CacheState::Local) + return 0; + + // 2. If this's cache mode is "validated", then return 300. + if (m_cache_mode.value() == Fetch::Infrastructure::Response::CacheState::Validated) + return 300; + } + + // 3. Return this's response body info's encoded size plus 300. + // Spec Note: The constant number added to transferSize replaces exposing the total byte size of the HTTP headers, + // as that may expose the presence of certain cookies. See this issue: https://github.com/w3c/resource-timing/issues/238 + return m_response_body_info.encoded_size + 300; +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-responsestatus +Fetch::Infrastructure::Status PerformanceResourceTiming::response_status() const +{ + // The responseStatus getter steps are to return this's response status. + // Spec Note: responseStatus is determined in Fetch. For a cross-origin no-cors request it would be 0 because the + // response would be an opaque filtered response. + return m_response_status; +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-renderblockingstatus +Bindings::RenderBlockingStatusType PerformanceResourceTiming::render_blocking_status() const +{ + // The renderBlockingStatus getter steps are to return blocking if this's timing info's render-blocking is true; + // otherwise non-blocking. + if (m_timing_info->render_blocking()) + return Bindings::RenderBlockingStatusType::Blocking; + + return Bindings::RenderBlockingStatusType::NonBlocking; +} + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-contenttype +String const& PerformanceResourceTiming::content_type() const +{ + // The contentType getter steps are to return this's resource info's content type. + return m_response_body_info.content_type; +} + +} diff --git a/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.h b/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.h new file mode 100644 index 00000000000..7bc6cac944b --- /dev/null +++ b/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web::ResourceTiming { + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming +class PerformanceResourceTiming : public PerformanceTimeline::PerformanceEntry { + WEB_PLATFORM_OBJECT(PerformanceResourceTiming, PerformanceTimeline::PerformanceEntry); + GC_DECLARE_ALLOCATOR(PerformanceResourceTiming); + +public: + virtual ~PerformanceResourceTiming() override; + + static void mark_resource_timing(GC::Ref timing_info, String const& requested_url, FlyString const& initiator_type, JS::Object& global, Optional const& cache_mode, Fetch::Infrastructure::Response::BodyInfo body_info, Fetch::Infrastructure::Status response_status, FlyString delivery_type = ""_fly_string); + + // NOTE: These three functions are answered by the registry for the given entry type. + // https://w3c.github.io/timing-entrytypes-registry/#registry + + // https://w3c.github.io/timing-entrytypes-registry/#dfn-availablefromtimeline + static PerformanceTimeline::AvailableFromTimeline available_from_timeline() { return PerformanceTimeline::AvailableFromTimeline::Yes; } + + // https://w3c.github.io/timing-entrytypes-registry/#dfn-maxbuffersize + static Optional max_buffer_size() { return 250; } + + // https://w3c.github.io/timing-entrytypes-registry/#dfn-should-add-entry + virtual PerformanceTimeline::ShouldAddEntry should_add_entry(Optional = {}) const override { return PerformanceTimeline::ShouldAddEntry::Yes; } + + virtual FlyString const& entry_type() const override; + + // https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-initiatortype + FlyString const& initiator_type() const { return m_initiator_type; } + + // https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-deliverytype + FlyString const& delivery_type() const { return m_delivery_type; } + + FlyString next_hop_protocol() const; + + virtual HighResolutionTime::DOMHighResTimeStamp worker_start() const; + virtual HighResolutionTime::DOMHighResTimeStamp redirect_start() const; + virtual HighResolutionTime::DOMHighResTimeStamp redirect_end() const; + virtual HighResolutionTime::DOMHighResTimeStamp fetch_start() const; + HighResolutionTime::DOMHighResTimeStamp domain_lookup_start() const; + HighResolutionTime::DOMHighResTimeStamp domain_lookup_end() const; + HighResolutionTime::DOMHighResTimeStamp connect_start() const; + HighResolutionTime::DOMHighResTimeStamp connect_end() const; + HighResolutionTime::DOMHighResTimeStamp secure_connection_start() const; + HighResolutionTime::DOMHighResTimeStamp request_start() const; + HighResolutionTime::DOMHighResTimeStamp final_response_headers_start() const; + HighResolutionTime::DOMHighResTimeStamp first_interim_response_start() const; + HighResolutionTime::DOMHighResTimeStamp response_start() const; + HighResolutionTime::DOMHighResTimeStamp response_end() const; + u64 encoded_body_size() const; + u64 decoded_body_size() const; + u64 transfer_size() const; + Fetch::Infrastructure::Status response_status() const; + Bindings::RenderBlockingStatusType render_blocking_status() const; + String const& content_type() const; + +protected: + PerformanceResourceTiming(JS::Realm&, String const& name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration, GC::Ref timing_info); + + void setup_the_resource_timing_entry(FlyString const& initiator_type, String const& requested_url, GC::Ref timing_info, Optional const& cache_mode, Fetch::Infrastructure::Response::BodyInfo body_info, Fetch::Infrastructure::Status response_status, FlyString delivery_type = ""_fly_string); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(JS::Cell::Visitor&) override; + +private: + FlyString m_initiator_type; + String m_requested_url; + GC::Ref m_timing_info; + Fetch::Infrastructure::Response::BodyInfo m_response_body_info; + Optional m_cache_mode; + Fetch::Infrastructure::Status m_response_status; + FlyString m_delivery_type; +}; + +HighResolutionTime::DOMHighResTimeStamp convert_fetch_timestamp(HighResolutionTime::DOMHighResTimeStamp time_stamp, JS::Object const& global); + +} diff --git a/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.idl b/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.idl new file mode 100644 index 00000000000..dfd62cc49da --- /dev/null +++ b/Libraries/LibWeb/ResourceTiming/PerformanceResourceTiming.idl @@ -0,0 +1,36 @@ +#import + +// https://w3c.github.io/resource-timing/#dom-renderblockingstatustype +enum RenderBlockingStatusType { + "blocking", + "non-blocking" +}; + +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming +[Exposed=(Window,Worker)] +interface PerformanceResourceTiming : PerformanceEntry { + readonly attribute DOMString initiatorType; + readonly attribute DOMString deliveryType; + readonly attribute ByteString nextHopProtocol; + readonly attribute DOMHighResTimeStamp workerStart; + readonly attribute DOMHighResTimeStamp redirectStart; + readonly attribute DOMHighResTimeStamp redirectEnd; + readonly attribute DOMHighResTimeStamp fetchStart; + readonly attribute DOMHighResTimeStamp domainLookupStart; + readonly attribute DOMHighResTimeStamp domainLookupEnd; + readonly attribute DOMHighResTimeStamp connectStart; + readonly attribute DOMHighResTimeStamp connectEnd; + readonly attribute DOMHighResTimeStamp secureConnectionStart; + readonly attribute DOMHighResTimeStamp requestStart; + readonly attribute DOMHighResTimeStamp finalResponseHeadersStart; + readonly attribute DOMHighResTimeStamp firstInterimResponseStart; + readonly attribute DOMHighResTimeStamp responseStart; + readonly attribute DOMHighResTimeStamp responseEnd; + readonly attribute unsigned long long transferSize; + readonly attribute unsigned long long encodedBodySize; + readonly attribute unsigned long long decodedBodySize; + readonly attribute unsigned short responseStatus; + readonly attribute RenderBlockingStatusType renderBlockingStatus; + readonly attribute DOMString contentType; + [Default] object toJSON(); +}; diff --git a/Libraries/LibWeb/XHR/XMLHttpRequest.cpp b/Libraries/LibWeb/XHR/XMLHttpRequest.cpp index 0ac835511a1..fa63675eed4 100644 --- a/Libraries/LibWeb/XHR/XMLHttpRequest.cpp +++ b/Libraries/LibWeb/XHR/XMLHttpRequest.cpp @@ -952,8 +952,8 @@ WebIDL::ExceptionOr XMLHttpRequest::send(Optionalterminate(); } - // FIXME: 7. Report timing for this’s fetch controller given the current global object. - // We cannot do this for responses that have a body yet, as we do not setup the stream that then calls processResponseEndOfBody in `fetch_response_handover`. + // 7. Report timing for this’s fetch controller given the current global object. + m_fetch_controller->report_timing(HTML::current_principal_global_object()); // 8. Run handle response end-of-body for this. TRY(handle_response_end_of_body()); diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 288dd9b25aa..1ab0f4d7ed2 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -285,6 +285,7 @@ libweb_js_bindings(RequestIdleCallback/IdleDeadline) libweb_js_bindings(ResizeObserver/ResizeObserver) libweb_js_bindings(ResizeObserver/ResizeObserverEntry) libweb_js_bindings(ResizeObserver/ResizeObserverSize) +libweb_js_bindings(ResourceTiming/PerformanceResourceTiming) libweb_js_bindings(ServiceWorker/ServiceWorker) libweb_js_bindings(ServiceWorker/ServiceWorkerContainer) libweb_js_bindings(ServiceWorker/ServiceWorkerGlobalScope GLOBAL) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index 5bd5b219c89..4fdc25356d7 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -4523,6 +4523,7 @@ using namespace Web::NavigationTiming; using namespace Web::PerformanceTimeline; using namespace Web::RequestIdleCallback; using namespace Web::ResizeObserver; +using namespace Web::ResourceTiming; using namespace Web::Selection; using namespace Web::ServiceWorker; using namespace Web::StorageAPI; diff --git a/Tests/LibWeb/Text/expected/PerformanceObserver/PerformanceObserver-supportedEntryTypes.txt b/Tests/LibWeb/Text/expected/PerformanceObserver/PerformanceObserver-supportedEntryTypes.txt index 1d7ad1b86a4..f48033b1fe5 100644 --- a/Tests/LibWeb/Text/expected/PerformanceObserver/PerformanceObserver-supportedEntryTypes.txt +++ b/Tests/LibWeb/Text/expected/PerformanceObserver/PerformanceObserver-supportedEntryTypes.txt @@ -1,4 +1,4 @@ -PerformanceObserver.supportedEntryTypes: mark,measure +PerformanceObserver.supportedEntryTypes: mark,measure,resource PerformanceObserver.supportedEntryTypes instanceof Array: true Object.isFrozen(PerformanceObserver.supportedEntryTypes): true PerformanceObserver.supportedEntryTypes === PerformanceObserver.supportedEntryTypes: true diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 6f9784f60dc..d15c07921ff 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -282,6 +282,7 @@ PerformanceMeasure PerformanceNavigation PerformanceObserver PerformanceObserverEntryList +PerformanceResourceTiming PerformanceTiming PeriodicWave Plugin