Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix iframe.onload being fired twice on about:blank. #16091

Closed
wants to merge 1 commit into from

Conversation

@gpoesia
Copy link
Contributor

gpoesia commented Mar 23, 2017

Supress the load event in the script thread when the iframe doesn't have a src attribute.
In that case, a load event is already fired when binding the iframe to the tree.


  • ./mach build -d does not report any errors

  • ./mach test-tidy does not report any errors

  • These changes fix #15727 (github issue number if applicable).

  • There are tests for these changes OR


This change is Reviewable

@highfive
Copy link

highfive commented Mar 23, 2017

Heads up! This PR modifies the following files:

  • @fitzgen: components/script/script_thread.rs
  • @KiChjang: components/script/script_thread.rs
@highfive
Copy link

highfive commented Mar 23, 2017

warning Warning warning

  • These commits include an empty title element (<title></title>). Consider adding appropriate metadata.
Copy link
Member

cbrewster left a comment

Looks good, although I did find another edge case ☹️

}
});
}, "IFrameSingleOnLoad");
document.getElementById('blank_iframe').contentWindow.location.reload();

This comment has been minimized.

@cbrewster

cbrewster Mar 23, 2017

Member

I'm not sure if this causes the same behavior as doing an inline onload on the iframe. I wonder if we could just create and add an iframe via javascript. (I would test this first on the master branch and see if that yields the onload event being fired twice).

This comment has been minimized.

@gpoesia

gpoesia Mar 23, 2017

Author Contributor

I had tested it on master and it did trigger two load events. However, I agree it's better to add a new iframe, since it's simpler and doesn't raise doubts. Will do!

function iframeOnLoad(t) {
iframe_loads++;

setTimeout(function() {

This comment has been minimized.

@cbrewster

cbrewster Mar 23, 2017

Member

Unfortunately, I am not entirely sure if there is a better option than doing a setTimeout.

Some(iframe) => {
// If the iframe has no "src" attribute, a load event will have already been fired
// when binding the iframe to the tree.
if iframe.upcast::<Element>().has_attribute(&local_name!("src")) {

This comment has been minimized.

@cbrewster

cbrewster Mar 23, 2017

Member

I have been thinking about this and testing a bit more... I don't know if this will be a sufficient solution. Consider the following case:

  • You have the iframe with no src
  • The load event steps for the initial about:blank are fired async as per the spec
  • In a script you set the src of the iframe to some URL
  • The load event steps are now processed and a load event is fired for the initial about:blank

A possible fix is to cancel the load event steps runnable when the src is set.
I have been working on about:blank fixes (#14764) and here is a commit that does this 46220d4
I would be ok if you wanted to implement something like that here since I don't know how soon I will have enough time to finish up the rest of that PR

This comment has been minimized.

@gpoesia

gpoesia Mar 24, 2017

Author Contributor

Hmm, right, now I see that case. I couldn't reproduce it, though (I tried this:

<html>
  <body>
  <iframe id="iframe" onload="alert('onload');"></iframe>
  <script type="text/javascript">
    document.getElementById("iframe").src = "a.html";
  </script>
  </body>
</html>

)

Does the spec say anything on what should happen to the first load event in the case of changing the src? I looked at your commit, but if I got it correctly it will only prevent the load steps from being loaded as long as the IFrameLoadEventSteps runnable checks the cancel flag after it is set, right? I'm imagining a case in which the program is exactly before this.iframe_load_event_steps(self.pipeline_id);, but after the if condition has been evaluated, and at this point the src changes and the other thread tries to cancel the event. The check makes it even harder for both events to run, but does it really guarantee it?
If you think it helps I can merge that commit into this PR. What do you think?

This comment has been minimized.

@cbrewster

cbrewster Mar 25, 2017

Member

So my solution may not be the best way, the spec basically says to delay the load event if a new navigation is triggered: https://html.spec.whatwg.org/multipage/embedded-content.html#iframe-load-event-steps (See the first NOTE).

The code is not running multithreaded here, we just have asynchronous tasks. So when we queue a task to do the iframe load event steps, this will allow other things to potentially be processed first (i.e. src changing before the task is handled). We currently have a LoadBlocker for Documents that handle this. Maybe we should do something similar with iframes.

@nox
Copy link
Member

nox commented Mar 24, 2017

@highfive highfive assigned cbrewster and unassigned nox Mar 24, 2017
@gpoesia gpoesia force-pushed the gpoesia:master branch from 4707b2d to ea98474 Mar 25, 2017
@gpoesia
Copy link
Contributor Author

gpoesia commented Mar 25, 2017

@cbrewster Oh, I see, thanks for the clarifications and patience! For some reason I thought we had multiple threads involved. I've merged your solution into this commit then, and also modified the test to create the iframe in JavaScript instead. Would you take another look?

Copy link
Member

cbrewster left a comment

Looks like there were some unintended changes in the MANIFEST

@@ -54200,6 +54200,16 @@
{}
]
],
"html/webappapis/scripting/events/iframe-onload-frame.html": [

This comment has been minimized.

{}
]
],
"html/webappapis/scripting/events/iframe-onload.html2": [

This comment has been minimized.

@@ -179815,6 +179831,18 @@
"491fd73a4ec8812cb8dc1ee01e34a7ff2a07ffb3",
"testharness"
],
"html/webappapis/scripting/events/iframe-onload-frame.html": [

This comment has been minimized.

"d5e1649c88102f27da5fe1ac16715cfee7f70f84",
"testharness"
],
"html/webappapis/scripting/events/iframe-onload.html2": [

This comment has been minimized.

@@ -7023,6 +7023,11 @@
{}
]
],
"css/border_collapse_simple_ref.html2": [

This comment has been minimized.

@@ -20385,6 +20405,10 @@
"00c9c85a861dacec7b34f019a43e347c11098ab0",
"support"
],
"css/border_collapse_simple_ref.html2": [

This comment has been minimized.

@@ -23413,6 +23437,10 @@
"0617c8de92ec1a283ac429dbbb073805e6bad28c",
"reftest"
],
"css/table_margin_a.html.bak": [

This comment has been minimized.

@@ -23425,6 +23453,10 @@
"d2ccfd91d67914e7e6653e1ce86deeebf5cd5977",
"support"
],
"css/table_margin_ref.html.bak": [

This comment has been minimized.

@@ -25737,6 +25769,10 @@
"333a80e3d4aec4a8be5566605fae5675a821085b",
"support"
],
"mozilla/referrer-policy/generic/subresource/mozresource.pyc": [

This comment has been minimized.

function iframeOnLoad(t) {
iframe_loads++;

setTimeout(function() {

This comment has been minimized.

@cbrewster

cbrewster Mar 26, 2017

Member

does this test fail on master? I think

t.step(function() {
    assert_true(iframe_loads === 1, "iframe.onload should be called exactly once.");
});

should be above the timeout

@cbrewster
Copy link
Member

cbrewster commented Mar 27, 2017

I have been thinking about this more, there are still edge cases where using the cancelled flag won't work. We may need to develop a more sophisticated way to delay iframe load events. Possibly something similar to document's LoadBlocker but a more simplified version.

@cbrewster
Copy link
Member

cbrewster commented Mar 28, 2017

@gpoesia sorry about the back and forth on this one... I believe you can remove my changes and this should be good to go. I forgot that when firing a load event, we check to see if the pipeline of the child document that was loaded matches the latest pipeline of the iframe. This already handles the case I mentioned above.

@gpoesia gpoesia force-pushed the gpoesia:master branch from ea98474 to 09d2fad Mar 29, 2017
@gpoesia
Copy link
Contributor Author

gpoesia commented Mar 29, 2017

@cbrewster no problem! I've fixed the unwanted changes in the MANIFEST files, and reverted the cancel flag part as well.

About the test, I've checked again and it does fail on master. The idea was waiting to call t.step() to give some time for the next load event to run, should it be fired. Then the assertion fails on both calls to t.step() (one from each event). If I put the assertion outside the timeout, then the first event will set a timeout to call t.done() directly, while the other would fail. Then it depends on what is stronger - the failed assertion or the delayed call to t.done(). Do you prefer one over the other? Let me know and I can change it as well.

@cbrewster
Copy link
Member

cbrewster commented Mar 29, 2017

@gpoesia looks good! Thanks!

@bors-servo r+

@bors-servo
Copy link
Contributor

bors-servo commented Mar 29, 2017

📌 Commit 09d2fad has been approved by cbrewster

@jdm
Copy link
Member

jdm commented Aug 15, 2017

@cbrewster Do you have time to continue reviewing this?

@cbrewster
Copy link
Member

cbrewster commented Aug 15, 2017

I do, but I'm not entirely sure what the correct, spec-compliant approach is for fixing this issue. It would be good to have someone else look into it.

@jdm
Copy link
Member

jdm commented Aug 15, 2017

r? @asajeffrey
Alan, could you take this over?

@highfive highfive assigned asajeffrey and unassigned cbrewster Aug 15, 2017
@asajeffrey
Copy link
Member

asajeffrey commented Aug 29, 2017

Back from vacation, I'll have a look over this.

@asajeffrey
Copy link
Member

asajeffrey commented Sep 7, 2017

Some IRC chat with @jdm: http://logs.glob.uno/?c=mozilla%23servo&s=7+Sep+2017&e=7+Sep+2017#c746388

TL;DR we shouldn't be navigating to the initial about:blank, we should just have it magically appear. The problem line is

self.navigate_or_reload_child_browsing_context(Some(load_data), NavigationType::InitialAboutBlank, false);

@asajeffrey
Copy link
Member

asajeffrey commented Sep 11, 2017

Hmm, we're already navigating the newly created browsing context when we process the iframe attributes:

self.navigate_or_reload_child_browsing_context(Some(load_data), NavigationType::Regular, false);

Can we remove the navigation when we create the browsing context?

self.navigate_or_reload_child_browsing_context(Some(load_data), NavigationType::InitialAboutBlank, false);

If we remove that navigation, there may be some extra code needed to handle the first-load-with-no-src case

let event_loop = window.dom_manipulation_task_source();
let _ = event_loop.queue(box IFrameLoadEventSteps::new(self),
window.upcast());
return;

@asajeffrey
Copy link
Member

asajeffrey commented Sep 12, 2017

Doing a bit more digging...

If we added a field to InProgressLoad tracking whether or not the document is an initial about:blank, then this would be available to script_thread::load, which creates the Document object. This might be the right place to suppress the second load event without needing any changes to the constellation.

@asajeffrey
Copy link
Member

asajeffrey commented Sep 13, 2017

And some more digging... if we add another option to DocumentSource for initial about:blank documents, and store the document source in the Document, then we can change DocumentProgressHandler::dispatch_load to be something like:

     fn dispatch_load(&self) {
         let document = self.addr.root();
         if document.browsing_context().is_none() || document.source == DocumentSource::InitialAboutBlank {
             return;
         }
         ...

This way the constellation won't get asked to forward the load event on to the parent.

@gpoesia I realize this is a very different approach from the one you were using before, does this sound sensible to you?

@asajeffrey
Copy link
Member

asajeffrey commented Sep 18, 2017

@gpoesia thoughts?

@asajeffrey
Copy link
Member

asajeffrey commented Sep 22, 2017

@gpoesia ping?

@gpoesia
Copy link
Contributor Author

gpoesia commented Sep 22, 2017

Thanks for looking into it, @asajeffrey!
I remember the issue I ran into when trying to stop the load event earlier was this one I described here: #16091 (comment)

Basically, the reflow that was caused by the second load event was actually important. Without it, the compositor thread wouldn't ever paint the iframe (and tests would hang, as a consequence). I didn't figure out why that was the case, and it's even possible it isn't true anymore.

I really like the solution of simply adding another DocumentSource. I'll have some time to implement it today and we can see if we run into same issue. Even if we do, we can try to figure out a way to circumvent it without polluting the whole code base with a special case for initial about:blanks. I'll be able to do that this weekend if need be.

@gpoesia
Copy link
Contributor Author

gpoesia commented Sep 22, 2017

@asajeffrey this is what would happen when the load event was suppressed before the second reflow: #13149

@asajeffrey
Copy link
Member

asajeffrey commented Sep 22, 2017

Hmm, so we need to find a way to trigger a reflow without a second load event.

BTW, heads up that #18587 might make a difference here!

@gpoesia gpoesia force-pushed the gpoesia:master branch from b86b66f to dd5e6c3 Sep 23, 2017
@gpoesia
Copy link
Contributor Author

gpoesia commented Sep 23, 2017

@asajeffrey I've updated the code, is that more or less what you had in mind?

Unfortunately, the navigate-child-function-parent.html WPT test also times out with this patch. I thought there was a chance that the reflow that happens in Document::maybe_queue_document_completion would suffice, if it was only that a second reflow was needed. However, I believe it's more subtle than that.

From what I could gather, we need a reflow that happens after the compositor has been notified of the iframe's existence and latest pipeline ID. Since the data flow is always Module <--> Constellation, the only way of the compositor knowing is via constellation (I think the crucial part is the message it receives from Constellation::handle_load_complete). I think a reflow is necessary after that message. Incidentally, one would happen in the second time HTMLIFrameElement::iframe_load_event_steps is called, which takes place down the rabbit role in Constellation::handle_load_complete, so tests would pass before.

I guess receiving a LoadComplete message is all the Compositor needs, but I'm not sure. My last attempt was on the safer side of letting everything that would happen in the second load event take place, except for the actual firing of the event in the element.

So:

1- Does my code implement now what you had in mind? If it doesn't, I can fix it and try to make it work.
2- If it does and you also believe there's no way of not letting the Constellation know the second load event happened, do you see another place where it would be natural to trigger a reflow before the event reaches the iframe element again?

Another possible solution I can try is to simply store a flag in HTMLIFrameElement, which says whether further load events should be suppressed. We can update this flag in HTMLIFrameElement::process_the_iframe_attributes, setting it to true in the initial about:blank special case and false otherwise. Then, we check for it in iframe_load_event_steps and do the same I was doing before, of letting the reflow happen regardless of whether the event was fired. I'll see if I can make that work today. It was one of the first things I thought when looking at the issue, but by that time it clearly seemed to be an ugly hack that didn't go to the problem's root. But if it turns out to be the case that everything in the path to the second load event except the firing itself is needed, it might just be simpler. And now I can at least explain the whole reason behind the flag in a comment. Does it sound reasonable?

@gpoesia gpoesia force-pushed the gpoesia:master branch from dd5e6c3 to 8d91a90 Sep 23, 2017
@bors-servo
Copy link
Contributor

bors-servo commented Sep 26, 2017

The latest upstream changes (presumably #18635) made this pull request unmergeable. Please resolve the merge conflicts.

@asajeffrey
Copy link
Member

asajeffrey commented Sep 26, 2017

Yes, this is the sort of thing I had in mind, yay!

There are a few nits, like it would be nice to use an enum rather than a bool for whether a document is the initial about:blank, but we can sort those out once we've worked out how to get the tests to pass.

It sounds like you have a point, we need a way for the constellation to know that the iframe has finished loading. Is this just for constellation::handle_is_ready_to_save_image, or do we need a reflow for correct execution in non-test environments?

One thing which has occurred to me is that constellation::handle_script_new_iframe shouldn't add a pending change. This would also have the effect of allowing save image earlier, but I'm concerned that it'll end up being too early, or we'll end up with weird race conditions.

@gpoesia
Copy link
Contributor Author

gpoesia commented Sep 27, 2017

As far as I could tell, this is all to satisfy constellation::handle_is_ready_to_save_image, yes. It might be the case that it serves other purposes as well, but the issue I ran into specifically happened because constellation::handle_is_ready_to_save_image would always return PipelineUnknown. But we can try to fix just that and see whether it breaks somewhere else :)

I see two ways of going about the Constellation issue: we can create a "special path" for the iframe load to reach the constellation in that case, or we can just extend the path with more flags/enums so that the event stops earlier than usual. I think this latter one is less special-cased than creating a new path (e.g. a new message type for the constellation). I'm not sure about where the reflow would fit, though. Am I right in thinking that only the script thread has enough information to issue a reflow?

@asajeffrey Do you think adding the flag in HTMLIFrameElement is too hacky? :)
I've implemented it and it seems to work.

@asajeffrey
Copy link
Member

asajeffrey commented Sep 28, 2017

@gpoesia which flag on HTMLIFrameElement were you thinking of? If it's one for "does the iframe contain the initial about:blank" can we do this by finding the nested document and asking it if it's the initial about:blank? I'd rather do that than keep track of more mutable state.

@asajeffrey
Copy link
Member

asajeffrey commented Nov 6, 2017

@gpoesia what would be the best way to move forward on this?

@asajeffrey
Copy link
Member

asajeffrey commented Aug 19, 2019

@gpoesia Are you still working on this?

@asajeffrey
Copy link
Member

asajeffrey commented Jan 6, 2020

This looks dormant, feel free to reopen it!

@asajeffrey asajeffrey closed this Jan 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

8 participants
You can’t perform that action at this time.