From 8f8c6160c753b8c43e59134832b7f23452ee8b22 Mon Sep 17 00:00:00 2001 From: Noam Rosenthal Date: Thu, 12 Jan 2023 11:21:07 -0800 Subject: [PATCH] Refactor nested-navigations resource-timing flow This changes how frames, iframes & objects decide how to report their navigations as resource timing entries to their parent: - Any frame-initiated navigation (e.g. iframe.src change) is reported as an entry. This complies with current spec. - For nested navigations that fail Timing-Allow-Origin, we don't report the normal responseEnd - instead we report the load event time as the responseEnd, to prevent leakage of navigation-related cross-origin information (see https://github.com/w3c/resource-timing/issues/340) This incidentally fixes other existing issues with nested navigations and resource timing, such as flaky tests and inconsistencies regarding restored iframes. Bug: 1404695 Bug: 1348080 Bug: 1290721 Bug: 1380078 Bug: 1378015 Bug: 957181 Spec changes: https://github.com/whatwg/html/pull/8643 https://github.com/whatwg/fetch/pull/1579 Change-Id: I010b026788193cc77a7de3f3d75304602f41fcd5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4145963 Reviewed-by: Yoav Weiss Commit-Queue: Noam Rosenthal Cr-Commit-Position: refs/heads/main@{#1091970} --- ...ent-frame-with-cross-origin-child.sub.html | 6 +- .../parent-frame-with-same-origin-child.html | 6 +- .../iframe-sequence-of-events.html | 16 ++- .../nested-nav-fallback-timing.html | 33 +++++++ resource-timing/resources/delay-load.html | 4 + resource-timing/resources/frame-timing.js | 97 +++++++++++-------- 6 files changed, 115 insertions(+), 47 deletions(-) create mode 100644 resource-timing/nested-nav-fallback-timing.html create mode 100644 resource-timing/resources/delay-load.html diff --git a/performance-timeline/resources/parent-frame-with-cross-origin-child.sub.html b/performance-timeline/resources/parent-frame-with-cross-origin-child.sub.html index 4be0df872cbfd2..a49174519e42de 100644 --- a/performance-timeline/resources/parent-frame-with-cross-origin-child.sub.html +++ b/performance-timeline/resources/parent-frame-with-cross-origin-child.sub.html @@ -8,9 +8,11 @@ document.body.appendChild(childFrame) performance.mark("entry-name") + const loadPromise = new Promise(resolve => window.addEventListener("load", resolve)) - childFrame.addEventListener('load', () => { + childFrame.addEventListener('load', async () => { + await loadPromise const entries = performance.getEntries(true) window.parent.postMessage(entries.length, "*") }) - \ No newline at end of file + diff --git a/performance-timeline/resources/parent-frame-with-same-origin-child.html b/performance-timeline/resources/parent-frame-with-same-origin-child.html index c9248a4e8bb9f4..813c2a725bdd5e 100644 --- a/performance-timeline/resources/parent-frame-with-same-origin-child.html +++ b/performance-timeline/resources/parent-frame-with-same-origin-child.html @@ -8,9 +8,11 @@ document.body.appendChild(childFrame) performance.mark("entry-name") + const loadPromise = new Promise(resolve => window.addEventListener("load", resolve)); - childFrame.addEventListener('load', () => { + childFrame.addEventListener('load', async () => { + await loadPromise; const entries = performance.getEntries(true) window.parent.postMessage(entries.length, "*") }) - \ No newline at end of file + diff --git a/resource-timing/iframe-sequence-of-events.html b/resource-timing/iframe-sequence-of-events.html index 5f99a5cab2de6b..02d1c362c9df49 100644 --- a/resource-timing/iframe-sequence-of-events.html +++ b/resource-timing/iframe-sequence-of-events.html @@ -4,9 +4,21 @@ + + - \ No newline at end of file + diff --git a/resource-timing/nested-nav-fallback-timing.html b/resource-timing/nested-nav-fallback-timing.html new file mode 100644 index 00000000000000..b8bba5614d0d12 --- /dev/null +++ b/resource-timing/nested-nav-fallback-timing.html @@ -0,0 +1,33 @@ + + +Test ResourceTiming reporting for cross-origin iframe. + + + + + + + + diff --git a/resource-timing/resources/delay-load.html b/resource-timing/resources/delay-load.html new file mode 100644 index 00000000000000..4898c1be8ebfff --- /dev/null +++ b/resource-timing/resources/delay-load.html @@ -0,0 +1,4 @@ + + + + diff --git a/resource-timing/resources/frame-timing.js b/resource-timing/resources/frame-timing.js index e0c364e9b2c3e2..019bd424b55065 100644 --- a/resource-timing/resources/frame-timing.js +++ b/resource-timing/resources/frame-timing.js @@ -1,48 +1,63 @@ function test_frame_timing_before_load_event(type) { - promise_test(async t => { - const {document, performance} = type === 'frame' ? window.parent : window; - const delay = 500; - const frame = document.createElement(type); - t.add_cleanup(() => frame.remove()); - await new Promise(resolve => { - frame.addEventListener('load', resolve); - frame.src = `resources/iframe-with-delay.sub.html?delay=${delay}`; - (type === 'frame' ? document.querySelector('frameset') : document.body).appendChild(frame); - }); + promise_test(async t => { + const {document, performance} = type === 'frame' ? window.parent : window; + const delay = 500; + const frame = document.createElement(type); + t.add_cleanup(() => frame.remove()); + await new Promise(resolve => { + frame.addEventListener('load', resolve); + frame.src = `/resource-timing/resources/iframe-with-delay.sub.html?delay=${delay}`; + (type === 'frame' ? document.querySelector('frameset') : document.body).appendChild(frame); + }); - const entries = performance.getEntriesByName(frame.src); - const navigationEntry = frame.contentWindow.performance.getEntriesByType('navigation')[0]; - assert_equals(entries.length, 1); - assert_equals(entries[0].initiatorType, type); - assert_greater_than(performance.now(), entries[0].responseEnd + delay); - const domContentLoadedEventAbsoluteTime = navigationEntry.domContentLoadedEventStart + frame.contentWindow.performance.timeOrigin; - const frameResponseEndAbsoluteTime = entries[0].responseEnd + performance.timeOrigin; - assert_greater_than(domContentLoadedEventAbsoluteTime, frameResponseEndAbsoluteTime); - }, `A ${type} should report its RT entry when the response is done and before it is completely loaded`); + const entries = performance.getEntriesByName(frame.src); + const navigationEntry = frame.contentWindow.performance.getEntriesByType('navigation')[0]; + assert_equals(entries.length, 1); + assert_equals(entries[0].initiatorType, type); + assert_greater_than(performance.now(), entries[0].responseEnd + delay); + const domContentLoadedEventAbsoluteTime = + navigationEntry.domContentLoadedEventStart + + frame.contentWindow.performance.timeOrigin; + const frameResponseEndAbsoluteTime = entries[0].responseEnd + performance.timeOrigin; + assert_greater_than(domContentLoadedEventAbsoluteTime, frameResponseEndAbsoluteTime); + }, `A ${type} should report its RT entry when the response is done and before it is completely loaded`); } -function test_frame_timing_change_src(type) { - promise_test(async t => { - const {document, performance} = type === 'frame' ? window.parent : window; - const frame = document.createElement(type); - t.add_cleanup(() => frame.remove()); - await new Promise(resolve => { - const done = () => { - resolve(); - frame.removeEventListener('load', done); - } - frame.addEventListener('load', done); - frame.src = 'resources/green.html?1'; - (type === 'frame' ? document.querySelector('frameset') : document.body).appendChild(frame); - }); +function test_frame_timing_change_src(type, + origin1 = document.origin, + origin2 = document.origin, + tao = false, label = '') { + const uid = token(); + promise_test(async t => { + const {document, performance} = type === 'frame' ? window.parent : window; + const frame = document.createElement(type); + t.add_cleanup(() => frame.remove()); + function createURL(origin) { + const url = new URL(`${origin}/resource-timing/resources/green.html`, location.href); + url.searchParams.set("uid", uid); + if (tao) + url.searchParams.set("pipe", "header(Timing-Allow-Origin, *)"); + return url.toString(); + } - await new Promise(resolve => { - frame.addEventListener('load', resolve); - frame.src = 'resources/green.html?2'; - }); + await new Promise(resolve => { + const done = () => { + resolve(); + frame.removeEventListener('load', done); + } + frame.addEventListener('load', done); + frame.src = createURL(origin1); + const root = type === 'frame' ? document.querySelector('frameset') : document.body; + root.appendChild(frame); + }); - const entries = performance.getEntries().filter(e => e.name.includes('green.html')); - assert_equals(entries.length, 2); - }, `A ${type} should report separate RT entries if its src changed from the outside`); -} \ No newline at end of file + await new Promise(resolve => { + frame.addEventListener('load', resolve); + frame.src = createURL(origin2); + }); + + const entries = performance.getEntries().filter(e => e.name.includes(uid)); + assert_equals(entries.length, 2); + }, label || `A ${type} should report separate RT entries if its src changed from the outside`); +}