From e03fec2a1553db0f1ee0cb562842326549f47f13 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Mon, 6 Jan 2025 10:26:55 +0000 Subject: [PATCH] LibWeb: Use DocumentLoadTimingInfo values in PerformanceTiming interface --- .../NavigationTiming/PerformanceTiming.cpp | 9 + .../NavigationTiming/PerformanceTiming.h | 17 +- ...sure_associated_with_navigation_timing.txt | 14 ++ .../common/performance-timeline-utils.js | 56 +++++ ...ure_associated_with_navigation_timing.html | 66 ++++++ .../resources/webperftestharness.js | 124 +++++++++++ .../resources/webperftestharnessextension.js | 202 ++++++++++++++++++ 7 files changed, 481 insertions(+), 7 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/user-timing/measure_associated_with_navigation_timing.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/common/performance-timeline-utils.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/user-timing/measure_associated_with_navigation_timing.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharness.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharnessextension.js diff --git a/Libraries/LibWeb/NavigationTiming/PerformanceTiming.cpp b/Libraries/LibWeb/NavigationTiming/PerformanceTiming.cpp index 6ddc2fd0210..7b9740ac9ba 100644 --- a/Libraries/LibWeb/NavigationTiming/PerformanceTiming.cpp +++ b/Libraries/LibWeb/NavigationTiming/PerformanceTiming.cpp @@ -24,4 +24,13 @@ void PerformanceTiming::initialize(JS::Realm& realm) WEB_SET_PROTOTYPE_FOR_INTERFACE(PerformanceTiming); } +DOM::DocumentLoadTimingInfo const& PerformanceTiming::document_load_timing_info() const +{ + auto& global_object = HTML::relevant_global_object(*this); + VERIFY(is(global_object)); + auto& window = static_cast(global_object); + auto document = window.document(); + return document->load_timing_info(); +} + } diff --git a/Libraries/LibWeb/NavigationTiming/PerformanceTiming.h b/Libraries/LibWeb/NavigationTiming/PerformanceTiming.h index e5d01c7f8ff..6159824fd6a 100644 --- a/Libraries/LibWeb/NavigationTiming/PerformanceTiming.h +++ b/Libraries/LibWeb/NavigationTiming/PerformanceTiming.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Andreas Kling + * Copyright (c) 2025, Tim Ledbetter * * SPDX-License-Identifier: BSD-2-Clause */ @@ -19,7 +20,7 @@ public: ~PerformanceTiming(); - u64 navigation_start() { return 0; } + u64 navigation_start() { return document_load_timing_info().navigation_start_time; } u64 unload_event_start() { return 0; } u64 unload_event_end() { return 0; } u64 redirect_start() { return 0; } @@ -34,16 +35,18 @@ public: u64 response_start() { return 0; } u64 response_end() { return 0; } u64 dom_loading() { return 0; } - u64 dom_interactive() { return 0; } - u64 dom_content_loaded_event_start() { return 0; } - u64 dom_content_loaded_event_end() { return 0; } - u64 dom_complete() { return 0; } - u64 load_event_start() { return 0; } - u64 load_event_end() { return 0; } + u64 dom_interactive() { return document_load_timing_info().dom_interactive_time; } + u64 dom_content_loaded_event_start() { return document_load_timing_info().dom_content_loaded_event_start_time; } + u64 dom_content_loaded_event_end() { return document_load_timing_info().dom_content_loaded_event_end_time; } + u64 dom_complete() { return document_load_timing_info().dom_complete_time; } + u64 load_event_start() { return document_load_timing_info().load_event_start_time; } + u64 load_event_end() { return document_load_timing_info().load_event_end_time; } private: explicit PerformanceTiming(JS::Realm&); + DOM::DocumentLoadTimingInfo const& document_load_timing_info() const; + virtual void initialize(JS::Realm&) override; }; diff --git a/Tests/LibWeb/Text/expected/wpt-import/user-timing/measure_associated_with_navigation_timing.txt b/Tests/LibWeb/Text/expected/wpt-import/user-timing/measure_associated_with_navigation_timing.txt new file mode 100644 index 00000000000..b936072438a --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/user-timing/measure_associated_with_navigation_timing.txt @@ -0,0 +1,14 @@ +Harness status: OK + +Found 8 tests + +7 Pass +1 Fail +Pass Measure of navigationStart to now should be positive value. +Pass Measure of navigationStart to loadEventEnd should be positive value. +Pass Measure of current mark to navigationStart should be negative value. +Fail loadTime plus loadEventEnd to a mark "a" should equal to navigationStart to "a". +Pass Second measure of current mark to navigationStart should be negative value. +Pass Measures of loadTime should have same duration. +Pass Measure from domComplete event to most recent mark "a" should have longer duration. +Pass Measure from most recent mark to navigationStart should have longer duration. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/common/performance-timeline-utils.js b/Tests/LibWeb/Text/input/wpt-import/common/performance-timeline-utils.js new file mode 100644 index 00000000000..b20241cc610 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/common/performance-timeline-utils.js @@ -0,0 +1,56 @@ +/* +author: W3C http://www.w3.org/ +help: http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute +*/ +var performanceNamespace = window.performance; +var namespace_check = false; +function wp_test(func, msg, properties) +{ + // only run the namespace check once + if (!namespace_check) + { + namespace_check = true; + + if (performanceNamespace === undefined || performanceNamespace == null) + { + // show a single error that window.performance is undefined + // The window.performance attribute provides a hosting area for performance related attributes. + test(function() { assert_true(performanceNamespace !== undefined && performanceNamespace != null, "window.performance is defined and not null"); }, "window.performance is defined and not null."); + } + } + + test(func, msg, properties); +} + +function test_true(value, msg, properties) +{ + wp_test(function () { assert_true(value, msg); }, msg, properties); +} + +function test_equals(value, equals, msg, properties) +{ + wp_test(function () { assert_equals(value, equals, msg); }, msg, properties); +} + +// assert for every entry in `expectedEntries`, there is a matching entry _somewhere_ in `actualEntries` +function test_entries(actualEntries, expectedEntries) { + test_equals(actualEntries.length, expectedEntries.length) + expectedEntries.forEach(function (expectedEntry) { + var foundEntry = actualEntries.find(function (actualEntry) { + return typeof Object.keys(expectedEntry).find(function (key) { + return actualEntry[key] !== expectedEntry[key] + }) === 'undefined' + }) + test_true(!!foundEntry, `Entry ${JSON.stringify(expectedEntry)} could not be found.`) + if (foundEntry) { + assert_object_equals(foundEntry.toJSON(), expectedEntry) + } + }) +} + +function delayedLoadListener(callback) { + window.addEventListener('load', function() { + // TODO(cvazac) Remove this setTimeout when spec enforces sync entries. + step_timeout(callback, 0) + }) +} diff --git a/Tests/LibWeb/Text/input/wpt-import/user-timing/measure_associated_with_navigation_timing.html b/Tests/LibWeb/Text/input/wpt-import/user-timing/measure_associated_with_navigation_timing.html new file mode 100644 index 00000000000..105b6200cee --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/user-timing/measure_associated_with_navigation_timing.html @@ -0,0 +1,66 @@ + + + + +functionality test of window.performance.measure + + + + + + + + + + +

Description

+

This test validates functionality of the interface window.performance.measure using keywords from the Navigation Timing spec.

+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharness.js b/Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharness.js new file mode 100644 index 00000000000..9627e18a03e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharness.js @@ -0,0 +1,124 @@ +// +// Helper functions for User Timing tests +// + +var timingAttributes = [ + "navigationStart", + "unloadEventStart", + "unloadEventEnd", + "redirectStart", + "redirectEnd", + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "secureConnectionStart", + "requestStart", + "responseStart", + "responseEnd", + "domLoading", + "domInteractive", + "domContentLoadedEventStart", + "domContentLoadedEventEnd", + "domComplete", + "loadEventStart", + "loadEventEnd" +]; + +function has_required_interfaces() +{ + if (window.performance.mark == undefined || + window.performance.clearMarks == undefined || + window.performance.measure == undefined || + window.performance.clearMeasures == undefined || + window.performance.getEntriesByName == undefined || + window.performance.getEntriesByType == undefined || + window.performance.getEntries == undefined) { + return false; + } + + return true; +} + +function test_namespace(child_name, skip_root) +{ + if (skip_root === undefined) { + var msg = 'window.performance is defined'; + wp_test(function () { assert_not_equals(performanceNamespace, undefined, msg); }, msg); + } + + if (child_name !== undefined) { + var msg2 = 'window.performance.' + child_name + ' is defined'; + wp_test(function() { assert_not_equals(performanceNamespace[child_name], undefined, msg2); }, msg2); + } +} + +function test_attribute_exists(parent_name, attribute_name, properties) +{ + var msg = 'window.performance.' + parent_name + '.' + attribute_name + ' is defined.'; + wp_test(function() { assert_not_equals(performanceNamespace[parent_name][attribute_name], undefined, msg); }, msg, properties); +} + +function test_enum(parent_name, enum_name, value, properties) +{ + var msg = 'window.performance.' + parent_name + '.' + enum_name + ' is defined.'; + wp_test(function() { assert_not_equals(performanceNamespace[parent_name][enum_name], undefined, msg); }, msg, properties); + + msg = 'window.performance.' + parent_name + '.' + enum_name + ' = ' + value; + wp_test(function() { assert_equals(performanceNamespace[parent_name][enum_name], value, msg); }, msg, properties); +} + +function test_timing_order(attribute_name, greater_than_attribute, properties) +{ + // ensure it's not 0 first + var msg = "window.performance.timing." + attribute_name + " > 0"; + wp_test(function() { assert_true(performanceNamespace.timing[attribute_name] > 0, msg); }, msg, properties); + + // ensure it's in the right order + msg = "window.performance.timing." + attribute_name + " >= window.performance.timing." + greater_than_attribute; + wp_test(function() { assert_true(performanceNamespace.timing[attribute_name] >= performanceNamespace.timing[greater_than_attribute], msg); }, msg, properties); +} + +function test_timing_greater_than(attribute_name, greater_than, properties) +{ + var msg = "window.performance.timing." + attribute_name + " > " + greater_than; + test_greater_than(performanceNamespace.timing[attribute_name], greater_than, msg, properties); +} + +function test_timing_equals(attribute_name, equals, msg, properties) +{ + var test_msg = msg || "window.performance.timing." + attribute_name + " == " + equals; + test_equals(performanceNamespace.timing[attribute_name], equals, test_msg, properties); +} + +// +// Non-test related helper functions +// + +function sleep_milliseconds(n) +{ + var start = new Date().getTime(); + while (true) { + if ((new Date().getTime() - start) >= n) break; + } +} + +// +// Common helper functions +// + +function test_greater_than(value, greater_than, msg, properties) +{ + wp_test(function () { assert_greater_than(value, greater_than, msg); }, msg, properties); +} + +function test_greater_or_equals(value, greater_than, msg, properties) +{ + wp_test(function () { assert_greater_than_equal(value, greater_than, msg); }, msg, properties); +} + +function test_not_equals(value, notequals, msg, properties) +{ + wp_test(function() { assert_not_equals(value, notequals, msg); }, msg, properties); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharnessextension.js b/Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharnessextension.js new file mode 100644 index 00000000000..8640918d4f2 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/user-timing/resources/webperftestharnessextension.js @@ -0,0 +1,202 @@ +// +// Helper functions for User Timing tests +// + +var mark_names = [ + '', + '1', + 'abc', +]; + +var measures = [ + [''], + ['2', 1], + ['aaa', 'navigationStart', ''], +]; + +function test_method_exists(method, method_name, properties) +{ + var msg; + if (typeof method === 'function') + msg = 'performance.' + method.name + ' is supported!'; + else + msg = 'performance.' + method_name + ' is supported!'; + wp_test(function() { assert_equals(typeof method, 'function', msg); }, msg, properties); +} + +function test_method_throw_exception(func_str, exception, msg) +{ + let exception_name; + let test_func; + if (typeof exception == "function") { + exception_name = exception.name; + test_func = assert_throws_js; + } else { + exception_name = exception; + test_func = assert_throws_dom; + } + var msg = 'Invocation of ' + func_str + ' should throw ' + exception_name + ' Exception.'; + wp_test(function() { test_func(exception, function() {eval(func_str)}, msg); }, msg); +} + +function test_noless_than(value, greater_than, msg, properties) +{ + wp_test(function () { assert_true(value >= greater_than, msg); }, msg, properties); +} + +function test_fail(msg, properties) +{ + wp_test(function() { assert_unreached(); }, msg, properties); +} + +function test_resource_entries(entries, expected_entries) +{ + // This is slightly convoluted so that we can sort the output. + var actual_entries = {}; + var origin = window.location.protocol + "//" + window.location.host; + + for (var i = 0; i < entries.length; ++i) { + var entry = entries[i]; + var found = false; + for (var expected_entry in expected_entries) { + if (entry.name == origin + expected_entry) { + found = true; + if (expected_entry in actual_entries) { + test_fail(expected_entry + ' is not expected to have duplicate entries'); + } + actual_entries[expected_entry] = entry; + break; + } + } + if (!found) { + test_fail(entries[i].name + ' is not expected to be in the Resource Timing buffer'); + } + } + + sorted_urls = []; + for (var i in actual_entries) { + sorted_urls.push(i); + } + sorted_urls.sort(); + for (var i in sorted_urls) { + var url = sorted_urls[i]; + test_equals(actual_entries[url].initiatorType, + expected_entries[url], + origin + url + ' is expected to have initiatorType ' + expected_entries[url]); + } + for (var j in expected_entries) { + if (!(j in actual_entries)) { + test_fail(origin + j + ' is expected to be in the Resource Timing buffer'); + } + } +} + +function performance_entrylist_checker(type) +{ + const entryType = type; + + function entry_check(entry, expectedNames, testDescription = '') + { + const msg = testDescription + 'Entry \"' + entry.name + '\" should be one that we have set.'; + wp_test(function() { assert_in_array(entry.name, expectedNames, msg); }, msg); + test_equals(entry.entryType, entryType, testDescription + 'entryType should be \"' + entryType + '\".'); + if (type === "measure") { + test_true(isFinite(entry.startTime), testDescription + 'startTime should be a number.'); + test_true(isFinite(entry.duration), testDescription + 'duration should be a number.'); + } else if (type === "mark") { + test_greater_than(entry.startTime, 0, testDescription + 'startTime should greater than 0.'); + test_equals(entry.duration, 0, testDescription + 'duration of mark should be 0.'); + } + } + + function entrylist_order_check(entryList) + { + let inOrder = true; + for (let i = 0; i < entryList.length - 1; ++i) + { + if (entryList[i + 1].startTime < entryList[i].startTime) { + inOrder = false; + break; + } + } + return inOrder; + } + + function entrylist_check(entryList, expectedLength, expectedNames, testDescription = '') + { + test_equals(entryList.length, expectedLength, testDescription + 'There should be ' + expectedLength + ' entries.'); + test_true(entrylist_order_check(entryList), testDescription + 'Entries in entrylist should be in order.'); + for (let i = 0; i < entryList.length; ++i) + { + entry_check(entryList[i], expectedNames, testDescription + 'Entry_list ' + i + '. '); + } + } + + return{"entrylist_check":entrylist_check}; +} + +function PerformanceContext(context) +{ + this.performanceContext = context; +} + +PerformanceContext.prototype = +{ + + initialMeasures: function(item, index, array) + { + this.performanceContext.measure.apply(this.performanceContext, item); + }, + + mark: function() + { + this.performanceContext.mark.apply(this.performanceContext, arguments); + }, + + measure: function() + { + this.performanceContext.measure.apply(this.performanceContext, arguments); + }, + + clearMarks: function() + { + this.performanceContext.clearMarks.apply(this.performanceContext, arguments); + }, + + clearMeasures: function() + { + this.performanceContext.clearMeasures.apply(this.performanceContext, arguments); + + }, + + getEntries: function() + { + return this.performanceContext.getEntries.apply(this.performanceContext, arguments); + }, + + getEntriesByType: function() + { + return this.performanceContext.getEntriesByType.apply(this.performanceContext, arguments); + }, + + getEntriesByName: function() + { + return this.performanceContext.getEntriesByName.apply(this.performanceContext, arguments); + }, + + setResourceTimingBufferSize: function() + { + return this.performanceContext.setResourceTimingBufferSize.apply(this.performanceContext, arguments); + }, + + registerResourceTimingBufferFullCallback: function(func) + { + this.performanceContext.onresourcetimingbufferfull = func; + }, + + clearResourceTimings: function() + { + this.performanceContext.clearResourceTimings.apply(this.performanceContext, arguments); + } + +};