LibWeb: Implement PerformanceObserver

This commit is contained in:
Luke Wilde 2023-08-25 01:26:47 +01:00 committed by Andreas Kling
parent 5055883b9f
commit af2886449a
Notes: sideshowbarker 2024-07-16 23:05:02 +09:00
22 changed files with 793 additions and 57 deletions

View file

@ -4,5 +4,7 @@ source_set("PerformanceTimeline") {
sources = [
"EntryTypes.cpp",
"PerformanceEntry.cpp",
"PerformanceObserver.cpp",
"PerformanceObserverEntryList.cpp",
]
}

View file

@ -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",

View file

@ -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

View file

@ -0,0 +1,98 @@
<script src="../include.js"></script>
<script>
const bufferedMessages = [];
function printlnBuffered(message) {
bufferedMessages.push(message);
}
const globalObserver = new PerformanceObserver((list, observer) => {
printlnBuffered(`observer === globalObserver: ${observer === globalObserver}`);
printlnBuffered(
`list instanceof PerformanceObserverEntryList: ${
list instanceof PerformanceObserverEntryList
}`
);
const allEntries = list.getEntries();
printlnBuffered(`allEntries instanceof Array: ${allEntries instanceof Array}`);
printlnBuffered(`allEntries.length === 3: ${allEntries.length === 3}`);
printlnBuffered(`allEntries[0] === startMark: ${allEntries[0] === startMark}`);
printlnBuffered(`allEntries[1] === endMark: ${allEntries[1] === endMark}`);
printlnBuffered(`allEntries[2] === measureMark: ${allEntries[2] === measureMark}`);
const markEntries = list.getEntriesByType("mark");
printlnBuffered(`markEntries instanceof Array: ${markEntries instanceof Array}`);
printlnBuffered(`markEntries.length === 2: ${markEntries.length === 2}`);
printlnBuffered(`markEntries[0] === startMark: ${markEntries[0] === startMark}`);
printlnBuffered(`markEntries[1] === endMark: ${markEntries[1] === endMark}`);
const measureEntries = list.getEntriesByType("measure");
printlnBuffered(`measureEntries instanceof Array: ${measureEntries instanceof Array}`);
printlnBuffered(`measureEntries.length === 1: ${measureEntries.length === 1}`);
printlnBuffered(`measureEntries[0] === measureMark: ${measureEntries[0] === measureMark}`);
const startEntries = list.getEntriesByName("start");
printlnBuffered(`startEntries instanceof Array: ${startEntries instanceof Array}`);
printlnBuffered(`startEntries.length === 1: ${startEntries.length === 1}`);
printlnBuffered(`startEntries[0] === startMark: ${startEntries[0] === startMark}`);
const endEntries = list.getEntriesByName("end");
printlnBuffered(`endEntries instanceof Array: ${endEntries instanceof Array}`);
printlnBuffered(`endEntries.length === 1: ${endEntries.length === 1}`);
printlnBuffered(`endEntries[0] === endMark: ${endEntries[0] === endMark}`);
const measureEntriesByName = list.getEntriesByName("measure");
printlnBuffered(
`measureEntriesByName instanceof Array: ${measureEntriesByName instanceof Array}`
);
printlnBuffered(`measureEntriesByName.length === 1: ${measureEntriesByName.length === 1}`);
printlnBuffered(
`measureEntriesByName[0] === measureMark: ${measureEntriesByName[0] === measureMark}`
);
});
globalObserver.observe({ entryTypes: ["measure", "mark"] });
const startMark = performance.mark("start");
const endMark = performance.mark("end");
const measureMark = performance.measure("measure", "start", "end");
function printCatchedException(func) {
try {
func();
} catch (e) {
if (e instanceof DOMException) {
printlnBuffered(`${e.name}: ${e.message}`);
} else {
printlnBuffered(e.toString());
}
}
}
printCatchedException(() => {
globalObserver.observe();
});
printCatchedException(() => {
globalObserver.observe({ entryTypes: [], buffered: true });
});
printCatchedException(() => {
globalObserver.observe({ entryTypes: [], type: "" });
});
printCatchedException(() => {
globalObserver.observe({ type: "" });
});
test(() => {
for (const message of bufferedMessages) {
println(message);
}
globalObserver.disconnect();
performance.mark("bad");
performance.measure("badmeasure", "end", "bad");
const records = globalObserver.takeRecords();
println(`records.length === 0: ${records.length === 0}`);
});
</script>

View file

@ -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

View file

@ -543,6 +543,9 @@ struct LinearGradientData;
namespace Web::PerformanceTimeline {
class PerformanceEntry;
class PerformanceObserver;
class PerformanceObserverEntryList;
struct PerformanceObserverInit;
}
namespace Web::PermissionsPolicy {

View file

@ -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,

View file

@ -23,8 +23,11 @@
#include <LibWeb/HTML/Timer.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
#include <LibWeb/HighResolutionTime/SupportedPerformanceTypes.h>
#include <LibWeb/Infra/Base64.h>
#include <LibWeb/PerformanceTimeline/EntryTypes.h>
#include <LibWeb/PerformanceTimeline/PerformanceObserver.h>
#include <LibWeb/PerformanceTimeline/PerformanceObserverEntryList.h>
#include <LibWeb/UserTiming/PerformanceMark.h>
#include <LibWeb/UserTiming/PerformanceMeasure.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
@ -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<void> WindowOrWorkerGlobalScopeMixin::queue_performance_entry(JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry> new_entry)
void WindowOrWorkerGlobalScopeMixin::queue_performance_entry(JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry> 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<JS::Handle<PerformanceTimeline::PerformanceObserver>> interested_observers;
// 2. Let entryType be newEntrys entryType value.
auto const& entry_type = new_entry->entry_type();
@ -313,14 +314,31 @@ WebIDL::ExceptionOr<void> 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<void> 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<HighResolutionTime::Performance>, 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<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> filter_buffer_by_name_and_type(Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>> const& buffer, Optional<String> name, Optional<String> type)
{
// 1. Let result be an initially empty list.
Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>> 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<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> WindowOrWorkerGlobalScopeMixin::filter_buffer_map_by_name_and_type(Optional<String> name, Optional<String> type) const
{
@ -431,4 +420,111 @@ ErrorOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> WindowOrWorke
return result;
}
void WindowOrWorkerGlobalScopeMixin::register_performance_observer(Badge<PerformanceTimeline::PerformanceObserver>, JS::NonnullGCPtr<PerformanceTimeline::PerformanceObserver> observer)
{
m_registered_performance_observer_objects.set(observer, AK::HashSetExistingEntryBehavior::Keep);
}
void WindowOrWorkerGlobalScopeMixin::unregister_performance_observer(Badge<PerformanceTimeline::PerformanceObserver>, JS::NonnullGCPtr<PerformanceTimeline::PerformanceObserver> observer)
{
m_registered_performance_observer_objects.remove(observer);
}
bool WindowOrWorkerGlobalScopeMixin::has_registered_performance_observer(JS::NonnullGCPtr<PerformanceTimeline::PerformanceObserver> 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 pos observer buffer.
// 4. Empty pos 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<JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>> 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<PerformanceTimeline::PerformanceObserverEntryList>(realm, realm, move(entries_as_gc_ptrs));
// 6. Let droppedEntriesCount be null.
Optional<u64> 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 pos 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);
}
});
}
}

View file

@ -48,12 +48,18 @@ public:
void clear_interval(i32);
PerformanceTimeline::PerformanceEntryTuple& relevant_performance_entry_tuple(FlyString const& entry_type);
WebIDL::ExceptionOr<void> queue_performance_entry(JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry> new_entry);
void queue_performance_entry(JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry> new_entry);
void clear_performance_entry_buffer(Badge<HighResolutionTime::Performance>, FlyString const& entry_type);
void remove_entries_from_performance_entry_buffer(Badge<HighResolutionTime::Performance>, FlyString const& entry_type, String entry_name);
ErrorOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> filter_buffer_map_by_name_and_type(Optional<String> name, Optional<String> type) const;
void register_performance_observer(Badge<PerformanceTimeline::PerformanceObserver>, JS::NonnullGCPtr<PerformanceTimeline::PerformanceObserver>);
void unregister_performance_observer(Badge<PerformanceTimeline::PerformanceObserver>, JS::NonnullGCPtr<PerformanceTimeline::PerformanceObserver>);
bool has_registered_performance_observer(JS::NonnullGCPtr<PerformanceTimeline::PerformanceObserver>);
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<JS::NonnullGCPtr<PerformanceTimeline::PerformanceObserver>> 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:

View file

@ -63,7 +63,7 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<UserTiming::PerformanceMark>> Performance::
// 2. Queue entry.
auto* window_or_worker = dynamic_cast<HTML::WindowOrWorkerGlobalScopeMixin*>(&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<JS::NonnullGCPtr<UserTiming::PerformanceMeasure>> Performanc
auto entry = realm.heap().allocate<UserTiming::PerformanceMeasure>(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.

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* 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)
}

View file

@ -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<PerformanceObserverInit const&> = {}) const = 0;
protected:
PerformanceEntry(JS::Realm&, String const& name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration);

View file

@ -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<JS::Handle<PerformanceEntry>> performance_entry_buffer;
Vector<JS::NonnullGCPtr<PerformanceEntry>> 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);
}
};
}

View file

@ -0,0 +1,229 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PerformanceObserverPrototype.h>
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
#include <LibWeb/HighResolutionTime/SupportedPerformanceTypes.h>
#include <LibWeb/PerformanceTimeline/EntryTypes.h>
#include <LibWeb/PerformanceTimeline/PerformanceEntry.h>
#include <LibWeb/PerformanceTimeline/PerformanceObserver.h>
#include <LibWeb/WebIDL/CallbackType.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::PerformanceTimeline {
WebIDL::ExceptionOr<JS::NonnullGCPtr<PerformanceObserver>> PerformanceObserver::construct_impl(JS::Realm& realm, JS::GCPtr<WebIDL::CallbackType> callback)
{
return realm.heap().allocate<PerformanceObserver>(realm, realm, callback);
}
PerformanceObserver::PerformanceObserver(JS::Realm& realm, JS::GCPtr<WebIDL::CallbackType> 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<Bindings::PerformanceObserverPrototype>(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<void> PerformanceObserver::observe(PerformanceObserverInit& options)
{
auto& realm = this->realm();
// 1. Let relevantGlobal be this's relevant global object.
auto* relevant_global = dynamic_cast<HTML::WindowOrWorkerGlobalScopeMixin*>(&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::WindowOrWorkerGlobalScopeMixin*>(&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<JS::Handle<PerformanceTimeline::PerformanceEntry>> PerformanceObserver::take_records()
{
// The takeRecords() method must return a copy of this's observer buffer, and also empty this's observer buffer.
Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>> records;
for (auto& record : m_observer_buffer)
records.append(*record);
m_observer_buffer.clear();
return records;
}
void PerformanceObserver::unset_requires_dropped_entries(Badge<HTML::WindowOrWorkerGlobalScopeMixin>)
{
m_requires_dropped_entries = false;
}
void PerformanceObserver::append_to_observer_buffer(Badge<HTML::WindowOrWorkerGlobalScopeMixin>, JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry> entry)
{
m_observer_buffer.append(entry);
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
namespace Web::PerformanceTimeline {
// https://w3c.github.io/performance-timeline/#dom-performanceobserverinit
struct PerformanceObserverInit {
Optional<Vector<String>> entry_types;
Optional<String> type;
Optional<bool> 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<JS::NonnullGCPtr<PerformanceObserver>> construct_impl(JS::Realm&, JS::GCPtr<WebIDL::CallbackType>);
virtual ~PerformanceObserver() override;
WebIDL::ExceptionOr<void> observe(PerformanceObserverInit& options);
void disconnect();
Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>> take_records();
bool requires_dropped_entries() const { return m_requires_dropped_entries; }
void unset_requires_dropped_entries(Badge<HTML::WindowOrWorkerGlobalScopeMixin>);
Vector<PerformanceObserverInit> const& options_list() const { return m_options_list; }
WebIDL::CallbackType& callback() { return *m_callback; }
void append_to_observer_buffer(Badge<HTML::WindowOrWorkerGlobalScopeMixin>, JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>);
private:
PerformanceObserver(JS::Realm&, JS::GCPtr<WebIDL::CallbackType>);
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<WebIDL::CallbackType> m_callback;
// https://w3c.github.io/performance-timeline/#dfn-observer-buffer
// A PerformanceEntryList object called the observer buffer that is initially empty.
Vector<JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>> 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<PerformanceObserverInit> m_options_list;
};
}

View file

@ -0,0 +1,25 @@
#import <PerformanceTimeline/PerformanceObserverEntryList.idl>
// 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<DOMString> 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<DOMString> supportedEntryTypes;
};

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/QuickSort.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PerformanceObserverEntryListPrototype.h>
#include <LibWeb/PerformanceTimeline/PerformanceEntry.h>
#include <LibWeb/PerformanceTimeline/PerformanceObserverEntryList.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::PerformanceTimeline {
PerformanceObserverEntryList::PerformanceObserverEntryList(JS::Realm& realm, Vector<JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>>&& 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<Bindings::PerformanceObserverEntryListPrototype>(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<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> filter_buffer_by_name_and_type(Vector<JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>> const& buffer, Optional<String> name, Optional<String> type)
{
// 1. Let result be an initially empty list.
Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>> 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<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> 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<String> {}, /* type= */ Optional<String> {}));
}
// https://w3c.github.io/performance-timeline/#dom-performanceobserverentrylist-getentriesbytype
WebIDL::ExceptionOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> 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<String> {}, type));
}
// https://w3c.github.io/performance-timeline/#dom-performanceobserverentrylist-getentriesbyname
WebIDL::ExceptionOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> PerformanceObserverEntryList::get_entries_by_name(String const& name, Optional<String> 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));
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Bindings/PlatformObject.h>
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<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> get_entries() const;
WebIDL::ExceptionOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> get_entries_by_type(String const& type) const;
WebIDL::ExceptionOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> get_entries_by_name(String const& name, Optional<String> type) const;
private:
PerformanceObserverEntryList(JS::Realm&, Vector<JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>>&&);
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<JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>> m_entry_list;
};
ErrorOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> filter_buffer_by_name_and_type(Vector<JS::NonnullGCPtr<PerformanceTimeline::PerformanceEntry>> const& buffer, Optional<String> name, Optional<String> type);
}

View file

@ -0,0 +1,9 @@
#import <HighResolutionTime/Performance.idl>
// 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);
};

View file

@ -36,7 +36,7 @@ public:
static Optional<u64> 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<PerformanceTimeline::PerformanceObserverInit const&> = {}) const override { return PerformanceTimeline::ShouldAddEntry::Yes; }
virtual FlyString const& entry_type() const override;

View file

@ -38,7 +38,7 @@ public:
static Optional<u64> 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<PerformanceTimeline::PerformanceObserverInit const&> = {}) const override { return PerformanceTimeline::ShouldAddEntry::Yes; }
virtual FlyString const& entry_type() const override;

View file

@ -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)