Re: [whatwg] Avoiding synchronous iframe load

On Tue, 15 Oct 2013, Ryosuke Niwa wrote:
> 
> I'm trying to make page loads on iframe always asynchronous in WebKit.  
> However, the current specification appears to indicate that the 
> navigation happens synchronously.

Fundamentally, navigation can't be synchronous, since the script that 
started it gets to run to completion even if it's in the iframe, the data 
usually has to come off the network, etc. So we need to be more precise in 
defining what we mean here than just "synchronous" or "asynchronous".


> Namely, in the following example, the first alert should be "true" as 
> far as I read the specification.
> 
> <script> var a = false; </script>
> <iframe src="javascript:a=true" onload="a = true"></iframe>
> <script> alert(a); /* or even setTimeout(function(){alert(a);},0); */ setTimeout(function(){alert(a);},10) </script>

Well, javascript: is a bit of a weird case. In fact I'm planning on 
revamping how that's specced shortly:

   https://www.w3.org/bugzilla_public/show_bug.cgi?id=13720

But right now, the above should alert false. Let me walk through why.

First, we start in the event loop, with one thread running. It picks up a 
parser task, the parser inserts the <iframe> with its attributes set, and 
that leads us to:

# When an iframe element is inserted into a document, the user agent must 
# create a nested browsing context, and then process the iframe attributes 
# for the first time
 -- http://www.whatwg.org/specs/web-apps/current-work/#the-iframe-element

So, first we create the nested browsing context:

# When a browsing context is first created, it must be created with a 
# single Document in its session history, whose address is about:blank,
# [...]
 -- http://www.whatwg.org/specs/web-apps/current-work/#browsing-context

...and then process the iframe attributes. This is still running 
synchronously as part of the parser task that is on the event loop.

# 3. Navigate the element's child browsing context to url.
 -- http://www.whatwg.org/specs/web-apps/current-work/#process-the-iframe-attributes

Ok, so then we navigate to the URL (which is a javascript: URL). Nothing 
interesting happens until step 14, which is still synchronous:

# 14. [...] Otherwise, fetch the new resource, with the manual redirect 
# flag set.
 -- http://www.whatwg.org/specs/web-apps/current-work/#navigate

So, we run the fetch algorithm. It does a bunch of irrelevant setup, then 
we get to step 9 (still running synchronously):

# 9. If the algorithm was not invoked with the synchronous flag, perform 
# the remaining steps asynchronously.
 -- http://www.whatwg.org/specs/web-apps/current-work/#fetch

That's us. So the "navigate" algorithm is no longer blocked by the "fetch" 
algorithm. We now have two threads, "navigate" (which is running 
synchronously from the parser task in the event loop) and "fetch".

Let's follow the navigate thread first. Next step is step 15:

# 15. If gone async is false, return to whatever algorithm invoked the 
# navigation steps and continue running these steps asynchronously.

That's us again. So now the "process the iframe attributes" algorithm is 
no longer blocked by the "navigate" algorithm. We now have three threads: 
the event loop thread, the "navigate" thread, and the "fetch" thread.

The "process the iframe attributes" has nothing more for us to do, so it 
returns, and eventually gets back to the parser, which eventually returns 
to the event loop, and starts processing tasks.

Everything past this point, therefore, happens "asynchronously".

Now, one of the things that could happen is going back to the parser and 
eventually running the script. If it is, then it'll alert false, since we 
haven't run any other scripts yet. If the script is replaced with one that 
uses setTimeout(), then it'll queue a task, and then which happens first 
is undefined, since it depends on how long the asynchronous threads take 
to run.


Let's look at those threads.

The "fetch" one first.

The next interesting step for "fetch" is step 11:

# 11. [...] Otherwise, at a time convenient to the user and the user 
# agent, download (or otherwise obtain) the resource, applying the 
# semantics of the relevant specifications (e.g. [...] dereferencing 
# javascript: URLs, etc).

The "dereferenced" algorithm for javascript does various things, including 
running the script. Unfortunately, as specced today, the script runs in an 
asynchronous fashion, which is completely bogus. This is what needs to be 
respecced (see bug above). My guess is that what we'll do is have the 
script run synchronously form "navigate" where the navigation currently 
calls "fetch", instead of going through the "fetch" algorithm. The only 
impact this would have on this analysis is when the script sets the inner 
frame's window.a property, but it's not tested by the script, so doesn't 
matter for our purposes.

Ignoring that, it goes on to say:

# 3. [...] the URL must be treated in a manner equivalent to an HTTP 
# resource with a 200 OK response whose Content-Type metadata is text/html 
# and whose response body is the return value converted to a string value
 -- http://www.whatwg.org/specs/web-apps/current-work/#concept-js-deref

The execution of the script returns the string 'true'. (Note: Chrome seems 
to have a bug here. It treats it as void for some reason.)

This in particular is relevant for the "fetch" algorithm, which, in step 
14, uses this data:

# 14. [...] When the resource is available, or if there is an error of 
# some description, queue a task that uses the resource as appropriate

That ends "fetch"'s thread.


So, now let's look at the "navigate" thread. The next interesting step is 
step 18:

# 18. Wait for one or more bytes to be available or for the user agent to 
# establish that the resource in question is empty.

This waits until the "fetch" thread does something. We've already gone 
through that, so we can continue here.

So, we get to step 23, where we treat the resource as text/html. This has 
us queue a task:

# the user agent must queue a task to create a Document object [...] 
# create an HTML parser, and associate it with the document [...] Each 
# task that the networking task source places on the task queue while the 
# fetching algorithm runs must then fill the parser's input byte stream 
# with the fetched bytes and cause the HTML parser to perform the 
# appropriate processing of the input stream [...]
 -- http://www.whatwg.org/specs/web-apps/current-work/#read-html

And so the threads have now finished, but we have some tasks queued up: 
one for creating a Document and so on, and one for handling the output 
from the javascript: script. The latter gets fed to the parser, which 
eventually runs the "the end" algorithm, which eventually reaches the 
final step which is:

# 12. Queue a task to mark the Document as completely loaded.
 -- http://www.whatwg.org/specs/web-apps/current-work/#the-end

That tasks then eventually runs, and when that runs, this text in the 
iframe section gets triggered:

# When a Document in an iframe is marked as completely loaded, the user 
# agent must synchronously run the iframe load event steps.
 -- http://www.whatwg.org/specs/web-apps/current-work/#the-iframe-element

...and finally, there we are, with the load event running and setting "a" 
to "true" in the outer Window.


This is assuming the script returns "true". If the script were to return 
void, then in step 21 of the "navigate" algorithm we'd be faced with:

# 21. If the resource's out-of-band metadata [...] requires some sort of 
# processing that will not affect the browsing context, then perform
# that processing and abort these steps.

That's what would apply if the script had returned void, since in that 
case, the deref steps say:

# If the result of executing the script is void (there is no return 
# value), then the URL must be treated in a manner equivalent to an HTTP 
# resource with an HTTP 204 No Content response.

In that _specific_ case, per spec, no 'load' event would ever actually 
fire, since the Document would never change and so its "completely loaded" 
state never gets set. (Shockingly, this appears to actually be 
interoperably implemented. Gotta love the Web.)


> Am I reading the specification wrong/missing something?  If not, could 
> you amend the specification to make page loads on an iframe always 
> asynchronous?

It's always asynchronous, for some definition of asynchronous which is 
most concisely yet precisely explained by the long explanation above.


On Thu, 17 Oct 2013, Elliott Sprehn wrote:
>
> Note that loads can never be fully async, you'd break tons of content. 
> Navigating to about:blank is synchronous.
>
> frame = document.createElement('iframe');
> document.body.appendChild(frame);
> frame.contentDocument; // synchronously available

That's not a navigation. When an <iframe> is created, it gets prefilled 
with a magic about:blank document that is just synchronously there without 
the navigation algorithm being invoked.


On Thu, 17 Oct 2013, Boris Zbarsky wrote:
> > 
> > frame = document.createElement('iframe');
> > frame.src = 'javascript:alert(1);'
> > document.body.appendChild(frame);
> > alert(2);
> 
> [...] it alerts 2 and then 1.

Per spec also.


> What the _user_ sees is the "1" and then the "2" because the newer alert 
> goes on top of the older one in the UI, then reveals the older one when 
> it's dismissed.

Per spec, the alert blocks the event loop, FWIW.


On Thu, 17 Oct 2013, Ryosuke Niwa wrote:
> 
> I'm still somewhat puzzled by the fact processing the iframe attributes 
> synchronously navigates to a new url (which itself could be async?) 
> whereas following a hyperlink simply queues a task to navigate: 
> http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#following-hyperlinks

Turns out that click on a link is "more" async than populating an iframe. 
The link behaviour is definitely required for Web compat. (See in 
particular how it interacts with form submission, which has yet a third 
even weirder way of answering the question "sync or async".)


On Fri, 18 Oct 2013, Boris Zbarsky wrote:
> 
> This algorithm then synchronously performs certain steps.  
> Specifically, checking for scroll-to-anchor and doing the scroll.  At 
> least as far as I can tell.  I don't know why it keeps checking the 
> "gone async" value in steps 11, 12, 15, since nothing in the algorithm 
> sets it to true until step 16 as far as I can tell.  Is "gone async" 
> some sort of persistent state attached to the navigation, as opposed to 
> the local variable it seems to be at step 7?

Step 17 (after going async) can jump back to step 8.


> > Perhaps step 15 on 
> > http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html#navigate 
> > indicates the specification already mandates it to be asynchronous.
> 
> I can't tell what this specification is actually saying here, 
> unfortunately. Too much indirection.  :(

If you have any suggestions on making this clearer, please let me know. 
This is probably the most complicated part of the Web platform and I've no 
idea how to make it any clearer.


> The main hard design constraint I know of here is that navigating frames 
> to about:blank via appending them to the DOM should ideally not change 
> which document scripts see in the frame (though it does in Gecko right 
> now; as I said we consider that a bug).

Not sure exactly what case you mean here. Do you mean:

   <iframe></iframe>

...or:

   <iframe src="about:blank"></iframe>

...or:

   <iframe src="about:blank?"></iframe>

...?

Right now, this reports true per spec:

   <iframe></iframe>
   <script>
    var d1 = frames[0].document;
    setTimeout(function () { var d2 = frames[0].document; w(d1===d2) }, 100);
   </script>

...but this reports false:

   <iframe src="about:blank"></iframe>
   <script>
    var d1 = frames[0].document;
    setTimeout(function () { var d2 = frames[0].document; w(d1===d2) }, 100);
   </script>

In Firefox, they both report false. In Safari and Chrome, they both report 
true. I don't really understand what Safari are Chrome are doing, given 
their behaviour with other values like "bogus:" which also returns true. 
Maybe "about:blank" is being treated like a bogus URL?


> Gecko currently has that behavior: iframe @src changes start a 
> navigation sync, while link clicks (and form submission) just post an 
> event to start a navigation.  I wonder whether the spec simply specified 
> that behavior...

Certainly wasn't "simply".

See: https://www.w3.org/Bugs/Public/show_bug.cgi?id=18854

You helped me spec this. ;-)


On Wed, 23 Oct 2013, Ryosuke Niwa wrote:
> 
> My preference is to match Firefox's behavior, and special-case 
> about:blank to navigate synchronously but fire load event 
> asynchronously. i.e. what Boris said she wants Firefox to do.
> 
> That's probably the most consistent & Web-compatible behavior we can 
> get.

As far as I can tell, this is exactly what the spec says.


On Thu, 17 Oct 2013, Boris Zbarsky wrote:
> 
> Yes, but in the case of Firefox we consider that a bug.
> 
> In particular, we believe that the behavior web authors expect is for 
> the document to be created synchronously.  When its onload fires is an 
> open question; we believe for web compat that should be async.

That's what the spec says.


> I strongly believe that async loading of javascript: is desirable (which 
> is why Gecko switched to it); if the spec requires something else I'm 
> all in favor of changing the spec.

Right now, the specific case of javascript: loading in the spec is bogus 
(it's async but in the sense of not even happening during a task, which 
is just crazy). It's going to be changed to be async on a queued task like 
"fetch", once I revamp how it's specced, as per the aforementioned bug.

-- 
Ian Hickson               U+1047E                )\._.,--....,'``.    fL
http://ln.hixie.ch/       U+263A                /,   _.. \   _\  ;`._ ,.
Things that are impossible just take longer.   `._.-(,_..'--(,_..'`-.;.'

Received on Friday, 25 October 2013 18:42:58 UTC