diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/PerformanceTimeline/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/PerformanceTimeline/BUILD.gn index 4e348ee49fd..f4a1b657916 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/PerformanceTimeline/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/PerformanceTimeline/BUILD.gn @@ -4,5 +4,7 @@ source_set("PerformanceTimeline") { sources = [ "EntryTypes.cpp", "PerformanceEntry.cpp", + "PerformanceObserver.cpp", + "PerformanceObserverEntryList.cpp", ] } diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni index 6bcc8611dcf..0cba9441758 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni @@ -212,6 +212,8 @@ standard_idl_files = [ "//Userland/Libraries/LibWeb/MathML/MathMLElement.idl", "//Userland/Libraries/LibWeb/NavigationTiming/PerformanceTiming.idl", "//Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntry.idl", + "//Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.idl", + "//Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.idl", "//Userland/Libraries/LibWeb/RequestIdleCallback/IdleDeadline.idl", "//Userland/Libraries/LibWeb/ResizeObserver/ResizeObserver.idl", "//Userland/Libraries/LibWeb/Streams/ByteLengthQueuingStrategy.idl", diff --git a/Tests/LibWeb/Text/expected/PerformanceObserver/PerformanceObserver_basic.txt b/Tests/LibWeb/Text/expected/PerformanceObserver/PerformanceObserver_basic.txt new file mode 100644 index 00000000000..946e4a8dde6 --- /dev/null +++ b/Tests/LibWeb/Text/expected/PerformanceObserver/PerformanceObserver_basic.txt @@ -0,0 +1,28 @@ +TypeError: Must specify one of entryTypes or type +TypeError: Cannot specify type or buffered if entryTypes is specified +TypeError: Cannot specify type or buffered if entryTypes is specified +InvalidModificationError: Cannot change a PerformanceObserver from observing multiple types to observing a single type +observer === globalObserver: true +list instanceof PerformanceObserverEntryList: true +allEntries instanceof Array: true +allEntries.length === 3: true +allEntries[0] === startMark: true +allEntries[1] === endMark: true +allEntries[2] === measureMark: true +markEntries instanceof Array: true +markEntries.length === 2: true +markEntries[0] === startMark: true +markEntries[1] === endMark: true +measureEntries instanceof Array: true +measureEntries.length === 1: true +measureEntries[0] === measureMark: true +startEntries instanceof Array: true +startEntries.length === 1: true +startEntries[0] === startMark: true +endEntries instanceof Array: true +endEntries.length === 1: true +endEntries[0] === endMark: true +measureEntriesByName instanceof Array: true +measureEntriesByName.length === 1: true +measureEntriesByName[0] === measureMark: true +records.length === 0: true diff --git a/Tests/LibWeb/Text/input/PerformanceObserver/PerformanceObserver_basic.html b/Tests/LibWeb/Text/input/PerformanceObserver/PerformanceObserver_basic.html new file mode 100644 index 00000000000..e3d3d57e7e3 --- /dev/null +++ b/Tests/LibWeb/Text/input/PerformanceObserver/PerformanceObserver_basic.html @@ -0,0 +1,98 @@ + + diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index b2237256a57..60fdd97e4d6 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -509,6 +509,8 @@ set(SOURCES Painting/ViewportPaintable.cpp PerformanceTimeline/EntryTypes.cpp PerformanceTimeline/PerformanceEntry.cpp + PerformanceTimeline/PerformanceObserver.cpp + PerformanceTimeline/PerformanceObserverEntryList.cpp PermissionsPolicy/AutoplayAllowlist.cpp PixelUnits.cpp Platform/AudioCodecPlugin.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 82d042e40ae..22927f65187 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -543,6 +543,9 @@ struct LinearGradientData; namespace Web::PerformanceTimeline { class PerformanceEntry; +class PerformanceObserver; +class PerformanceObserverEntryList; +struct PerformanceObserverInit; } namespace Web::PermissionsPolicy { diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h index 3ac67b256cf..1caf816ce2a 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h @@ -41,6 +41,9 @@ public: // https://www.w3.org/TR/intersection-observer/#intersectionobserver-task-source IntersectionObserver, + // https://w3c.github.io/performance-timeline/#dfn-performance-timeline-task-source + PerformanceTimeline, + // Some elements, such as the HTMLMediaElement, must have a unique task source per instance. // Keep this field last, to serve as the base value of all unique task sources. UniqueTaskSourceStart, diff --git a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index d9561e15e23..8aa1cf47ecb 100644 --- a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -23,8 +23,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -35,11 +38,6 @@ namespace Web::HTML { WindowOrWorkerGlobalScopeMixin::~WindowOrWorkerGlobalScopeMixin() = default; -// 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) - void WindowOrWorkerGlobalScopeMixin::initialize(JS::Realm&) { #define __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(entry_type, cpp_class) \ @@ -58,6 +56,10 @@ void WindowOrWorkerGlobalScopeMixin::visit_edges(JS::Cell::Visitor& visitor) { for (auto& it : m_timers) visitor.visit(it.value); + for (auto& observer : m_registered_performance_observer_objects) + visitor.visit(observer); + for (auto& entry : m_performance_entry_buffer_map) + entry.value.visit_edges(visitor); } // https://html.spec.whatwg.org/multipage/webappapis.html#dom-origin @@ -301,11 +303,10 @@ PerformanceTimeline::PerformanceEntryTuple& WindowOrWorkerGlobalScopeMixin::rele } // https://www.w3.org/TR/performance-timeline/#dfn-queue-a-performanceentry -WebIDL::ExceptionOr WindowOrWorkerGlobalScopeMixin::queue_performance_entry(JS::NonnullGCPtr new_entry) +void WindowOrWorkerGlobalScopeMixin::queue_performance_entry(JS::NonnullGCPtr new_entry) { - auto& vm = new_entry->vm(); - - // FIXME: 1. Let interested observers be an initially empty set of PerformanceObserver objects. + // 1. Let interested observers be an initially empty set of PerformanceObserver objects. + Vector> interested_observers; // 2. Let entryType be newEntry’s entryType value. auto const& entry_type = new_entry->entry_type(); @@ -313,14 +314,31 @@ WebIDL::ExceptionOr WindowOrWorkerGlobalScopeMixin::queue_performance_entr // 3. Let relevantGlobal be newEntry's relevant global object. // NOTE: Already is `this`. - // FIXME: 4. For each registered performance observer regObs in relevantGlobal's list of registered performance observer - // objects: - // 1. If regObs's options list contains a PerformanceObserverInit options whose entryTypes member includes entryType - // or whose type member equals to entryType: - // 1. If should add entry with newEntry and options returns true, append regObs's observer to interested observers. + // 4. For each registered performance observer regObs in relevantGlobal's list of registered performance observer + // objects: + for (auto const& registered_observer : m_registered_performance_observer_objects) { + // 1. If regObs's options list contains a PerformanceObserverInit options whose entryTypes member includes entryType + // or whose type member equals to entryType: + auto iterator = registered_observer->options_list().find_if([&entry_type](PerformanceTimeline::PerformanceObserverInit const& entry) { + if (entry.entry_types.has_value()) + return entry.entry_types->contains_slow(String::from_utf8(entry_type).release_value_but_fixme_should_propagate_errors()); - // FIXME: 5. For each observer in interested observers: - // 1. Append newEntry to observer's observer buffer. + VERIFY(entry.type.has_value()); + return entry.type.value() == entry_type; + }); + + if (!iterator.is_end()) { + // 1. If should add entry with newEntry and options returns true, append regObs's observer to interested observers. + if (new_entry->should_add_entry(*iterator) == PerformanceTimeline::ShouldAddEntry::Yes) + interested_observers.append(registered_observer); + } + } + + // 5. For each observer in interested observers: + for (auto const& observer : interested_observers) { + // 1. Append newEntry to observer's observer buffer. + observer->append_to_observer_buffer({}, new_entry); + } // 6. Let tuple be the relevant performance entry tuple of entryType and relevantGlobal. auto& tuple = relevant_performance_entry_tuple(entry_type); @@ -334,10 +352,10 @@ WebIDL::ExceptionOr WindowOrWorkerGlobalScopeMixin::queue_performance_entr // 9. If isBufferFull is false and shouldAdd is true, append newEntry to tuple's performance entry buffer. if (!is_buffer_full && should_add == PerformanceTimeline::ShouldAddEntry::Yes) - TRY_OR_THROW_OOM(vm, tuple.performance_entry_buffer.try_append(JS::make_handle(new_entry))); + tuple.performance_entry_buffer.append(new_entry); - // FIXME: 10. Queue the PerformanceObserver task with relevantGlobal as input. - return {}; + // 10. Queue the PerformanceObserver task with relevantGlobal as input. + queue_the_performance_observer_task(); } void WindowOrWorkerGlobalScopeMixin::clear_performance_entry_buffer(Badge, FlyString const& entry_type) @@ -354,35 +372,6 @@ void WindowOrWorkerGlobalScopeMixin::remove_entries_from_performance_entry_buffe }); } -// https://www.w3.org/TR/performance-timeline/#dfn-filter-buffer-by-name-and-type -static ErrorOr>> filter_buffer_by_name_and_type(Vector> const& buffer, Optional name, Optional type) -{ - // 1. Let result be an initially empty list. - Vector> result; - - // 2. For each PerformanceEntry entry in buffer, run the following steps: - for (auto const& entry : buffer) { - // 1. If type is not null and if type is not identical to entry's entryType attribute, continue to next entry. - if (type.has_value() && type.value() != entry->entry_type()) - continue; - - // 2. If name is not null and if name is not identical to entry's name attribute, continue to next entry. - if (name.has_value() && name.value() != entry->name()) - continue; - - // 3. append entry to result. - TRY(result.try_append(entry)); - } - - // 3. Sort results's entries in chronological order with respect to startTime - quick_sort(result, [](auto const& left_entry, auto const& right_entry) { - return left_entry->start_time() < right_entry->start_time(); - }); - - // 4. Return result. - return result; -} - // https://www.w3.org/TR/performance-timeline/#dfn-filter-buffer-map-by-name-and-type ErrorOr>> WindowOrWorkerGlobalScopeMixin::filter_buffer_map_by_name_and_type(Optional name, Optional type) const { @@ -431,4 +420,111 @@ ErrorOr>> WindowOrWorke return result; } +void WindowOrWorkerGlobalScopeMixin::register_performance_observer(Badge, JS::NonnullGCPtr observer) +{ + m_registered_performance_observer_objects.set(observer, AK::HashSetExistingEntryBehavior::Keep); +} + +void WindowOrWorkerGlobalScopeMixin::unregister_performance_observer(Badge, JS::NonnullGCPtr observer) +{ + m_registered_performance_observer_objects.remove(observer); +} + +bool WindowOrWorkerGlobalScopeMixin::has_registered_performance_observer(JS::NonnullGCPtr observer) +{ + return m_registered_performance_observer_objects.contains(observer); +} + +// https://w3c.github.io/performance-timeline/#dfn-queue-the-performanceobserver-task +void WindowOrWorkerGlobalScopeMixin::queue_the_performance_observer_task() +{ + // 1. If relevantGlobal's performance observer task queued flag is set, terminate these steps. + if (m_performance_observer_task_queued) + return; + + // 2. Set relevantGlobal's performance observer task queued flag. + m_performance_observer_task_queued = true; + + // 3. Queue a task that consists of running the following substeps. The task source for the queued task is the performance + // timeline task source. + queue_global_task(Task::Source::PerformanceTimeline, this_impl(), [this]() { + auto& realm = this_impl().realm(); + + // 1. Unset performance observer task queued flag of relevantGlobal. + m_performance_observer_task_queued = false; + + // 2. Let notifyList be a copy of relevantGlobal's list of registered performance observer objects. + auto notify_list = m_registered_performance_observer_objects; + + // 3. For each registered performance observer object registeredObserver in notifyList, run these steps: + for (auto& registered_observer : notify_list) { + // 1. Let po be registeredObserver's observer. + // 2. Let entries be a copy of po’s observer buffer. + // 4. Empty po’s observer buffer. + auto entries = registered_observer->take_records(); + + // 3. If entries is empty, return. + // FIXME: Do they mean `continue`? + if (entries.is_empty()) + continue; + + Vector> entries_as_gc_ptrs; + for (auto& entry : entries) + entries_as_gc_ptrs.append(*entry); + + // 5. Let observerEntryList be a new PerformanceObserverEntryList, with its entry list set to entries. + auto observer_entry_list = realm.heap().allocate(realm, realm, move(entries_as_gc_ptrs)); + + // 6. Let droppedEntriesCount be null. + Optional dropped_entries_count; + + // 7. If po's requires dropped entries is set, perform the following steps: + if (registered_observer->requires_dropped_entries()) { + // 1. Set droppedEntriesCount to 0. + dropped_entries_count = 0; + + // 2. For each PerformanceObserverInit item in registeredObserver's options list: + for (auto const& item : registered_observer->options_list()) { + // 1. For each DOMString entryType that appears either as item's type or in item's entryTypes: + auto increment_dropped_entries_count = [this, &dropped_entries_count](FlyString const& type) { + // 1. Let map be relevantGlobal's performance entry buffer map. + auto const& map = m_performance_entry_buffer_map; + + // 2. Let tuple be the result of getting the value of entry on map given entryType as key. + auto const& tuple = map.get(type); + VERIFY(tuple.has_value()); + + // 3. Increase droppedEntriesCount by tuple's dropped entries count. + dropped_entries_count.value() += tuple->dropped_entries_count; + }; + + if (item.type.has_value()) { + increment_dropped_entries_count(item.type.value()); + } else { + VERIFY(item.entry_types.has_value()); + for (auto const& type : item.entry_types.value()) + increment_dropped_entries_count(type); + } + } + + // 3. Set po's requires dropped entries to false. + registered_observer->unset_requires_dropped_entries({}); + } + + // 8. Let callbackOptions be a PerformanceObserverCallbackOptions with its droppedEntriesCount set to + // droppedEntriesCount if droppedEntriesCount is not null, otherwise unset. + auto callback_options = JS::Object::create(realm, realm.intrinsics().object_prototype()); + if (dropped_entries_count.has_value()) + MUST(callback_options->create_data_property("droppedEntriesCount", JS::Value(dropped_entries_count.value()))); + + // 9. Call po’s observer callback with observerEntryList as the first argument, with po as the second + // argument and as callback this value, and with callbackOptions as the third argument. + // If this throws an exception, report the exception. + auto completion = WebIDL::invoke_callback(registered_observer->callback(), registered_observer, observer_entry_list, registered_observer, callback_options); + if (completion.is_abrupt()) + HTML::report_exception(completion, realm); + } + }); +} + } diff --git a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h index 08211eda9be..086a6409c40 100644 --- a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h +++ b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h @@ -48,12 +48,18 @@ public: void clear_interval(i32); PerformanceTimeline::PerformanceEntryTuple& relevant_performance_entry_tuple(FlyString const& entry_type); - WebIDL::ExceptionOr queue_performance_entry(JS::NonnullGCPtr new_entry); + void queue_performance_entry(JS::NonnullGCPtr new_entry); void clear_performance_entry_buffer(Badge, FlyString const& entry_type); void remove_entries_from_performance_entry_buffer(Badge, FlyString const& entry_type, String entry_name); ErrorOr>> filter_buffer_map_by_name_and_type(Optional name, Optional type) const; + void register_performance_observer(Badge, JS::NonnullGCPtr); + void unregister_performance_observer(Badge, JS::NonnullGCPtr); + bool has_registered_performance_observer(JS::NonnullGCPtr); + + void queue_the_performance_observer_task(); + protected: void initialize(JS::Realm&); void visit_edges(JS::Cell::Visitor&); @@ -70,8 +76,11 @@ private: // https://www.w3.org/TR/performance-timeline/#performance-timeline // Each global object has: - // FIXME: - a performance observer task queued flag - // FIXME: - a list of registered performance observer objects that is initially empty + // - a performance observer task queued flag + bool m_performance_observer_task_queued { false }; + + // - a list of registered performance observer objects that is initially empty + OrderedHashTable> m_registered_performance_observer_objects; // https://www.w3.org/TR/performance-timeline/#dfn-performance-entry-buffer-map // a performance entry buffer map map, keyed on a DOMString, representing the entry type to which the buffer belongs. The map's value is the following tuple: diff --git a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp index f1fad3b3946..5ccdbc5e121 100644 --- a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp +++ b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp @@ -63,7 +63,7 @@ WebIDL::ExceptionOr> Performance:: // 2. Queue entry. auto* window_or_worker = dynamic_cast(&realm.global_object()); VERIFY(window_or_worker); - TRY(window_or_worker->queue_performance_entry(entry)); + window_or_worker->queue_performance_entry(entry); // 3. Add entry to the performance entry buffer. // FIXME: This seems to be a holdover from moving to the `queue` structure for PerformanceObserver, as this would cause a double append. @@ -293,7 +293,7 @@ WebIDL::ExceptionOr> Performanc auto entry = realm.heap().allocate(realm, realm, measure_name, start_time, duration, detail); // 10. Queue entry. - TRY(window_or_worker->queue_performance_entry(entry)); + window_or_worker->queue_performance_entry(entry); // 11. Add entry to the performance entry buffer. // FIXME: This seems to be a holdover from moving to the `queue` structure for PerformanceObserver, as this would cause a double append. diff --git a/Userland/Libraries/LibWeb/HighResolutionTime/SupportedPerformanceTypes.h b/Userland/Libraries/LibWeb/HighResolutionTime/SupportedPerformanceTypes.h new file mode 100644 index 00000000000..55e02ccf410 --- /dev/null +++ b/Userland/Libraries/LibWeb/HighResolutionTime/SupportedPerformanceTypes.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +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) + +} diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntry.h b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntry.h index 7a3572816f8..297fc1682ae 100644 --- a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntry.h +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntry.h @@ -36,7 +36,7 @@ public: HighResolutionTime::DOMHighResTimeStamp duration() const { return m_duration; } // https://w3c.github.io/timing-entrytypes-registry/#dfn-should-add-entry - virtual PerformanceTimeline::ShouldAddEntry should_add_entry() const = 0; + virtual PerformanceTimeline::ShouldAddEntry should_add_entry(Optional = {}) const = 0; protected: PerformanceEntry(JS::Realm&, String const& name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration); diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntryTuple.h b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntryTuple.h index 938cd6bf914..18d86fa9571 100644 --- a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntryTuple.h +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceEntryTuple.h @@ -14,7 +14,7 @@ namespace Web::PerformanceTimeline { struct PerformanceEntryTuple { // https://www.w3.org/TR/performance-timeline/#dfn-performance-entry-buffer // A performance entry buffer to store PerformanceEntry objects, that is initially empty. - Vector> performance_entry_buffer; + Vector> performance_entry_buffer; // https://www.w3.org/TR/performance-timeline/#dfn-maxbuffersize // An integer maxBufferSize, initialized to the registry value for this entry type. @@ -45,6 +45,12 @@ struct PerformanceEntryTuple { // 4. Return true. return true; } + + void visit_edges(JS::Cell::Visitor& visitor) + { + for (auto& entry : performance_entry_buffer) + visitor.visit(entry); + } }; } diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.cpp b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.cpp new file mode 100644 index 00000000000..221c6475d57 --- /dev/null +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.cpp @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::PerformanceTimeline { + +WebIDL::ExceptionOr> PerformanceObserver::construct_impl(JS::Realm& realm, JS::GCPtr callback) +{ + return realm.heap().allocate(realm, realm, callback); +} + +PerformanceObserver::PerformanceObserver(JS::Realm& realm, JS::GCPtr callback) + : Bindings::PlatformObject(realm) + , m_callback(move(callback)) +{ +} + +PerformanceObserver::~PerformanceObserver() = default; + +void PerformanceObserver::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + set_prototype(&Bindings::ensure_web_prototype(realm, "PerformanceObserver")); +} + +void PerformanceObserver::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_callback.ptr()); + for (auto& entry : m_observer_buffer) + visitor.visit(entry); +} + +// https://w3c.github.io/performance-timeline/#dom-performanceobserver-observe +WebIDL::ExceptionOr PerformanceObserver::observe(PerformanceObserverInit& options) +{ + auto& realm = this->realm(); + + // 1. Let relevantGlobal be this's relevant global object. + auto* relevant_global = dynamic_cast(&HTML::relevant_global_object(*this)); + VERIFY(relevant_global); + + // 2. If options's entryTypes and type members are both omitted, then throw a "TypeError". + if (!options.entry_types.has_value() && !options.type.has_value()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Must specify one of entryTypes or type"sv }; + + // 3. If options's entryTypes is present and any other member is also present, then throw a "TypeError". + if (options.entry_types.has_value() && (options.type.has_value() || options.buffered.has_value())) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Cannot specify type or buffered if entryTypes is specified"sv }; + + // 4. Update or check this's observer type by running these steps: + // 1. If this's observer type is "undefined": + if (m_observer_type == ObserverType::Undefined) { + // 1. If options's entryTypes member is present, then set this's observer type to "multiple". + if (options.entry_types.has_value()) + m_observer_type = ObserverType::Multiple; + + // 2. If options's type member is present, then set this's observer type to "single". + if (options.type.has_value()) + m_observer_type = ObserverType::Single; + } + // 2. If this's observer type is "single" and options's entryTypes member is present, then throw an "InvalidModificationError". + else if (m_observer_type == ObserverType::Single) { + if (options.entry_types.has_value()) + return WebIDL::InvalidModificationError::create(realm, "Cannot change a PerformanceObserver from observing a single type to observing multiple types"sv); + } + // 3. If this's observer type is "multiple" and options's type member is present, then throw an "InvalidModificationError". + else if (m_observer_type == ObserverType::Multiple) { + if (options.type.has_value()) + return WebIDL::InvalidModificationError::create(realm, "Cannot change a PerformanceObserver from observing multiple types to observing a single type"sv); + } + + // 5. Set this's requires dropped entries to true. + m_requires_dropped_entries = true; + + // 6. If this's observer type is "multiple", run the following steps: + if (m_observer_type == ObserverType::Multiple) { + // 1. Let entry types be options's entryTypes sequence. + VERIFY(options.entry_types.has_value()); + auto& entry_types = options.entry_types.value(); + + // 2. Remove all types from entry types that are not contained in relevantGlobal's frozen array of supported entry types. + // The user agent SHOULD notify developers if entry types is modified. For example, a console warning listing removed + // types might be appropriate. + entry_types.remove_all_matching([](String const& type) { +#define __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(entry_type, cpp_class) \ + if (entry_type == type) \ + return false; + ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES +#undef __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES + + dbgln("Potential FIXME: Removing unsupported PerformanceEntry type '{}' from list of observed types in PerformanceObserver::observe()", type); + return true; + }); + + // 3. If the resulting entry types sequence is an empty sequence, abort these steps. + // The user agent SHOULD notify developers when the steps are aborted to notify that registration has been aborted. + // For example, a console warning might be appropriate. + if (entry_types.is_empty()) { + dbgln("Potential FIXME: Returning from PerformanceObserver::observe() as we don't support any of the specified types (or none was specified)."); + return {}; + } + + // 4. If the list of registered performance observer objects of relevantGlobal contains a registered performance + // observer whose observer is this, replace its options list with a list containing options as its only item. + // 5. Otherwise, create and append a registered performance observer object to the list of registered performance + // observer objects of relevantGlobal, with observer set to this and options list set to a list containing + // options as its only item. + // NOTE: See the comment on PerformanceObserver::options_list about why this doesn't create a separate registered + // performance observer object. + m_options_list.clear(); + m_options_list.append(options); + relevant_global->register_performance_observer({}, *this); + } + // 7. Otherwise, run the following steps: + else { + // 1. Assert that this's observer type is "single". + VERIFY(m_observer_type == ObserverType::Single); + + // 2. If options's type is not contained in the relevantGlobal's frozen array of supported entry types, abort these steps. + // The user agent SHOULD notify developers when this happens, for instance via a console warning. + VERIFY(options.type.has_value()); + auto& type = options.type.value(); + bool recognized_type = false; + +#define __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES(entry_type, cpp_class) \ + if (!recognized_type && entry_type == type) \ + recognized_type = true; + ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES +#undef __ENUMERATE_SUPPORTED_PERFORMANCE_ENTRY_TYPES + + if (!recognized_type) { + dbgln("Potential FIXME: Returning from PerformanceObserver::observe() as we don't support the PerformanceEntry type '{}'", type); + return {}; + } + + // 3. If the list of registered performance observer objects of relevantGlobal contains a registered performance + // observer obs whose observer is this: + if (relevant_global->has_registered_performance_observer(*this)) { + // 1. If obs's options list contains a PerformanceObserverInit item currentOptions whose type is equal to options's type, + // replace currentOptions with options in obs's options list. + auto index = m_options_list.find_first_index_if([&options](PerformanceObserverInit const& entry) { + return entry.type == options.type; + }); + if (index.has_value()) { + m_options_list[index.value()] = options; + } else { + // Otherwise, append options to obs's options list. + m_options_list.append(options); + } + } + // 4. Otherwise, create and append a registered performance observer object to the list of registered performance + // observer objects of relevantGlobal, with observer set to the this and options list set to a list containing + // options as its only item. + else { + m_options_list.clear(); + m_options_list.append(options); + relevant_global->register_performance_observer({}, *this); + } + + // 5. If options's buffered flag is set: + if (options.buffered.has_value() && options.buffered.value()) { + // 1. Let tuple be the relevant performance entry tuple of options's type and relevantGlobal. + auto const& tuple = relevant_global->relevant_performance_entry_tuple(type); + + // 2. For each entry in tuple's performance entry buffer: + for (auto const& entry : tuple.performance_entry_buffer) { + // 1. If should add entry with entry and options as parameters returns true, append entry to the observer buffer. + if (entry->should_add_entry(options) == ShouldAddEntry::Yes) + m_observer_buffer.append(*entry); + } + + // 3. Queue the PerformanceObserver task with relevantGlobal as input. + relevant_global->queue_the_performance_observer_task(); + } + } + + return {}; +} + +// https://w3c.github.io/performance-timeline/#dom-performanceobserver-disconnect +void PerformanceObserver::disconnect() +{ + // 1. Remove this from the list of registered performance observer objects of relevant global object. + auto* relevant_global = dynamic_cast(&HTML::relevant_global_object(*this)); + VERIFY(relevant_global); + relevant_global->unregister_performance_observer({}, *this); + + // 2. Empty this's observer buffer. + m_observer_buffer.clear(); + + // 3. Empty this's options list. + m_options_list.clear(); +} + +// https://w3c.github.io/performance-timeline/#dom-performanceobserver-takerecords +Vector> PerformanceObserver::take_records() +{ + // The takeRecords() method must return a copy of this's observer buffer, and also empty this's observer buffer. + Vector> records; + for (auto& record : m_observer_buffer) + records.append(*record); + m_observer_buffer.clear(); + return records; +} + +void PerformanceObserver::unset_requires_dropped_entries(Badge) +{ + m_requires_dropped_entries = false; +} + +void PerformanceObserver::append_to_observer_buffer(Badge, JS::NonnullGCPtr entry) +{ + m_observer_buffer.append(entry); +} + +} diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.h b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.h new file mode 100644 index 00000000000..c1a0a66b76f --- /dev/null +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::PerformanceTimeline { + +// https://w3c.github.io/performance-timeline/#dom-performanceobserverinit +struct PerformanceObserverInit { + Optional> entry_types; + Optional type; + Optional buffered; +}; + +// https://w3c.github.io/performance-timeline/#dom-performanceobserver +class PerformanceObserver final : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(PerformanceObserver, Bindings::PlatformObject); + +public: + enum class ObserverType { + Undefined, + Single, + Multiple, + }; + + static WebIDL::ExceptionOr> construct_impl(JS::Realm&, JS::GCPtr); + virtual ~PerformanceObserver() override; + + WebIDL::ExceptionOr observe(PerformanceObserverInit& options); + void disconnect(); + Vector> take_records(); + + bool requires_dropped_entries() const { return m_requires_dropped_entries; } + void unset_requires_dropped_entries(Badge); + + Vector const& options_list() const { return m_options_list; } + + WebIDL::CallbackType& callback() { return *m_callback; } + + void append_to_observer_buffer(Badge, JS::NonnullGCPtr); + +private: + PerformanceObserver(JS::Realm&, JS::GCPtr); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + // https://w3c.github.io/performance-timeline/#dfn-observer-callback + // A PerformanceObserverCallback observer callback set on creation. + JS::GCPtr m_callback; + + // https://w3c.github.io/performance-timeline/#dfn-observer-buffer + // A PerformanceEntryList object called the observer buffer that is initially empty. + Vector> m_observer_buffer; + + // https://w3c.github.io/performance-timeline/#dfn-observer-type + // A DOMString observer type which is initially "undefined". + ObserverType m_observer_type { ObserverType::Undefined }; + + // https://w3c.github.io/performance-timeline/#dfn-requires-dropped-entries + // A boolean requires dropped entries which is initially set to false. + bool m_requires_dropped_entries { false }; + + // https://w3c.github.io/performance-timeline/#dfn-options-list + // A registered performance observer is a struct consisting of an observer member (a PerformanceObserver object) + // and an options list member (a list of PerformanceObserverInit dictionaries). + // NOTE: This doesn't use a separate struct as methods such as disconnect() assume it can access an options list from `this`: a PerformanceObserver. + Vector m_options_list; +}; + +} diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.idl b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.idl new file mode 100644 index 00000000000..1ccb4df0781 --- /dev/null +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserver.idl @@ -0,0 +1,25 @@ +#import + +// https://w3c.github.io/performance-timeline/#dom-performanceobservercallbackoptions +dictionary PerformanceObserverCallbackOptions { + unsigned long long droppedEntriesCount; +}; + +callback PerformanceObserverCallback = undefined (PerformanceObserverEntryList entries, PerformanceObserver observer, optional PerformanceObserverCallbackOptions options = {}); + +// https://w3c.github.io/performance-timeline/#dom-performanceobserverinit +dictionary PerformanceObserverInit { + sequence entryTypes; + DOMString type; + boolean buffered; +}; + +// https://w3c.github.io/performance-timeline/#dom-performanceobserver +[Exposed=(Window,Worker), UseNewAKString] +interface PerformanceObserver { + constructor(PerformanceObserverCallback callback); + undefined observe(optional PerformanceObserverInit options = {}); + undefined disconnect(); + PerformanceEntryList takeRecords(); + //[SameObject] static readonly attribute sequence supportedEntryTypes; +}; diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.cpp b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.cpp new file mode 100644 index 00000000000..a78e6613564 --- /dev/null +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::PerformanceTimeline { + +PerformanceObserverEntryList::PerformanceObserverEntryList(JS::Realm& realm, Vector>&& entry_list) + : Bindings::PlatformObject(realm) + , m_entry_list(move(entry_list)) +{ +} + +PerformanceObserverEntryList::~PerformanceObserverEntryList() = default; + +void PerformanceObserverEntryList::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + set_prototype(&Bindings::ensure_web_prototype(realm, "PerformanceObserverEntryList")); +} + +void PerformanceObserverEntryList::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + for (auto& entry : m_entry_list) + visitor.visit(entry); +} + +// https://www.w3.org/TR/performance-timeline/#dfn-filter-buffer-by-name-and-type +ErrorOr>> filter_buffer_by_name_and_type(Vector> const& buffer, Optional name, Optional type) +{ + // 1. Let result be an initially empty list. + Vector> result; + + // 2. For each PerformanceEntry entry in buffer, run the following steps: + for (auto const& entry : buffer) { + // 1. If type is not null and if type is not identical to entry's entryType attribute, continue to next entry. + if (type.has_value() && type.value() != entry->entry_type()) + continue; + + // 2. If name is not null and if name is not identical to entry's name attribute, continue to next entry. + if (name.has_value() && name.value() != entry->name()) + continue; + + // 3. append entry to result. + TRY(result.try_append(entry)); + } + + // 3. Sort results's entries in chronological order with respect to startTime + quick_sort(result, [](auto const& left_entry, auto const& right_entry) { + return left_entry->start_time() < right_entry->start_time(); + }); + + // 4. Return result. + return result; +} + +// https://w3c.github.io/performance-timeline/#dom-performanceobserverentrylist-getentries +WebIDL::ExceptionOr>> PerformanceObserverEntryList::get_entries() const +{ + // Returns a PerformanceEntryList object returned by filter buffer by name and type algorithm with this's entry list, + // name and type set to null. + return TRY_OR_THROW_OOM(vm(), filter_buffer_by_name_and_type(m_entry_list, /* name= */ Optional {}, /* type= */ Optional {})); +} + +// https://w3c.github.io/performance-timeline/#dom-performanceobserverentrylist-getentriesbytype +WebIDL::ExceptionOr>> PerformanceObserverEntryList::get_entries_by_type(String const& type) const +{ + // Returns a PerformanceEntryList object returned by filter buffer by name and type algorithm with this's entry list, + // name set to null, and type set to the method's input type parameter. + return TRY_OR_THROW_OOM(vm(), filter_buffer_by_name_and_type(m_entry_list, /* name= */ Optional {}, type)); +} + +// https://w3c.github.io/performance-timeline/#dom-performanceobserverentrylist-getentriesbyname +WebIDL::ExceptionOr>> PerformanceObserverEntryList::get_entries_by_name(String const& name, Optional type) const +{ + // Returns a PerformanceEntryList object returned by filter buffer by name and type algorithm with this's entry list, + // name set to the method input name parameter, and type set to null if optional entryType is omitted, or set to the + // method's input type parameter otherwise. + return TRY_OR_THROW_OOM(vm(), filter_buffer_by_name_and_type(m_entry_list, name, type)); +} + +} diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.h b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.h new file mode 100644 index 00000000000..04a9194a6a9 --- /dev/null +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::PerformanceTimeline { + +// https://w3c.github.io/performance-timeline/#performanceobserverentrylist-interface +class PerformanceObserverEntryList final : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(PerformanceObserverEntryList, Bindings::PlatformObject); + +public: + virtual ~PerformanceObserverEntryList() override; + + 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; + +private: + PerformanceObserverEntryList(JS::Realm&, Vector>&&); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + // https://w3c.github.io/performance-timeline/#dfn-entry-list + // Returns a PerformanceEntryList object returned by filter buffer by name and type algorithm with this's entry list, + // name and type set to null. + Vector> m_entry_list; +}; + +ErrorOr>> filter_buffer_by_name_and_type(Vector> const& buffer, Optional name, Optional type); + +} diff --git a/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.idl b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.idl new file mode 100644 index 00000000000..772b26ccf7f --- /dev/null +++ b/Userland/Libraries/LibWeb/PerformanceTimeline/PerformanceObserverEntryList.idl @@ -0,0 +1,9 @@ +#import + +// https://w3c.github.io/performance-timeline/#performanceobserverentrylist-interface +[Exposed=(Window,Worker), UseNewAKString] +interface PerformanceObserverEntryList { + PerformanceEntryList getEntries(); + PerformanceEntryList getEntriesByType(DOMString type); + PerformanceEntryList getEntriesByName(DOMString name, optional DOMString type); +}; diff --git a/Userland/Libraries/LibWeb/UserTiming/PerformanceMark.h b/Userland/Libraries/LibWeb/UserTiming/PerformanceMark.h index fd79baa36e9..0013546ee01 100644 --- a/Userland/Libraries/LibWeb/UserTiming/PerformanceMark.h +++ b/Userland/Libraries/LibWeb/UserTiming/PerformanceMark.h @@ -36,7 +36,7 @@ public: static Optional max_buffer_size() { return OptionalNone {}; } // https://w3c.github.io/timing-entrytypes-registry/#dfn-should-add-entry - virtual PerformanceTimeline::ShouldAddEntry should_add_entry() const override { return PerformanceTimeline::ShouldAddEntry::Yes; } + virtual PerformanceTimeline::ShouldAddEntry should_add_entry(Optional = {}) const override { return PerformanceTimeline::ShouldAddEntry::Yes; } virtual FlyString const& entry_type() const override; diff --git a/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.h b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.h index 9c324693ef7..a8cd45e3ad7 100644 --- a/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.h +++ b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.h @@ -38,7 +38,7 @@ public: static Optional max_buffer_size() { return OptionalNone {}; } // https://w3c.github.io/timing-entrytypes-registry/#dfn-should-add-entry - virtual PerformanceTimeline::ShouldAddEntry should_add_entry() const override { return PerformanceTimeline::ShouldAddEntry::Yes; } + virtual PerformanceTimeline::ShouldAddEntry should_add_entry(Optional = {}) const override { return PerformanceTimeline::ShouldAddEntry::Yes; } virtual FlyString const& entry_type() const override; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index b8068a70246..839f9db8eb0 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -199,6 +199,8 @@ libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry) libweb_js_bindings(MathML/MathMLElement) libweb_js_bindings(NavigationTiming/PerformanceTiming) libweb_js_bindings(PerformanceTimeline/PerformanceEntry) +libweb_js_bindings(PerformanceTimeline/PerformanceObserver) +libweb_js_bindings(PerformanceTimeline/PerformanceObserverEntryList) libweb_js_bindings(RequestIdleCallback/IdleDeadline) libweb_js_bindings(ResizeObserver/ResizeObserver) libweb_js_bindings(Streams/ByteLengthQueuingStrategy)