diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index d317742369..f02819d86b 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -532,6 +532,7 @@ set(SOURCES URL/URLSearchParams.cpp URL/URLSearchParamsIterator.cpp UserTiming/PerformanceMark.cpp + UserTiming/PerformanceMeasure.cpp WebAssembly/Instance.cpp WebAssembly/Memory.cpp WebAssembly/Module.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 8061f085cf..b7941fd62a 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -567,6 +567,7 @@ class URLSearchParamsIterator; namespace Web::UserTiming { class PerformanceMark; +class PerformanceMeasure; } namespace Web::WebAssembly { diff --git a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index 5d9cc077d8..cdce3f7914 100644 --- a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -35,8 +36,9 @@ 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) +#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) JS::ThrowCompletionOr WindowOrWorkerGlobalScopeMixin::initialize(JS::Realm& realm) { diff --git a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp index 89a2178930..7ad1f94762 100644 --- a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp +++ b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp @@ -8,8 +8,11 @@ #include #include #include +#include #include #include +#include +#include #include #include @@ -89,6 +92,237 @@ void Performance::clear_marks(Optional mark_name) // 3. Return undefined. } +WebIDL::ExceptionOr Performance::convert_name_to_timestamp(JS::Realm& realm, String const& name) +{ + auto& vm = realm.vm(); + + // 1. If the global object is not a Window object, throw a TypeError. + if (!is(realm.global_object())) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, TRY_OR_THROW_OOM(vm, String::formatted("'{}' is an attribute in the PerformanceTiming interface and thus can only be used in a Window context", name)) }; + + // 2. If name is navigationStart, return 0. + if (name == NavigationTiming::EntryNames::navigationStart) + return 0.0; + + auto timing_interface = timing(); + VERIFY(timing_interface); + + // 3. Let startTime be the value of navigationStart in the PerformanceTiming interface. + auto start_time = timing_interface->navigation_start(); + + // 4. Let endTime be the value of name in the PerformanceTiming interface. + u64 end_time { 0 }; + +#define __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME(camel_case_name, snake_case_name) \ + if (name == NavigationTiming::EntryNames::camel_case_name) \ + end_time = timing_interface->snake_case_name(); + ENUMERATE_NAVIGATION_TIMING_ENTRY_NAMES +#undef __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME + + // 5. If endTime is 0, throw an InvalidAccessError. + if (end_time == 0) + return WebIDL::InvalidAccessError::create(realm, DeprecatedString::formatted("The '{}' entry in the PerformanceTiming interface is equal to 0, meaning it hasn't happened yet", name)); + + // 6. Return result of subtracting startTime from endTime. + return static_cast(end_time - start_time); +} + +// https://w3c.github.io/user-timing/#dfn-convert-a-mark-to-a-timestamp +WebIDL::ExceptionOr Performance::convert_mark_to_timestamp(JS::Realm& realm, Variant mark) +{ + if (mark.has()) { + auto const& mark_string = mark.get(); + + // 1. If mark is a DOMString and it has the same name as a read only attribute in the PerformanceTiming interface, let end + // time be the value returned by running the convert a name to a timestamp algorithm with name set to the value of mark. +#define __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME(name, _) \ + if (mark_string == NavigationTiming::EntryNames::name) \ + return convert_name_to_timestamp(realm, mark_string); + ENUMERATE_NAVIGATION_TIMING_ENTRY_NAMES +#undef __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME + + // 2. Otherwise, if mark is a DOMString, let end time be the value of the startTime attribute from the most recent occurrence + // of a PerformanceMark object in the performance entry buffer whose name is mark. If no matching entry is found, throw a + // SyntaxError. + auto* window_or_worker = dynamic_cast(&realm.global_object()); + VERIFY(window_or_worker); + + auto& tuple = window_or_worker->relevant_performance_entry_tuple(PerformanceTimeline::EntryTypes::mark); + auto& performance_entry_buffer = tuple.performance_entry_buffer; + + auto maybe_entry = performance_entry_buffer.last_matching([&mark_string](JS::Handle const& entry) { + return entry->name() == mark_string; + }); + + if (!maybe_entry.has_value()) + return WebIDL::SyntaxError::create(realm, DeprecatedString::formatted("No PerformanceMark object with name '{}' found in the performance timeline", mark_string)); + + return maybe_entry.value()->start_time(); + } + + // 3. Otherwise, if mark is a DOMHighResTimeStamp: + auto mark_time_stamp = mark.get(); + + // 1. If mark is negative, throw a TypeError. + if (mark_time_stamp < 0.0) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Cannot have negative time values in PerformanceMark"sv }; + + // 2. Otherwise, let end time be mark. + return mark_time_stamp; +} + +// https://w3c.github.io/user-timing/#dom-performance-measure +WebIDL::ExceptionOr> Performance::measure(String const& measure_name, Variant const& start_or_measure_options, Optional end_mark) +{ + auto& realm = this->realm(); + auto* window_or_worker = dynamic_cast(&realm.global_object()); + VERIFY(window_or_worker); + auto& vm = this->vm(); + + // 1. If startOrMeasureOptions is a PerformanceMeasureOptions object and at least one of start, end, duration, and detail + // are present, run the following checks: + auto const* start_or_measure_options_dictionary_object = start_or_measure_options.get_pointer(); + if (start_or_measure_options_dictionary_object + && (start_or_measure_options_dictionary_object->start.has_value() + || start_or_measure_options_dictionary_object->end.has_value() + || start_or_measure_options_dictionary_object->duration.has_value() + || !start_or_measure_options_dictionary_object->detail.is_undefined())) { + // 1. If endMark is given, throw a TypeError. + if (end_mark.has_value()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Cannot provide PerformanceMeasureOptions and endMark at the same time"sv }; + + // 2. If startOrMeasureOptions's start and end members are both omitted, throw a TypeError. + if (!start_or_measure_options_dictionary_object->start.has_value() && !start_or_measure_options_dictionary_object->end.has_value()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "PerformanceMeasureOptions must contain one or both of 'start' and 'end'"sv }; + + // 3. If startOrMeasureOptions's start, duration, and end members are all present, throw a TypeError. + if (start_or_measure_options_dictionary_object->start.has_value() && start_or_measure_options_dictionary_object->end.has_value() && start_or_measure_options_dictionary_object->duration.has_value()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "PerformanceMeasureOptions cannot contain 'start', 'duration' and 'end' properties all at once"sv }; + } + + // 2. Compute end time as follows: + HighResolutionTime::DOMHighResTimeStamp end_time { 0.0 }; + + // 1. If endMark is given, let end time be the value returned by running the convert a mark to a timestamp algorithm passing + // in endMark. + if (end_mark.has_value()) { + end_time = TRY(convert_mark_to_timestamp(realm, end_mark.value())); + } + // 2. Otherwise, if startOrMeasureOptions is a PerformanceMeasureOptions object, and if its end member is present, let end + // time be the value returned by running the convert a mark to a timestamp algorithm passing in startOrMeasureOptions's end. + else if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->end.has_value()) { + end_time = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->end.value())); + } + // 3. Otherwise, if startOrMeasureOptions is a PerformanceMeasureOptions object, and if its start and duration members are + // both present: + else if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->start.has_value() && start_or_measure_options_dictionary_object->duration.has_value()) { + // 1. Let start be the value returned by running the convert a mark to a timestamp algorithm passing in start. + auto start = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->start.value())); + + // 2. Let duration be the value returned by running the convert a mark to a timestamp algorithm passing in duration. + auto duration = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->duration.value())); + + // 3. Let end time be start plus duration. + end_time = start + duration; + } + // 4. Otherwise, let end time be the value that would be returned by the Performance object's now() method. + else { + // FIXME: Performance#now doesn't currently use TimeOrigin's functions, update this and Performance#now to match Performance#now's specification. + end_time = HighResolutionTime::unsafe_shared_current_time(); + } + + // 3. Compute start time as follows: + HighResolutionTime::DOMHighResTimeStamp start_time { 0.0 }; + + // 1. If startOrMeasureOptions is a PerformanceMeasureOptions object, and if its start member is present, let start time be + // the value returned by running the convert a mark to a timestamp algorithm passing in startOrMeasureOptions's start. + if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->start.has_value()) { + start_time = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->start.value())); + } + // 2. Otherwise, if startOrMeasureOptions is a PerformanceMeasureOptions object, and if its duration and end members are + // both present: + else if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->duration.has_value() && start_or_measure_options_dictionary_object->end.has_value()) { + // 1. Let duration be the value returned by running the convert a mark to a timestamp algorithm passing in duration. + auto duration = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->duration.value())); + + // 2. Let end be the value returned by running the convert a mark to a timestamp algorithm passing in end. + auto end = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->end.value())); + + // 3. Let start time be end minus duration. + start_time = end - duration; + } + // 3. Otherwise, if startOrMeasureOptions is a DOMString, let start time be the value returned by running the convert a mark + // to a timestamp algorithm passing in startOrMeasureOptions. + else if (start_or_measure_options.has()) { + start_time = TRY(convert_mark_to_timestamp(realm, start_or_measure_options.get())); + } + // 4. Otherwise, let start time be 0. + else { + start_time = 0.0; + } + + // NOTE: Step 4 (creating the entry) is done after determining values, as we set the values once during creation and never + // change them after. + + // 5. Set entry's name attribute to measureName. + // NOTE: Will be done during construction. + + // 6. Set entry's entryType attribute to DOMString "measure". + // NOTE: Already done via the `entry_type` virtual function. + + // 7. Set entry's startTime attribute to start time. + // NOTE: Will be done during construction. + + // 8. Set entry's duration attribute to the duration from start time to end time. The resulting duration value MAY be negative. + auto duration = end_time - start_time; + + // 9. Set entry's detail attribute as follows: + JS::Value detail { JS::js_null() }; + + // 1. If startOrMeasureOptions is a PerformanceMeasureOptions object and startOrMeasureOptions's detail member is present: + if (start_or_measure_options_dictionary_object && !start_or_measure_options_dictionary_object->detail.is_undefined()) { + // 1. Let record be the result of calling the StructuredSerialize algorithm on startOrMeasureOptions's detail. + auto record = TRY(HTML::structured_serialize(vm, start_or_measure_options_dictionary_object->detail)); + + // 2. Set entry's detail to the result of calling the StructuredDeserialize algorithm on record and the current realm. + detail = TRY(HTML::structured_deserialize(vm, record, realm, Optional {})); + } + + // 2. Otherwise, set it to null. + // NOTE: Already the default value of `detail`. + + // 4. Create a new PerformanceMeasure object (entry) with this's relevant realm. + auto entry = MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, measure_name, start_time, duration, detail)); + + // 10. Queue entry. + TRY(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. + + // 12. Return entry. + return entry; +} + +// https://w3c.github.io/user-timing/#dom-performance-clearmeasures +void Performance::clear_measures(Optional measure_name) +{ + auto& realm = this->realm(); + auto* window_or_worker = dynamic_cast(&realm.global_object()); + VERIFY(window_or_worker); + + // 1. If measureName is omitted, remove all PerformanceMeasure objects in the performance entry buffer. + if (!measure_name.has_value()) { + window_or_worker->clear_performance_entry_buffer({}, PerformanceTimeline::EntryTypes::measure); + return; + } + + // 2. Otherwise remove all PerformanceMeasure objects listed in the performance entry buffer whose name is measureName. + window_or_worker->remove_entries_from_performance_entry_buffer({}, PerformanceTimeline::EntryTypes::measure, measure_name.value()); + + // 3. Return undefined. +} + // https://www.w3.org/TR/performance-timeline/#getentries-method WebIDL::ExceptionOr>> Performance::get_entries() const { diff --git a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.h b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.h index c49fdaeb33..da92f02924 100644 --- a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.h +++ b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace Web::HighResolutionTime { @@ -26,6 +27,8 @@ public: WebIDL::ExceptionOr> mark(String const& mark_name, UserTiming::PerformanceMarkOptions const& mark_options = {}); void clear_marks(Optional mark_name); + WebIDL::ExceptionOr> measure(String const& measure_name, Variant const& start_or_measure_options, Optional end_mark); + void clear_measures(Optional measure_name); WebIDL::ExceptionOr>> get_entries() const; WebIDL::ExceptionOr>> get_entries_by_type(String const& type) const; @@ -37,6 +40,9 @@ private: virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; + WebIDL::ExceptionOr convert_name_to_timestamp(JS::Realm& realm, String const& name); + WebIDL::ExceptionOr convert_mark_to_timestamp(JS::Realm& realm, Variant mark); + JS::NonnullGCPtr m_window; JS::GCPtr m_timing; diff --git a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.idl b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.idl index 9a8e74aaf1..44c3b8946f 100644 --- a/Userland/Libraries/LibWeb/HighResolutionTime/Performance.idl +++ b/Userland/Libraries/LibWeb/HighResolutionTime/Performance.idl @@ -3,6 +3,7 @@ #import #import #import +#import // https://www.w3.org/TR/performance-timeline/#dom-performanceentrylist typedef sequence PerformanceEntryList; @@ -19,8 +20,8 @@ interface Performance : EventTarget { // "User Timing" extensions to the Performance interface PerformanceMark mark(DOMString markName, optional PerformanceMarkOptions markOptions = {}); undefined clearMarks(optional DOMString markName); - // FIXME: PerformanceMeasure measure(DOMString measureName, optional (DOMString or PerformanceMeasureOptions) startOrMeasureOptions = {}, optional DOMString endMark); - // FIXME: undefined clearMeasures(optional DOMString measureName); + PerformanceMeasure measure(DOMString measureName, optional (DOMString or PerformanceMeasureOptions) startOrMeasureOptions = {}, optional DOMString endMark); + undefined clearMeasures(optional DOMString measureName); // https://www.w3.org/TR/performance-timeline/#extensions-to-the-performance-interface // "Performance Timeline" extensions to the Performance interface diff --git a/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.cpp b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.cpp new file mode 100644 index 0000000000..5afaed4aa2 --- /dev/null +++ b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::UserTiming { + +PerformanceMeasure::PerformanceMeasure(JS::Realm& realm, String const& name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration, JS::Value detail) + : PerformanceTimeline::PerformanceEntry(realm, name, start_time, duration) + , m_detail(detail) +{ +} + +PerformanceMeasure::~PerformanceMeasure() = default; + +WebIDL::ExceptionOr> PerformanceMeasure::create(JS::Realm& realm, String const& measure_name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration, JS::Value detail) +{ + return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, measure_name, start_time, duration, detail)); +} + +FlyString const& PerformanceMeasure::entry_type() const +{ + return PerformanceTimeline::EntryTypes::measure; +} + +JS::ThrowCompletionOr PerformanceMeasure::initialize(JS::Realm& realm) +{ + MUST_OR_THROW_OOM(Base::initialize(realm)); + set_prototype(&Bindings::ensure_web_prototype(realm, "PerformanceMeasure")); + + return {}; +} + +void PerformanceMeasure::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_detail); +} + +} diff --git a/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.h b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.h new file mode 100644 index 0000000000..478acf930c --- /dev/null +++ b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::UserTiming { + +// https://w3c.github.io/user-timing/#dom-performancemeasureoptions +struct PerformanceMeasureOptions { + JS::Value detail { JS::js_undefined() }; + Optional> start; + Optional duration; + Optional> end; +}; + +// https://w3c.github.io/user-timing/#dom-performancemeasure +class PerformanceMeasure final : public PerformanceTimeline::PerformanceEntry { + WEB_PLATFORM_OBJECT(PerformanceMeasure, PerformanceTimeline::PerformanceEntry); + +public: + virtual ~PerformanceMeasure(); + + static WebIDL::ExceptionOr> create(JS::Realm&, String const& measure_name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration, JS::Value detail); + + // 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 + // NOTE: The empty state represents Infinite size. + 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 FlyString const& entry_type() const override; + + JS::Value detail() const { return m_detail; } + +private: + PerformanceMeasure(JS::Realm&, String const& name, HighResolutionTime::DOMHighResTimeStamp start_time, HighResolutionTime::DOMHighResTimeStamp duration, JS::Value detail); + + virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; + virtual void visit_edges(JS::Cell::Visitor&) override; + + // https://w3c.github.io/user-timing/#dom-performancemeasure-detail + JS::Value m_detail { JS::js_null() }; +}; + +} diff --git a/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.idl b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.idl new file mode 100644 index 0000000000..a3df4281bb --- /dev/null +++ b/Userland/Libraries/LibWeb/UserTiming/PerformanceMeasure.idl @@ -0,0 +1,13 @@ +#import + +[Exposed=(Window,Worker), UseNewAKString] +interface PerformanceMeasure : PerformanceEntry { + readonly attribute any detail; +}; + +dictionary PerformanceMeasureOptions { + any detail; + (DOMString or DOMHighResTimeStamp) start; + DOMHighResTimeStamp duration; + (DOMString or DOMHighResTimeStamp) end; +}; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 756124b256..9d6166dbe7 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -221,6 +221,7 @@ libweb_js_bindings(UIEvents/WheelEvent) libweb_js_bindings(URL/URL) libweb_js_bindings(URL/URLSearchParams ITERABLE) libweb_js_bindings(UserTiming/PerformanceMark) +libweb_js_bindings(UserTiming/PerformanceMeasure) libweb_js_bindings(WebAssembly/Instance) libweb_js_bindings(WebAssembly/Memory) libweb_js_bindings(WebAssembly/Module)