LibWeb: Use DocumentLoadTimingInfo values in PerformanceTiming interface

This commit is contained in:
Tim Ledbetter 2025-01-06 10:26:55 +00:00 committed by Andreas Kling
parent f8b8c9c4a4
commit e03fec2a15
Notes: github-actions[bot] 2025-01-11 10:12:48 +00:00
7 changed files with 481 additions and 7 deletions

View file

@ -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<HTML::Window>(global_object));
auto& window = static_cast<HTML::Window&>(global_object);
auto document = window.document();
return document->load_timing_info();
}
}

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2021, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* 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;
};

View file

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

View file

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

View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>functionality test of window.performance.measure</title>
<link rel="author" title="Intel" href="http://www.intel.com/" />
<link rel="help" href="http://www.w3.org/TR/user-timing/#extensions-performance-interface"/>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="../common/performance-timeline-utils.js"></script>
<script src="resources/webperftestharness.js"></script>
<script src="resources/webperftestharnessextension.js"></script>
<script>
setup({ explicit_done: true });
function onload_test()
{
const measures_for_timing_order = [
['nav2now', 'navigationStart'],
['loadTime', 'navigationStart', 'loadEventEnd'],
['loadEventEnd2a', 'loadEventEnd', 'abc'],
['nav2a', 'navigationStart', 'abc'],
['domComplete2a', 'domComplete', 'abc'],
['negativeValue', 1, 'navigationStart'],
];
const context = new PerformanceContext(window.performance);
mark_names.forEach(function(name) {
context.mark(name);
});
measures_for_timing_order.forEach(context.initialMeasures, context);
test_greater_than(context.getEntriesByName('nav2now', 'measure')[0].duration, 0, 'Measure of navigationStart to now should be positive value.');
test_greater_than(context.getEntriesByName('loadTime', 'measure')[0].duration, 0, 'Measure of navigationStart to loadEventEnd should be positive value.');
test_greater_than(0, context.getEntriesByName('negativeValue', 'measure')[0].duration, 'Measure of current mark to navigationStart should be negative value.');
test_equals(context.getEntriesByName('loadTime', 'measure')[0].duration + context.getEntriesByName('loadEventEnd2a', 'measure')[0].duration, context.getEntriesByName('nav2a', 'measure')[0].duration, 'loadTime plus loadEventEnd to a mark "a" should equal to navigationStart to "a".');
// We later assert that time has passed between setting one set of marks and another set.
// However, this assertion will fail if the test executes fast enough such that the marks occur
// at the same clock time. This is more likely in browsers such as Firefox that reduce the
// precision of the clock exposed through this API to mitigate timing attacks. To mitigate the
// test failure, we sleep. Firefox may round timestamps to the nearest millisecond in either
// direction - e.g. 10ms & 11.999ms may both round to 11ms - so we need to sleep at least 2ms to
// avoid test failures. To be safe, we sleep 3ms.
sleep_milliseconds(3);
// Following cases test for scenarios that measure names are tied twice.
mark_names.forEach(function(name) {
context.mark(name);
});
measures_for_timing_order.forEach(context.initialMeasures, context);
test_greater_than(context.getEntriesByName('nav2now', 'measure')[1].duration, context.getEntriesByName('nav2now', 'measure')[0].duration, 'Second measure of current mark to navigationStart should be negative value.');
test_equals(context.getEntriesByName('loadTime', 'measure')[0].duration, context.getEntriesByName('loadTime', 'measure')[1].duration, 'Measures of loadTime should have same duration.');
test_greater_than(context.getEntriesByName('domComplete2a', 'measure')[1].duration, context.getEntriesByName('domComplete2a', 'measure')[0].duration, 'Measure from domComplete event to most recent mark "a" should have longer duration.');
test_greater_than(context.getEntriesByName('negativeValue', 'measure')[0].duration, context.getEntriesByName('negativeValue', 'measure')[1].duration, 'Measure from most recent mark to navigationStart should have longer duration.');
done();
}
</script>
</head>
<body onload="setTimeout(onload_test,0)">
<h1>Description</h1>
<p>This test validates functionality of the interface window.performance.measure using keywords from the Navigation Timing spec.</p>
<div id="log"></div>
</body>
</html>

View file

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

View file

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