Bug 28293: Implement defer attribute. r/sr=jst
authorJonas Sicking <jonas@sicking.cc>
Fri, 25 Jul 2008 19:42:12 -0700
changeset 16373 5034907cae2a05eb0b8beb348122a7b9729041f1
parent 16372 6ad2e5aa3c6448851c1d6c4cb3b55faa8eca1276
child 16374 143278174a83ebc3c4402d362e993a0230d92a9a
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs28293
milestone1.9.1a2pre
Bug 28293: Implement defer attribute. r/sr=jst
content/base/public/nsIScriptElement.h
content/base/src/nsDocument.cpp
content/base/src/nsScriptLoader.cpp
content/base/src/nsScriptLoader.h
content/base/test/Makefile.in
content/base/test/file_bug28293.sjs
content/base/test/test_bug28293.html
content/html/content/src/nsHTMLScriptElement.cpp
content/svg/content/src/nsSVGScriptElement.cpp
--- a/content/base/public/nsIScriptElement.h
+++ b/content/base/public/nsIScriptElement.h
@@ -76,17 +76,22 @@ public:
   virtual already_AddRefed<nsIURI> GetScriptURI() = 0;
   
   /**
    * Script source text for inline script elements.
    */
   virtual void GetScriptText(nsAString& text) = 0;
 
   virtual void GetScriptCharset(nsAString& charset) = 0;
-  
+
+  /**
+   * Is the script deferred. Currently only supported by HTML scripts.
+   */
+  virtual PRBool GetScriptDeferred() = 0;
+
   void SetScriptLineNumber(PRUint32 aLineNumber)
   {
     mLineNumber = aLineNumber;
   }
   PRUint32 GetScriptLineNumber()
   {
     return mLineNumber;
   }
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -3169,16 +3169,20 @@ nsDocument::EndUpdate(nsUpdateType aUpda
 
 void
 nsDocument::BeginLoad()
 {
   // Block onload here to prevent having to deal with blocking and
   // unblocking it while we know the document is loading.
   BlockOnload();
 
+  if (mScriptLoader) {
+    mScriptLoader->BeginDeferringScripts();
+  }
+
   NS_DOCUMENT_NOTIFY_OBSERVERS(BeginLoad, (this));
 }
 
 PRBool
 nsDocument::CheckGetElementByIdArg(const nsIAtom* aId)
 {
   if (aId == nsGkAtoms::_empty) {
     nsContentUtils::ReportToConsole(
@@ -3412,16 +3416,20 @@ nsDocument::DispatchContentLoadedEvents(
           }
         }
       }
       
       parent = parent->GetParentDocument();
     } while (parent);
   }
 
+  if (mScriptLoader) {
+    mScriptLoader->EndDeferringScripts();
+  }
+
   UnblockOnload(PR_TRUE);
 }
 
 void
 nsDocument::EndLoad()
 {
   // Drop the ref to our parser, if any, but keep hold of the sink so that we
   // can flush it from FlushPendingNotifications as needed.  We might have to
--- a/content/base/src/nsScriptLoader.cpp
+++ b/content/base/src/nsScriptLoader.cpp
@@ -93,16 +93,17 @@ public:
   }
   void FireScriptEvaluated(nsresult aResult)
   {
     mElement->ScriptEvaluated(aResult, mElement, mIsInline);
   }
 
   nsCOMPtr<nsIScriptElement> mElement;
   PRPackedBool mLoading;             // Are we still waiting for a load to complete?
+  PRPackedBool mDefer;               // Is execution defered?
   PRPackedBool mIsInline;            // Is the script inline or loaded?
   nsString mScriptText;              // Holds script for loaded scripts
   PRUint32 mJSVersion;
   nsCOMPtr<nsIURI> mURI;
   PRInt32 mLineNo;
 };
 
 // The nsScriptLoadRequest is passed as the context to necko, and thus
@@ -120,18 +121,18 @@ nsScriptLoader::nsScriptLoader(nsIDocume
     mEnabled(PR_TRUE)
 {
 }
 
 nsScriptLoader::~nsScriptLoader()
 {
   mObservers.Clear();
 
-  for (PRInt32 i = 0; i < mPendingRequests.Count(); i++) {
-    mPendingRequests[i]->FireScriptAvailable(NS_ERROR_ABORT);
+  for (PRInt32 i = 0; i < mRequests.Count(); i++) {
+    mRequests[i]->FireScriptAvailable(NS_ERROR_ABORT);
   }
 
   // Unblock the kids, in case any of them moved to a different document
   // subtree in the meantime and therefore aren't actually going away.
   for (PRUint32 j = 0; j < mPendingChildLoaders.Length(); ++j) {
     mPendingChildLoaders[j]->RemoveExecuteBlocker();
   }  
 }
@@ -374,16 +375,20 @@ nsScriptLoader::ProcessScriptElement(nsI
 
   nsCOMPtr<nsIContent> eltContent(do_QueryInterface(aElement));
   eltContent->SetScriptTypeID(typeID);
 
   // Create a request object for this script
   nsRefPtr<nsScriptLoadRequest> request = new nsScriptLoadRequest(aElement, version);
   NS_ENSURE_TRUE(request, NS_ERROR_OUT_OF_MEMORY);
 
+  request->mDefer = mDeferEnabled && aElement->GetScriptDeferred();
+
+  PRBool hadPendingRequests = !!GetFirstPendingRequest();
+
   // First check to see if this is an external script
   nsCOMPtr<nsIURI> scriptURI = aElement->GetScriptURI();
   if (scriptURI) {
     // Check that the containing page is allowed to load this URI.
     rv = nsContentUtils::GetSecurityManager()->
       CheckLoadURIWithPrincipal(mDocument->NodePrincipal(), scriptURI,
                                 nsIScriptSecurityManager::ALLOW_CHROME);
 
@@ -443,30 +448,33 @@ nsScriptLoader::ProcessScriptElement(nsI
     request->mLoading = PR_FALSE;
     request->mIsInline = PR_TRUE;
     request->mURI = mDocument->GetDocumentURI();
 
     request->mLineNo = aElement->GetScriptLineNumber();
 
     // If we've got existing pending requests, add ourselves
     // to this list.
-    if (mPendingRequests.Count() == 0 && ReadyToExecuteScripts() &&
-        nsContentUtils::IsSafeToRunScript()) {
+    if (!request->mDefer && !hadPendingRequests &&
+        ReadyToExecuteScripts() && nsContentUtils::IsSafeToRunScript()) {
       return ProcessRequest(request);
     }
   }
 
-  // Add the request to our pending requests list
-  NS_ENSURE_TRUE(mPendingRequests.AppendObject(request),
+  // Add the request to our requests list
+  NS_ENSURE_TRUE(mRequests.AppendObject(request),
                  NS_ERROR_OUT_OF_MEMORY);
 
+  if (request->mDefer) {
+    return NS_OK;
+  }
+
   // If there weren't any pending requests before, and this one is
   // ready to execute, do that as soon as it's safe.
-  if (mPendingRequests.Count() == 1 && !request->mLoading &&
-      ReadyToExecuteScripts()) {
+  if (!request->mLoading && !hadPendingRequests && ReadyToExecuteScripts()) {
     nsContentUtils::AddScriptRunner(new nsRunnableMethod<nsScriptLoader>(this,
       &nsScriptLoader::ProcessPendingRequests));
   }
 
   // Added as pending request, now we can send blocking back
   return NS_ERROR_HTMLPARSER_BLOCK;
 }
 
@@ -607,34 +615,47 @@ nsScriptLoader::EvaluateScript(nsScriptL
                    "JS_ReportPendingException wasn't called");
       ncc->SetExceptionWasThrown(PR_FALSE);
     }
     ::JS_EndRequest(cx);
   }
   return rv;
 }
 
+nsScriptLoadRequest*
+nsScriptLoader::GetFirstPendingRequest()
+{
+  for (PRInt32 i = 0; i < mRequests.Count(); ++i) {
+    if (!mRequests[i]->mDefer) {
+      return mRequests[i];
+    }
+  }
+
+  return nsnull;
+}
+
 void
 nsScriptLoader::ProcessPendingRequestsAsync()
 {
-  if (mPendingRequests.Count() || !mPendingChildLoaders.IsEmpty()) {
+  if (GetFirstPendingRequest() || !mPendingChildLoaders.IsEmpty()) {
     nsCOMPtr<nsIRunnable> ev = new nsRunnableMethod<nsScriptLoader>(this,
       &nsScriptLoader::ProcessPendingRequests);
 
     NS_DispatchToCurrentThread(ev);
   }
 }
 
 void
 nsScriptLoader::ProcessPendingRequests()
 {
   nsRefPtr<nsScriptLoadRequest> request;
-  while (mPendingRequests.Count() && ReadyToExecuteScripts() &&
-         !(request = mPendingRequests[0])->mLoading) {
-    mPendingRequests.RemoveObjectAt(0);
+  while (ReadyToExecuteScripts() &&
+         (request = GetFirstPendingRequest()) &&
+         !request->mLoading) {
+    mRequests.RemoveObject(request);
     ProcessRequest(request);
   }
 
   while (!mPendingChildLoaders.IsEmpty() && ReadyToExecuteScripts()) {
     nsRefPtr<nsScriptLoader> child = mPendingChildLoaders[0];
     mPendingChildLoaders.RemoveElementAt(0);
     child->RemoveExecuteBlocker();
   }
@@ -795,17 +816,17 @@ nsScriptLoader::OnStreamComplete(nsIStre
 {
   nsScriptLoadRequest* request = static_cast<nsScriptLoadRequest*>(aContext);
   NS_ASSERTION(request, "null request in stream complete handler");
   NS_ENSURE_TRUE(request, NS_ERROR_FAILURE);
 
   nsresult rv = PrepareLoadedRequest(request, aLoader, aStatus, aStringLen,
                                      aString);
   if (NS_FAILED(rv)) {
-    mPendingRequests.RemoveObject(request);
+    mRequests.RemoveObject(request);
     FireScriptAvailable(rv, request);
   }
 
   // Process our request and/or any pending ones
   ProcessPendingRequests();
 
   return NS_OK;
 }
@@ -858,17 +879,17 @@ nsScriptLoader::PrepareLoadedRequest(nsS
       return NS_ERROR_NOT_AVAILABLE;
     }
   }
 
   // This assertion could fire errorously if we ran out of memory when
   // inserting the request in the array. However it's an unlikely case
   // so if you see this assertion it is likely something else that is
   // wrong, especially if you see it more than once.
-  NS_ASSERTION(mPendingRequests.IndexOf(aRequest) >= 0,
+  NS_ASSERTION(mRequests.IndexOf(aRequest) >= 0,
                "aRequest should be pending!");
 
   // Mark this as loaded
   aRequest->mLoading = PR_FALSE;
 
   return NS_OK;
 }
 
@@ -896,8 +917,19 @@ nsScriptLoader::ShouldExecuteScript(nsID
   NS_ASSERTION(channelPrincipal, "Gotta have a principal here!");
 
   // If the channel principal isn't at least as powerful as the
   // document principal, then we don't execute the script.
   PRBool subsumes;
   rv = channelPrincipal->Subsumes(docPrincipal, &subsumes);
   return NS_SUCCEEDED(rv) && subsumes;
 }
+
+void
+nsScriptLoader::EndDeferringScripts()
+{
+  mDeferEnabled = PR_FALSE;
+  for (PRUint32 i = 0; i < mRequests.Count(); ++i) {
+    mRequests[i]->mDefer = PR_FALSE;
+  }
+
+  ProcessPendingRequests();
+}
--- a/content/base/src/nsScriptLoader.h
+++ b/content/base/src/nsScriptLoader.h
@@ -182,16 +182,34 @@ public:
 
   /**
    * Check whether it's OK to execute a script loaded via aChannel in
    * aDocument.
    */
   static PRBool ShouldExecuteScript(nsIDocument* aDocument,
                                     nsIChannel* aChannel);
 
+  /**
+   * Starts deferring deferred scripts and puts them in the mDeferredRequests
+   * queue instead.
+   */
+  void BeginDeferringScripts()
+  {
+    mDeferEnabled = PR_TRUE;
+  }
+
+  /**
+   * Stops defering scripts and immediately processes the mDeferredRequests
+   * queue.
+   *
+   * WARNING: This function will syncronously execute content scripts, so be
+   * prepared that the world might change around you.
+   */
+  void EndDeferringScripts();
+
 protected:
   /**
    * Process any pending requests asyncronously (i.e. off an event) if there
    * are any. Note that this is a no-op if there aren't any currently pending
    * requests.
    */
   virtual void ProcessPendingRequestsAsync();
 
@@ -224,19 +242,23 @@ protected:
                           const nsAFlatString& aScript);
 
   nsresult PrepareLoadedRequest(nsScriptLoadRequest* aRequest,
                                 nsIStreamLoader* aLoader,
                                 nsresult aStatus,
                                 PRUint32 aStringLen,
                                 const PRUint8* aString);
 
+  // Returns the first pending (non deferred) request
+  nsScriptLoadRequest* GetFirstPendingRequest();
+
   nsIDocument* mDocument;                   // [WEAK]
   nsCOMArray<nsIScriptLoaderObserver> mObservers;
-  nsCOMArray<nsScriptLoadRequest> mPendingRequests;
+  nsCOMArray<nsScriptLoadRequest> mRequests;
   nsCOMPtr<nsIScriptElement> mCurrentScript;
   // XXXbz do we want to cycle-collect these or something?  Not sure.
   nsTArray< nsRefPtr<nsScriptLoader> > mPendingChildLoaders;
   PRUint32 mBlockerCount;
   PRPackedBool mEnabled;
+  PRPackedBool mDeferEnabled;
 };
 
 #endif //__nsScriptLoader_h__
--- a/content/base/test/Makefile.in
+++ b/content/base/test/Makefile.in
@@ -187,16 +187,18 @@ include $(topsrcdir)/config/rules.mk
 		test_bug444722.html \
 		test_text_replaceWholeText.html \
 		test_text_wholeText.html \
 		wholeTexty-helper.xml \
 		test_bug444030.xhtml \
 		test_NodeIterator_basics_filters.xhtml \
 		test_NodeIterator_mutations_1.xhtml \
 		test_NodeIterator_mutations_2.html \
+		test_bug28293.html \
+		file_bug28293.sjs \
 		$(NULL)
 
 libs:: $(_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
 
 check::
 	@$(EXIT_ON_ERROR) \
 	for f in $(subst .cpp,,$(CPP_UNIT_TESTS)); do \
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_bug28293.sjs
@@ -0,0 +1,5 @@
+function handleRequest(request, response)
+{
+  response.setHeader("Content-Type", "text/plain", false);
+  response.write(decodeURIComponent(request.queryString));
+}
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_bug28293.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=28293
+-->
+<head>
+  <title>Test for Bug 28293</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>        
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script>
+res = 'A';
+
+SimpleTest.waitForExplicitFinish();
+onload = function () {
+
+  res+='2';
+
+  s = document.createElement('script');
+  s.textContent="res+='g';";
+  s.defer = true;
+  document.body.appendChild(s);
+
+  res+='3';
+
+  s = document.createElement('script');
+  s.src="file_bug28293.sjs?res+='h';";
+  s.defer = true;
+  document.body.appendChild(s);
+
+  s = document.createElement('script');
+  s.textContent="res+='i';done()";
+  s.defer = true;
+  document.body.appendChild(s);
+
+  res+='4';
+}
+
+function done() {
+  is(res, "ABCDEFGHIJ1abcdefM2g34hi", "scripts executed in the wrong order");
+  ok(!fHadExecuted, "Dynamic script executed too late");
+  SimpleTest.finish();
+}
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=28293">Mozilla Bug 28293</a>
+
+<script defer>
+res += 'a';
+</script>
+<script defer src="data:text/plain,res+='b'"></script>
+<script defer>
+res += 'c';
+</script>
+<script>
+res += 'B';
+</script>
+<script>
+res += 'C';
+
+s = document.createElement('script');
+s.src="file_bug28293.sjs?res+='d';";
+s.defer = true;
+document.body.appendChild(s);
+
+s = document.createElement('script');
+s.textContent="res+='D';";
+document.body.appendChild(s);
+
+res += 'E';
+</script>
+<script>
+res += 'F';
+document.addEventListener("DOMContentLoaded", function() {
+  res += '1'
+  s = document.createElement('script');
+  s.src="file_bug28293.sjs?res+='M';";
+  document.body.appendChild(s);
+}, false);
+res += 'G';
+</script>
+<script defer>
+res += 'e';
+</script>
+<script src="file_bug28293.sjs?res+='H';"></script>
+<script>
+res += 'I';
+s = document.createElement('script');
+s.src="file_bug28293.sjs?fHadExecuted=(res.indexOf('f')>=0);";
+document.body.appendChild(s);
+res += 'J';
+</script>
+<script defer>
+res += 'f';
+</script>
+
+</body>
+</html>
--- a/content/html/content/src/nsHTMLScriptElement.cpp
+++ b/content/html/content/src/nsHTMLScriptElement.cpp
@@ -332,17 +332,18 @@ public:
 
   // nsIDOMHTMLScriptElement
   NS_DECL_NSIDOMHTMLSCRIPTELEMENT
 
   // nsIScriptElement
   virtual void GetScriptType(nsAString& type);
   virtual already_AddRefed<nsIURI> GetScriptURI();
   virtual void GetScriptText(nsAString& text);
-  virtual void GetScriptCharset(nsAString& charset); 
+  virtual void GetScriptCharset(nsAString& charset);
+  virtual PRBool GetScriptDeferred();
 
   // nsIContent
   virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
                               nsIContent* aBindingParent,
                               PRBool aCompileEventHandlers);
 
   virtual nsresult GetInnerHTML(nsAString& aInnerHTML);
   virtual nsresult SetInnerHTML(const nsAString& aInnerHTML);
@@ -520,16 +521,25 @@ nsHTMLScriptElement::GetScriptText(nsASt
 
 void
 nsHTMLScriptElement::GetScriptCharset(nsAString& charset)
 {
   GetCharset(charset);
 }
 
 PRBool
+nsHTMLScriptElement::GetScriptDeferred()
+{
+  PRBool defer;
+  GetDefer(&defer);
+
+  return defer;
+}
+
+PRBool
 nsHTMLScriptElement::HasScriptContent()
 {
   return HasAttr(kNameSpaceID_None, nsGkAtoms::src) ||
          nsContentUtils::HasNonEmptyTextContent(this);
 }
 
 nsresult
 nsHTMLScriptElement::MaybeProcessScript()
--- a/content/svg/content/src/nsSVGScriptElement.cpp
+++ b/content/svg/content/src/nsSVGScriptElement.cpp
@@ -73,17 +73,18 @@ public:
   NS_FORWARD_NSIDOMNODE(nsSVGScriptElementBase::)
   NS_FORWARD_NSIDOMELEMENT(nsSVGScriptElementBase::)
   NS_FORWARD_NSIDOMSVGELEMENT(nsSVGScriptElementBase::)
 
   // nsIScriptElement
   virtual void GetScriptType(nsAString& type);
   virtual already_AddRefed<nsIURI> GetScriptURI();
   virtual void GetScriptText(nsAString& text);
-  virtual void GetScriptCharset(nsAString& charset); 
+  virtual void GetScriptCharset(nsAString& charset);
+  virtual PRBool GetScriptDeferred();
 
   // nsScriptElement
   virtual PRBool HasScriptContent();
 
   // nsSVGElement specializations:
   virtual void DidChangeString(PRUint8 aAttrEnum, PRBool aDoSetAttr);
 
   // nsIContent specializations:
@@ -207,16 +208,22 @@ nsSVGScriptElement::GetScriptText(nsAStr
 }
 
 void
 nsSVGScriptElement::GetScriptCharset(nsAString& charset)
 {
   charset.Truncate();
 }
 
+PRBool
+nsSVGScriptElement::GetScriptDeferred()
+{
+  return PR_FALSE;
+}
+
 //----------------------------------------------------------------------
 // nsScriptElement methods
 
 PRBool
 nsSVGScriptElement::HasScriptContent()
 {
   return !mStringAttributes[HREF].GetAnimValue().IsEmpty() ||
          nsContentUtils::HasNonEmptyTextContent(this);