Bug 1820485 [wpt PR 38826] - LoAF: Initial support for long scripts, a=testonly

Automatic update from web-platform-tests
LoAF: Initial support for long scripts

AnimationFrameMonitor signs into the probe system to track scripts.
Scripts that are 5ms or longer that are part of a LoAF (long amimation
frame) are reported as one of the LoAF entry.

Since some of the probes can be recursive, and also layouts/styles can
be recursive, AnimationFrameTimingMonitor maintains a little state
machine so that only the top-level scripts are captured:

- CompileAndRunScript is called for a <script> tag.
- ExecuteScript is called for a script tag (after compilation) and also
  when an imported module is executed.
- UserCallback is called specifically for callbacks who implemented it.
- CallFunction is called *a lot* and is there to collect source location
  for the top level script.
- WillHandlePromise is added. Note that task that begin with a promise
  resolution end only when the next microtask queue is emptied.

Note the following:
- Only script *entry points* are reported, as in, there are no
  recursions or time-overlaps between scripts.
- To reduce overhead, bookekeping is done once it's clear that the
  script is longer than 5ms. If this still creates too much overhead,
  we can increase the number and measure even longer scripts only.
- The state machine is somewhat similar to the one in
  PerformanceMonitor. However, the two classes have a different
  lifecycle and PerformanceMonitor has a lot of legacy, so copying
  some of the logic seemed less costly than trying to unify.

Missing pieces:
- Not all the user callbacks are measured/probed, e.g.
  PerformanceObserver callbacks. This would require a lot of detail
  and fine-tuning.
- At first, PromiseResolver-based entry points don't have a lot of
  details, e.g. source location and name. This will be done in a
  later phase.
- Queue time and presentation time are still missing.

Explainer:
https://github.com/w3c/longtasks/blob/loaf-explainer/loaf-explainer.md

Design doc:
https://docs.google.com/document/d/1SeMd4KbXWZf0ZnRSMvYhjSBpXPBln5xrRyTu2Gr68BY/edit#

Change-Id: I57b62ab51b3f1ab28bbfbcc2d992df4cc10d38ec
Bug: 1392685
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4268371
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Reviewed-by: Yoav Weiss <yoavweiss@chromium.org>
Commit-Queue: Noam Rosenthal <nrosenthal@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1114430}

--

wpt-commits: b8e9469a49469720bb72a078c4ecde798daa70f5
wpt-pr: 38826
This commit is contained in:
Noam Rosenthal 2023-03-20 18:08:25 +00:00 коммит произвёл moz-wptsync-bot
Родитель b95d3f786b
Коммит 3a9295d799
9 изменённых файлов: 338 добавлений и 13 удалений

Просмотреть файл

@ -0,0 +1,33 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: basic</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<body>
<h1>Long Animation Frame: event handlers</h1>
<div id="log"></div>
<script>
test_self_event_listener(t => {
const img = document.createElement("img");
img.src = "/images/green.png";
img.addEventListener("load", () => {
busy_wait();
});
document.body.appendChild(img);
t.add_cleanup(() => img.remove());
}, "IMG.onload");
test_self_event_listener(t => {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/common/dummy.xml");
xhr.addEventListener("load", () => {
busy_wait();
});
xhr.send();
}, "XMLHttpRequest.onload");
</script>
</body>

Просмотреть файл

@ -0,0 +1,36 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: requestIdleCallback</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<body>
<h1>Long Animation Frame: requestIdleCallback</h1>
<div id="log"></div>
<script>
setup(() =>
assert_implements(window.requestIdleCallback,
'requestIdleCallback is not supported.'));
/*
promise_test(async t => {
await expect_no_long_frame(() => requestIdleCallback(busy_wait), t);
}, 'A long busy wait in an idle callback is not a long animation frame');
*/
promise_test(async t => {
const segment_duration = very_long_frame_duration / 2;
requestIdleCallback(() => {
busy_wait(segment_duration);
requestAnimationFrame(() => {
busy_wait(segment_duration);
});
});
await expect_long_frame(() => {}, t);
}, 'A long busy wait split between an idle callback and a ' +
'requestAnimationFrame is a long animation frame');
</script>
</body>

Просмотреть файл

@ -15,7 +15,7 @@ const host_info = get_host_info();
const {ORIGIN, REMOTE_ORIGIN, HTTP_NOTSAMESITE_ORIGIN} = host_info; const {ORIGIN, REMOTE_ORIGIN, HTTP_NOTSAMESITE_ORIGIN} = host_info;
promise_test(async t => { promise_test(async t => {
const executor = await prepare_exec_iframe(t, ORIGIN); const [executor] = await prepare_exec_iframe(t, ORIGIN);
await expect_no_long_frame(() => executor.execute_script((duration) => { await expect_no_long_frame(() => executor.execute_script((duration) => {
const deadline = performance.now() + duration; const deadline = performance.now() + duration;
while (performance.now() < deadline) {} while (performance.now() < deadline) {}
@ -23,7 +23,7 @@ promise_test(async t => {
}, 'A long busy wait without render in a same-origin iframe is not a long animation frame'); }, 'A long busy wait without render in a same-origin iframe is not a long animation frame');
promise_test(async t => { promise_test(async t => {
const executor = await prepare_exec_iframe(t, HTTP_NOTSAMESITE_ORIGIN); const [executor] = await prepare_exec_iframe(t, HTTP_NOTSAMESITE_ORIGIN);
await expect_no_long_frame(() => executor.execute_script((duration) => { await expect_no_long_frame(() => executor.execute_script((duration) => {
const deadline = performance.now() + duration; const deadline = performance.now() + duration;
while (performance.now() < deadline) {} while (performance.now() < deadline) {}
@ -31,7 +31,7 @@ promise_test(async t => {
}, 'A long busy wait in a cross-origin iframe is not a long animation frame'); }, 'A long busy wait in a cross-origin iframe is not a long animation frame');
promise_test(async t => { promise_test(async t => {
const executor = await prepare_exec_iframe(t, ORIGIN); const [executor] = await prepare_exec_iframe(t, ORIGIN);
await expect_long_frame(() => executor.execute_script(async (duration) => { await expect_long_frame(() => executor.execute_script(async (duration) => {
await new Promise(resolve => window.requestAnimationFrame(resolve)); await new Promise(resolve => window.requestAnimationFrame(resolve));
const deadline = performance.now() + duration; const deadline = performance.now() + duration;
@ -40,7 +40,7 @@ promise_test(async t => {
}, 'A long busy wait in a same-origin requestAnimationFrame is a long animation frame'); }, 'A long busy wait in a same-origin requestAnimationFrame is a long animation frame');
promise_test(async t => { promise_test(async t => {
const executor = await prepare_exec_popup(t, ORIGIN); const [executor] = await prepare_exec_popup(t, ORIGIN);
await expect_no_long_frame(() => executor.execute_script((duration) => { await expect_no_long_frame(() => executor.execute_script((duration) => {
const deadline = performance.now() + duration; const deadline = performance.now() + duration;
while (performance.now() < deadline) {} while (performance.now() < deadline) {}
@ -49,7 +49,7 @@ promise_test(async t => {
for (const origin of ["ORIGIN", "REMOTE_ORIGIN", "HTTP_NOTSAMESITE_ORIGIN"]) { for (const origin of ["ORIGIN", "REMOTE_ORIGIN", "HTTP_NOTSAMESITE_ORIGIN"]) {
promise_test(async t => { promise_test(async t => {
const executor = await prepare_exec_iframe(t, host_info[origin]); const [executor] = await prepare_exec_iframe(t, host_info[origin]);
const entry = await executor.execute_script(async (duration) => { const entry = await executor.execute_script(async (duration) => {
const entryPromise = new Promise(resolve => new PerformanceObserver(list => { const entryPromise = new Promise(resolve => new PerformanceObserver(list => {
resolve(list.getEntries(0)); resolve(list.getEntries(0));

Просмотреть файл

@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: basic</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="resources/utils.js"></script>
<body>
<h1>Long Animation Frame: promise resolvers</h1>
<div id="log"></div>
<script type="module">
const {REMOTE_ORIGIN} = get_host_info();
test_promise_script(async t => {
await fetch("/common/dummy.xml");
busy_wait(very_long_frame_duration);
}, "resolve", "Promise.resolve", "fetch");
test_promise_script(async t => {
const response = await fetch("/common/dummy.xml");
await response.text();
busy_wait(very_long_frame_duration);
}, "resolve", "Promise.resolve", "Response.text");
test_promise_script(async t => {
const response = await import("/loading/resources/dummy.js");
busy_wait(very_long_frame_duration);
}, "resolve", "Promise.resolve", "import");
test_promise_script(async t => {
fetch(new URL("/common/dummy.xml", REMOTE_ORIGIN).href, {mode: "cors"})
.catch(() => {
busy_wait(very_long_frame_duration);
})
}, "reject", "Promise.reject" );
</script>
</body>

Просмотреть файл

@ -0,0 +1,47 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: basic</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<body>
<h1>Long Animation Frame: script blocks</h1>
<div id="log"></div>
<script>
test_self_script_block(t => {
const script = document.createElement("script");
script.innerHTML = `(${busy_wait.toString()})()`;
document.body.appendChild(script);
}, location.href, "classic-script");
test_self_script_block(t => {
const script = document.createElement("script");
script.type = "module";
script.innerHTML = `(${busy_wait.toString()})()`;
document.body.appendChild(script);
}, location.href, "module-script");
test_self_script_block(t => {
const script = document.createElement("script");
script.src = "resources/busy.js";
document.body.appendChild(script);
}, new URL("resources/busy.js", location.href).href, "classic-script");
test_self_script_block(t => {
const script = document.createElement("script");
script.src = "resources/busy.js";
script.type = "module";
document.body.appendChild(script);
}, new URL("resources/busy.js", location.href).href, "module-script");
test_self_script_block(t => {
const script = document.createElement("script");
script.type = "module";
script.innerHTML = `import("./resources/busy.js?import");`;
document.body.appendChild(script);
}, new URL("resources/busy.js?import", location.href).href, "execute-script");
</script>
</body>

Просмотреть файл

@ -0,0 +1,62 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: window attribution</title>
<meta name="timeout" content="long">
<body>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="/common/utils.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="resources/utils.js"></script>
<div id="log"></div>
<script>
const host_info = get_host_info();
const {ORIGIN, REMOTE_ORIGIN, HTTP_NOTSAMESITE_ORIGIN} = host_info;
promise_test (async t => {
const [entry, script] = await expect_long_frame_with_script(() => {
requestAnimationFrame(() => busy_wait());
}, () => true, t);
assert_equals(script.windowAttribution, "self");
assert_equals(script.window, window);
}, 'Scripts in this window should be self-attributed');
promise_test (async t => {
const [executor, iframe] = await prepare_exec_iframe(t, ORIGIN);
const [entry, script] = await expect_long_frame_with_script(() =>
executor.execute_script(async (duration) => {
await new Promise(resolve => window.requestAnimationFrame(resolve));
const deadline = performance.now() + duration;
while (performance.now() < deadline) {}
}, [very_long_frame_duration]), () => true, t);
assert_equals(script.windowAttribution, "descendant");
assert_equals(script.window, iframe.contentWindow);
}, 'Scripts in subframes should be descendant-attributed');
promise_test (async t => {
const [executor1, iframe1] = await prepare_exec_iframe(t, ORIGIN);
const [executor2, iframe2] = await prepare_exec_iframe(t, ORIGIN);
const [entry, script] = await expect_long_frame_with_script(() =>
executor1.execute_script(async (duration) => {
await new Promise(resolve => window.requestAnimationFrame(resolve));
const deadline = performance.now() + duration;
while (performance.now() < deadline) {}
}, [very_long_frame_duration]), () => true, t);
const find_entry = win =>
win.performance.getEntriesByType("long-animation-frame").find(
e => e.duration >= very_long_frame_duration &&
e.scripts.length).scripts[0];
const iframe1_entry = find_entry(iframe1.contentWindow);
const iframe2_entry = find_entry(iframe2.contentWindow);
assert_equals(iframe1_entry.windowAttribution, "self");
assert_equals(iframe2_entry.windowAttribution, "same-page");
assert_equals(iframe1_entry.window, iframe1.contentWindow);
assert_equals(iframe2_entry.window, iframe1.contentWindow);
}, 'Scripts in subframes should be same-page-attributed to other subframes');
</script>
</body>

Просмотреть файл

@ -0,0 +1,52 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>Long Animation Frame Timing: basic</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<body>
<h1>Long Animation Frame: user callbacks</h1>
<div id="log"></div>
<script>
test_self_user_callback(t =>
t.step_timeout(() => busy_wait()), "Window.setTimeout");
test_self_user_callback(() => {
const interval = setInterval(() => {
busy_wait();
clearInterval(interval);
}, 10);
}, "Window.setInterval");
test_self_user_callback(() =>
requestAnimationFrame(() => busy_wait()), "Window.requestAnimationFrame");
test_self_user_callback(t => {
const element = document.createElement("div");
document.body.appendChild(element);
t.add_cleanup(() => element.remove());
new ResizeObserver((entries, observer) => {
busy_wait(very_long_frame_duration);
observer.disconnect();
}).observe(element);
}, "ResizeObserver.callback");
test_self_user_callback(t => {
const element = document.createElement("div");
element.innerText = "123";
t.add_cleanup(() => element.remove());
new IntersectionObserver((entries, observer) => {
busy_wait(very_long_frame_duration);
observer.disconnect();
}).observe(element);
document.body.appendChild(element);
}, "IntersectionObserver.callback");
test_self_user_callback(t =>
scheduler.postTask(() => busy_wait()), "Scheduler.postTask");
</script>
</body>

Просмотреть файл

@ -0,0 +1,4 @@
(() => {
const deadline = performance.now() + 360;
while (performance.now() < deadline) {}
})();

Просмотреть файл

@ -5,14 +5,18 @@ setup(() =>
const very_long_frame_duration = 360; const very_long_frame_duration = 360;
function loaf_promise() { function loaf_promise(t) {
return new Promise(resolve => { return new Promise(resolve => {
const observer = new PerformanceObserver(entries => { const observer = new PerformanceObserver(entries => {
const entry = entries.getEntries()[0]; const entry = entries.getEntries()[0];
if (entry.duration >= very_long_frame_duration) if (entry.duration >= very_long_frame_duration) {
observer.disconnect();
resolve(entry); resolve(entry);
}
}); });
t.add_cleanup(() => observer.disconnect());
observer.observe({entryTypes: ['long-animation-frame']}); observer.observe({entryTypes: ['long-animation-frame']});
}); });
} }
@ -28,9 +32,9 @@ async function expect_long_frame(cb, t) {
await windowLoaded; await windowLoaded;
await new Promise(resolve => t.step_timeout(resolve, 0)); await new Promise(resolve => t.step_timeout(resolve, 0));
const timeout = new Promise((resolve, reject) => const timeout = new Promise((resolve, reject) =>
t.step_timeout(() => reject("timeout"), no_long_frame_timeout)); t.step_timeout(() => resolve("timeout"), no_long_frame_timeout));
const receivedLongFrame = loaf_promise(); const receivedLongFrame = loaf_promise(t);
await cb(); await cb(t);
const entry = await Promise.race([ const entry = await Promise.race([
receivedLongFrame, receivedLongFrame,
timeout timeout
@ -38,10 +42,24 @@ async function expect_long_frame(cb, t) {
return entry; return entry;
} }
async function expect_long_frame_with_script(cb, predicate, t) {
for (let i = 0; i < 10; ++i) {
const entry = await expect_long_frame(cb, t);
if (!entry.scripts.length)
continue;
for (const script of entry.scripts) {
if (predicate(script))
return [entry, script];
}
}
return [];
}
async function expect_no_long_frame(cb, t) { async function expect_no_long_frame(cb, t) {
await windowLoaded; await windowLoaded;
for (let i = 0; i < 5; ++i) { for (let i = 0; i < 5; ++i) {
const receivedLongFrame = loaf_promise(); const receivedLongFrame = loaf_promise(t);
await cb(); await cb();
const result = await Promise.race([receivedLongFrame, const result = await Promise.race([receivedLongFrame,
new Promise(resolve => t.step_timeout(() => resolve("timeout"), new Promise(resolve => t.step_timeout(() => resolve("timeout"),
@ -62,7 +80,7 @@ async function prepare_exec_iframe(t, origin) {
iframe.src = url.href; iframe.src = url.href;
document.body.appendChild(iframe); document.body.appendChild(iframe);
await new Promise(resolve => iframe.addEventListener("load", resolve)); await new Promise(resolve => iframe.addEventListener("load", resolve));
return new RemoteContext(uuid); return [new RemoteContext(uuid), iframe];
} }
@ -72,5 +90,38 @@ async function prepare_exec_popup(t, origin) {
url.searchParams.set("uuid", uuid); url.searchParams.set("uuid", uuid);
const popup = window.open(url); const popup = window.open(url);
t.add_cleanup(() => popup.close()); t.add_cleanup(() => popup.close());
return new RemoteContext(uuid); return [new RemoteContext(uuid), popup];
}
function test_loaf_script(cb, name, type, label) {
promise_test(async t => {
const [entry, script] = await expect_long_frame_with_script(cb,
script => (script.type === type && script.duration >= very_long_frame_duration), t);
assert_true(!!entry, "Entry detected");
assert_equals(script.name, name);
assert_greater_than_equal(script.duration, very_long_frame_duration);
assert_greater_than_equal(entry.duration, script.duration);
assert_greater_than_equal(script.executionStart, script.startTime);
assert_greater_than_equal(script.startTime, entry.startTime)
assert_equals(script.window, window);
assert_equals(script.forcedStyleAndLayoutDuration, 0);
assert_equals(script.windowAttribution, "self");
}, `LoAF script: ${name} ${type},${label ? ` ${label}` : ''}`);
}
function test_self_user_callback(cb, name) {
test_loaf_script(cb, name, "user-callback");
}
function test_self_event_listener(cb, name) {
test_loaf_script(cb, name, "event-listener");
}
function test_promise_script(cb, resolve_or_reject, name, label) {
test_loaf_script(cb, name, `${resolve_or_reject}-promise`, label);
}
function test_self_script_block(cb, name, type) {
test_loaf_script(cb, name, type);
} }