A huge amount of Puppeteer unit tests hang in `Runtime.callFunctionOn` when using IntersectionObserver
Categories
(Core :: Layout, defect, P1)
Tracking
()
Tracking | Status | |
---|---|---|
firefox74 | --- | fixed |
People
(Reporter: whimboo, Assigned: emilio)
References
Details
(Keywords: hang)
Attachments
(3 files)
Basically nearly all the tests in click.spec.js hang in the Runtime.callFunctionOn
method, and as such the tests time out like:
1) Firefox Browser Page Page.click should click on checkbox label and toggle (click.spec.js:156:5)
Message:
Timeout Exceeded 10000ms
Here the call which causes our end-point to hang:
puppeteer:protocol SEND ► {"sessionId":1,"method":"Runtime.callFunctionOn","params":{"functionDeclaration":"async(element, pageJavascriptEnabled) => {\n if (!element.isConnected)\n return 'Node is detached from document';\n if (element.nodeType !== Node.ELEMENT_NODE)\n return 'Node is not of type HTMLElement';\n // force-scroll if page's javascript is disabled.\n if (!pageJavascriptEnabled) {\n element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});\n return false;\n }\n const visibleRatio = await new Promise(resolve => {\n const observer = new IntersectionObserver(entries => {\n resolve(entries[0].intersectionRatio);\n observer.disconnect();\n });\n observer.observe(element);\n });\n if (visibleRatio !== 1.0)\n element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});\n return false;\n }\n//# sourceURL=__puppeteer_evaluation_script__\n","executionContextId":4,"arguments":[{"objectId":"975f0fe5-ec3a-c141-9816-de0521eb5119"},{"value":true}],"returnByValue":true,"awaitPromise":true,"userGesture":true},"id":22} +0ms
I will put this into the reserve for now given that seems to be important. Maybe we just miss to wait correctly for a Promise to be resolved? The function the above code comes from is _scrollIntoViewIfNeeded
from JSHandle.js
.
Reporter | ||
Comment 1•6 years ago
|
||
The problem is that the IntersectionObserver
never fires it's callback:
https://searchfox.org/mozilla-central/rev/9e45d74b956be046e5021a746b0c8912f1c27318/remote/test/puppeteer/lib/JSHandle.js#191-197
I seem to fail to parse this method. Where is actually an activity triggered which changes the intersection of the element? We seem to wait forever for that. Maybe Chrome has a different implementation? Andreas or Maja, do you both know more about this class? If not we might want to reach out to someone from the DOM team.
Reporter | ||
Comment 2•6 years ago
|
||
Interesting is that when I run such a test in non-headless mode I only have to move the mouse a bit anywhere in the content area to make this test passing.
Reporter | ||
Comment 3•6 years ago
|
||
And running with Chrome I get the following 3 life cycle events logged before the Promise gets resolved:
puppeteer:protocol ◀ RECV {"method":"Page.lifecycleEvent","params":{"frameId":"5F87880D2B7B4033CD6C1A16923CBE56","loaderId":"0F4D7D1F0254AF6217814F660B59FF45","name":"firstPaint","timestamp":43242.4669},"sessionId":"26D5DB4CD4AEE5CC277EBAAB8E74ECD4"} +8ms
puppeteer:protocol ◀ RECV {"method":"Page.lifecycleEvent","params":{"frameId":"5F87880D2B7B4033CD6C1A16923CBE56","loaderId":"0F4D7D1F0254AF6217814F660B59FF45","name":"firstContentfulPaint","timestamp":43242.4669},"sessionId":"26D5DB4CD4AEE5CC277EBAAB8E74ECD4"} +0ms
puppeteer:protocol ◀ RECV {"method":"Page.lifecycleEvent","params":{"frameId":"5F87880D2B7B4033CD6C1A16923CBE56","loaderId":"0F4D7D1F0254AF6217814F660B59FF45","name":"firstMeaningfulPaintCandidate","timestamp":43242.4669},"sessionId":"26D5DB4CD4AEE5CC277EBAAB8E74ECD4"} +0ms
Concerns about callFunctionOn
have come up before. In each case so far, we've found that it was actually some other underlying issue, e.g. the command times out waiting for a function that is checking that the viewport is resized. I think it makes more sense to re-purpose this bug for a specific reason for timeout, like my example.
Reporter | ||
Comment 5•6 years ago
|
||
Sure, we can make this bug specific to the IntersectionObserver
usage.
So read a bit about this kind of observer, and as used in Puppeteer (link see comment 1) no options are specified. That means that every property in options will receive the defaults. threshold
will be 0
, which means that at the moment when the element is fully visible in the viewport the callback has to be executed. But that is not happening.
Here an simplified HTML document: https://output.jsbin.com/qazaninala
Open Firefox's devtools and run the following code in the console:
var element = document.getElementById("agree");
new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
}).then(visibleRatio => {
alert(visibleRatio);
});
The promise doesn't resolve while the mouse doesn't hover over the visible viewport. In Chrome it resolves immediately.
Olli, could this be a bug with IntersectionObserver?
Comment 6•6 years ago
|
||
IntersectionObserver is more a layout thingie. Perhaps emilio knows about it, or knows who knows.
Assignee | ||
Comment 7•6 years ago
|
||
Which OS was comment 5 on? I get the promise resolved just fine on Linux.
Intersection observer notifications happen at each refresh driver tick... Does the promise resolve if you force the refresh driver to run by doing something like requestAnimationFrame(() => {})
or such?
It may be that IntersectionObserver doesn't schedule the refresh driver to run itself, and / or that we throttle it somehow.
Not sure if creating an intersection observer should force the refresh driver to tick...
Reporter | ||
Comment 8•6 years ago
|
||
This was tested with Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:74.0) Gecko/20100101 Firefox/74.0 ID:20200112214546.
Forcing the refresh driver to run makes it indeed work for me, and the callback is fired immediately. So there might be a OS specific code path which we don't hit on MacOS? Note that I haven't tested on Windows yet.
Reporter | ||
Comment 9•6 years ago
|
||
Please note that I also tried it with Linux Mint inside of VirtualBox, and it is also not working for me with that OS. Maybe you had your mouse inside the viewport when checking my steps?
Assignee | ||
Comment 10•6 years ago
|
||
I'm using Wayland, that's probably why. Pretty sure I didn't have a mouse in the viewport.
Anyhow... So in Gecko it seems to me there's no guarantee for the refresh driver to fire as a result of an intersection observation being created.
I guess this piece of the spec, in particular step 5 is what is supposed to do this:
- Schedule an iteration of the event loop in the root's browsing context.
Though I don't think that sentence is particularly well defined.
Assignee | ||
Comment 11•6 years ago
|
||
Could you give the patch a try and ensure that it fixes your issue? I'm running it through try atm (if it's green I'll send it for review I suppose).
Assignee | ||
Comment 12•6 years ago
|
||
Updated•6 years ago
|
Reporter | ||
Comment 13•6 years ago
|
||
I will have a look when the build is ready.
Reporter | ||
Comment 14•6 years ago
•
|
||
The try build is here:
https://treeherder.mozilla.org/#/jobs?repo=try&tier=1%2C2%2C3&revision=45206046b01dc195f432e3d5cbd44049bd087ca3
I also submitted a puppeteer job to see if we are doing already better with just this patch.
Reporter | ||
Comment 15•6 years ago
|
||
Emilio, I tested your build now and it is still not working with the Puppeteer test, but I can see a bit of changed behavior in Firefox when running my testcase manually. I don't need the mouse in the visible viewport anymore.
Maybe you should really try to run the test yourself. Here the steps:
- Goto /remote/test/puppeteer/test/click.spec.js and change line 156 from
it(
tofit(
. - Run
DEBUG="puppeteer:*" mach puppeteer-test
By using fit
you will instruct the test harness to only run this single test. And that is the one which is failing for me still. Only when moving the mouse the checkbox is getting checked. So maybe there is really another part left to do.
Reporter | ||
Comment 16•6 years ago
|
||
Also the try job for Puppeteer is still failing with:
[task 2020-01-15T17:42:20.472Z] 25) Firefox Browser Page Page.click should click on checkbox label and toggle ([33mclick.spec.js:156:5[0m)
[task 2020-01-15T17:42:20.472Z] Message:
[task 2020-01-15T17:42:20.472Z] [31mTimeout Exceeded 10000ms[0m
Reporter | ||
Comment 17•6 years ago
|
||
Emilio, please let me know if there is anything else I can help with.
Assignee | ||
Comment 18•6 years ago
|
||
So I can repro the failure, and I'll take a look, but fwiw... There's no easy way to launch firefox under a debugger for that test suite, looks like. I tried the obvious --debugger=rr
and such. In the end I hacked it up like this:
diff --git a/remote/test/puppeteer/lib/Launcher.js b/remote/test/puppeteer/lib/Launcher.js
index ceb57463db4f..2b5d662d51fb 100644
--- a/remote/test/puppeteer/lib/Launcher.js
+++ b/remote/test/puppeteer/lib/Launcher.js
@@ -390,7 +390,16 @@ class FirefoxLauncher {
extraPrefsFirefox = {}
} = options;
- const firefoxArguments = [];
+ let executable = executablePath;
+ if (!executablePath) {
+ const {missingText, executablePath} = resolveExecutablePath(this);
+ if (missingText)
+ throw new Error(missingText);
+ executable = executablePath;
+ }
+
+ const firefoxArguments = [executable];
+ executable = '/usr/local/bin/rr';
if (!ignoreDefaultArgs)
firefoxArguments.push(...this.defaultArgs(options));
else if (Array.isArray(ignoreDefaultArgs))
@@ -406,13 +415,6 @@ class FirefoxLauncher {
firefoxArguments.push(temporaryUserDataDir);
}
- let executable = executablePath;
- if (!executablePath) {
- const {missingText, executablePath} = resolveExecutablePath(this);
- if (missingText)
- throw new Error(missingText);
- executable = executablePath;
- }
const runner = new BrowserRunner(executable, firefoxArguments, temporaryUserDataDir);
runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe});
But a debug build running under rr is slower so I always hit:
puppeteer.spec.js:90:5 - Timeout Exceeded 10000ms while running "beforeAll" in suite "Firefox Browser"
I eventually figured where the 10 seconds came from and that I could override it using the TIMEOUT
variable but it took a bit... Exposing this stuff from ./mach puppeteer-test --help
or such would've been great.
Assignee | ||
Comment 19•6 years ago
|
||
Well, and the hack above didn't work because it kills the rr process abnormally and thus the trace doesn't finish.
Updated•6 years ago
|
Assignee | ||
Comment 20•6 years ago
|
||
Tweaked the patch a bit, mind giving it a shot and confirming it fixes the issue for you too?
Assignee | ||
Comment 21•6 years ago
|
||
I tried to write a WPT test for this but I couldn't get it to fail without my patch... :/
Assignee | ||
Comment 22•6 years ago
|
||
FWIW, the way I was trying is:
<!doctype html>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
<link rel="author" href="https://mozilla.org" title="Mozilla">
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1608931">
<link rel="help" href="https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe">
<div id="observe">Observe me</div>
<script>
const t = async_test("IntersectionObserver.observe schedules a rendering update to call the callback as needed if intersecting");
onload = t.step_func(function() {
requestIdleCallback(t.step_func(function() {
const element = document.getElementById("observe");
const observer = new IntersectionObserver(t.step_func_done(function(entries) {
assert_true(entries[0].intersectionRatio > 0, "Element is intersecting");
}));
observer.observe(element);
}));
});
</script>
Reporter | ||
Comment 23•6 years ago
|
||
(In reply to Emilio Cobos Álvarez (:emilio) from comment #20)
Tweaked the patch a bit, mind giving it a shot and confirming it fixes the issue for you too?
Can you please point me to a try build? If you can trigger a MacOS opt/debug build that would be great. I will be back later today and would be able to use those builds for testing.
Reporter | ||
Comment 24•6 years ago
|
||
Oh, I think it's:
https://treeherder.mozilla.org/#/jobs?repo=try&tier=1%2C2%2C3&revision=97e61a91623691c316c303826a45500aec2e64a0
I will trigger the Mac builds quickly before heading out.
Assignee | ||
Comment 25•6 years ago
|
||
https://treeherder.mozilla.org/#/jobs?repo=try&revision=40eb17959a28f7ec6509b34c4a20b42b59633f8d should be better (doesn't have the debug-only code).
Reporter | ||
Comment 26•6 years ago
|
||
Emilio, this works great! I just run the tests locally (also by enabling more in that file via fdescribe
) and even alone in this file we pass 8 more tests now! I've also triggered puppeteer tests for that try build:
These aren't done yet, and I will report back once they are. Let me see if I can somewhat modify your wpt test to actually make it failing locally. Maybe there is something specific the Puppeteer test is doing.
Reporter | ||
Comment 27•6 years ago
|
||
Puppeteer tests finished and it remains with 8 more passing tests:
https://treeherder.mozilla.org/logviewer.html#/jobs?job_id=285673589&repo=try&lineNumber=46196
Emilio, mind updating the bugs summary?
Reporter | ||
Comment 28•6 years ago
|
||
Emilio, this is the testcase as used by Puppeteer. Maybe it is related to the additional event handling? Use my code from comment 5 from inside the devtool's console, and you can see that the Promise is never resolved. For me that reproduces each and every time. With your build it is fixed.
Reporter | ||
Comment 29•6 years ago
|
||
Hm, could it be that this only happens when the IntersectionObserver code is run inside a framescript or JSWindowActorClient but not in the web page itself? Even if I strip down the above HTML testcase to only some lines it is still reproducible for me via the devtools console. But once that code is part of the web page, it just works.
Assignee | ||
Comment 30•6 years ago
|
||
Yeah, that sounds likely...
Comment 31•6 years ago
|
||
Assignee | ||
Comment 32•6 years ago
|
||
This can fire in some tests that take over the refresh driver via script.
Comment 33•6 years ago
|
||
Reporter | ||
Comment 34•6 years ago
|
||
Great work! With the patch landed on autoland I can see 11 more tests passing from the Puppeteer unit test suite. Thanks a lot Emilio!
Comment 35•6 years ago
|
||
bugherder |
https://hg.mozilla.org/mozilla-central/rev/9886115cd686
https://hg.mozilla.org/mozilla-central/rev/842e205217bf
Description
•