Bug 547399 - 'Workers: Don't let worker messages run if the worker is suspended'. r+sr=jst.
authorBen Turner <bent.mozilla@gmail.com>
Thu, 11 Mar 2010 12:36:44 -0800
changeset 39293 f4ce6c16c48b
parent 39292 e68368ef5c04
child 39294 06b335512dc6
push id12121
push userbturner@mozilla.com
push dateThu, 11 Mar 2010 20:37:10 +0000
treeherdermozilla-central@06b335512dc6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs547399
milestone1.9.3a3pre
Bug 547399 - 'Workers: Don't let worker messages run if the worker is suspended'. r+sr=jst.
dom/src/threads/nsDOMWorker.cpp
dom/src/threads/nsDOMWorker.h
dom/src/threads/test/Makefile.in
dom/src/threads/test/suspend_iframe.html
dom/src/threads/test/suspend_worker.js
dom/src/threads/test/test_suspend.html
--- a/dom/src/threads/nsDOMWorker.cpp
+++ b/dom/src/threads/nsDOMWorker.cpp
@@ -928,16 +928,27 @@ public:
       nsDOMWorker* targetWorker = mToInner ? mWorker.get() : mWorker->mParent;
       NS_ASSERTION(currentWorker == targetWorker, "Wrong worker!");
     }
 #endif
     if (mWorker->IsCanceled()) {
       return NS_ERROR_ABORT;
     }
 
+    // If the worker is suspended and we're running on the main thread then we
+    // can't actually dispatch the event yet. Instead we queue it for whenever
+    // we resume.
+    if (mWorker->IsSuspended() && NS_IsMainThread()) {
+      if (!mWorker->QueueSuspendedRunnable(this)) {
+        NS_ERROR("Out of memory?!");
+        return NS_ERROR_ABORT;
+      }
+      return NS_OK;
+    }
+
     nsCOMPtr<nsIDOMEventTarget> target = mToInner ?
       static_cast<nsDOMWorkerMessageHandler*>(mWorker->GetInnerScope()) :
       static_cast<nsDOMWorkerMessageHandler*>(mWorker);
 
     NS_ASSERTION(target, "Null target!");
     NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);
 
     mEvent->SetTarget(target);
@@ -1039,16 +1050,17 @@ nsDOMWorker::~nsDOMWorker()
     mPool->NoteDyingWorker(this);
   }
 
   if (mLock) {
     nsAutoLock::DestroyLock(mLock);
   }
 
   NS_ASSERTION(!mFeatures.Length(), "Live features!");
+  NS_ASSERTION(!mQueuedRunnables.Length(), "Events that never ran!");
 
   nsCOMPtr<nsIThread> mainThread;
   NS_GetMainThread(getter_AddRefs(mainThread));
 
   nsIPrincipal* principal;
   mPrincipal.forget(&principal);
   if (principal) {
     NS_ProxyRelease(mainThread, principal, PR_FALSE);
@@ -1344,16 +1356,19 @@ nsDOMWorker::Kill()
     mFeatures.Clear();
   }
 
   count = features.Length();
   for (index = 0; index < count; index++) {
     features[index]->Cancel();
   }
 
+  // Make sure we kill any queued runnables that we never had a chance to run.
+  mQueuedRunnables.Clear();
+
   // We no longer need to keep our inner scope.
   mInnerScope = nsnull;
   mScopeWN = nsnull;
   mGlobal = NULL;
 
   // And we can let our parent die now too.
   mParent = nsnull;
   mParentWN = nsnull;
@@ -1395,16 +1410,23 @@ nsDOMWorker::Resume()
 #endif
     shouldResumeFeatures = mSuspended;
     mSuspended = PR_FALSE;
   }
 
   if (shouldResumeFeatures) {
     ResumeFeatures();
   }
+
+  // Repost any events that were queued for the main thread while suspended.
+  PRUint32 count = mQueuedRunnables.Length();
+  for (PRUint32 index = 0; index < count; index++) {
+    NS_DispatchToCurrentThread(mQueuedRunnables[index]);
+  }
+  mQueuedRunnables.Clear();
 }
 
 PRBool
 nsDOMWorker::IsCanceled()
 {
   nsAutoLock lock(mLock);
 
   // There are several conditions under which we want JS code to abort and all
@@ -1929,16 +1951,23 @@ nsDOMWorker::SetExpirationTime(PRInterva
 PRIntervalTime
 nsDOMWorker::GetExpirationTime()
 {
   nsAutoLock lock(mLock);
   return mExpirationTime;
 }
 #endif
 
+PRBool
+nsDOMWorker::QueueSuspendedRunnable(nsIRunnable* aRunnable)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+  return mQueuedRunnables.AppendElement(aRunnable) ? PR_TRUE : PR_FALSE;
+}
+
 NS_IMETHODIMP
 nsDOMWorker::AddEventListener(const nsAString& aType,
                               nsIDOMEventListener* aListener,
                               PRBool aUseCapture)
 {
   return AddEventListener(aType, aListener, aUseCapture, PR_FALSE, 0);
 }
 
--- a/dom/src/threads/nsDOMWorker.h
+++ b/dom/src/threads/nsDOMWorker.h
@@ -59,16 +59,17 @@ class nsDOMWorker;
 class nsDOMWorkerFeature;
 class nsDOMWorkerMessageHandler;
 class nsDOMWorkerNavigator;
 class nsDOMWorkerPool;
 class nsDOMWorkerTimeout;
 class nsICancelable;
 class nsIDOMEventListener;
 class nsIEventTarget;
+class nsIRunnable;
 class nsIScriptGlobalObject;
 class nsIXPConnectWrappedNative;
 
 class nsDOMWorkerScope : public nsDOMWorkerMessageHandler,
                          public nsIWorkerScope,
                          public nsIXPCScriptable
 {
   friend class nsDOMWorker;
@@ -113,16 +114,17 @@ class nsDOMWorker : public nsDOMWorkerMe
   friend class nsDOMWorkerFeature;
   friend class nsDOMWorkerFunctions;
   friend class nsDOMWorkerScope;
   friend class nsDOMWorkerScriptLoader;
   friend class nsDOMWorkerTimeout;
   friend class nsDOMWorkerXHR;
   friend class nsDOMWorkerXHRProxy;
   friend class nsReportErrorRunnable;
+  friend class nsDOMFireEventRunnable;
 
   friend JSBool DOMWorkerOperationCallback(JSContext* aCx);
   friend void DOMWorkerErrorReporter(JSContext* aCx,
                                      const char* aMessage,
                                      JSErrorReport* aReport);
 
 #ifdef DEBUG
   // For fun assertions.
@@ -283,16 +285,18 @@ private:
   nsresult Close();
 
   nsresult TerminateInternal(PRBool aFromFinalize);
 
   nsIWorkerLocation* GetLocation() {
     return mLocation;
   }
 
+  PRBool QueueSuspendedRunnable(nsIRunnable* aRunnable);
+
 private:
 
   // mParent will live as long as mParentWN but only mParentWN will keep the JS
   // reflection alive, so we only hold one strong reference to mParentWN.
   nsDOMWorker* mParent;
   nsCOMPtr<nsIXPConnectWrappedNative> mParentWN;
 
   PRLock* mLock;
@@ -322,16 +326,18 @@ private:
 
   // Always protected by mLock
   PRIntervalTime mExpirationTime;
 
   nsCOMPtr<nsITimer> mKillTimer;
 
   nsCOMPtr<nsIWorkerLocation> mLocation;
 
+  nsTArray<nsCOMPtr<nsIRunnable> > mQueuedRunnables;
+
   PRPackedBool mSuspended;
   PRPackedBool mCompileAttempted;
 };
 
 /**
  * A worker "feature" holds the worker alive yet can be canceled, paused, and
  * resumed by the worker. It is up to each derived class to implement these
  * methods. This class uses a custom implementation of Release in order to
--- a/dom/src/threads/test/Makefile.in
+++ b/dom/src/threads/test/Makefile.in
@@ -77,16 +77,19 @@ include $(topsrcdir)/config/rules.mk
   test_regExpStatics.html \
   regExpStatics_worker.js \
   test_relativeLoad.html \
   relativeLoad_worker.js \
   relativeLoad_worker2.js \
   relativeLoad_import.js \
   test_scopeOnerror.html \
   scopeOnerror_worker.js \
+  test_suspend.html \
+  suspend_iframe.html \
+  suspend_worker.js \
   test_simpleThread.html \
   simpleThread_worker.js \
   test_terminate.html \
   terminate_worker.js \
   test_threadErrors.html \
   threadErrors_worker1.js \
   threadErrors_worker2.js \
   threadErrors_worker3.js \
new file mode 100644
--- /dev/null
+++ b/dom/src/threads/test/suspend_iframe.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for DOM Worker Threads Suspending</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="output"></div>
+<script class="testbody" type="text/javascript">
+
+  var output = document.getElementById("output");
+
+  var worker;
+
+  function terminateWorker() {
+    if (worker) {
+      worker.terminate();
+      worker = null;
+    }
+  }
+
+  function startWorker(messageCallback, errorCallback) {
+    worker = new Worker("suspend_worker.js");
+
+    worker.onmessage = function(event) {
+      output.textContent = output.textContent + event.data + "\n";
+      messageCallback(event.data);
+    };
+
+    worker.onerror = function(event) {
+      this.terminate();
+      errorCallback(event.message);
+    };
+  }
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/src/threads/test/suspend_worker.js
@@ -0,0 +1,5 @@
+var counter = 0;
+
+setInterval(function() {
+  postMessage(++counter);
+}, 100);
new file mode 100644
--- /dev/null
+++ b/dom/src/threads/test/test_suspend.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for DOM Worker Threads</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<iframe id="workerFrame" src="suspend_iframe.html" onload="subframeLoaded();">
+</iframe>
+<script class="testbody" type="text/javascript">
+
+  SimpleTest.waitForExplicitFinish();
+
+  var iframe;
+  var lastCount;
+
+  var suspended;
+  var resumed;
+
+  var interval;
+  var oldMessageCount;
+  var waitCount = 0;
+
+  function setCachePref(enabled) {
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    var prefBranch = Components.classes["@mozilla.org/preferences-service;1"]
+                               .getService(Components.interfaces.nsIPrefBranch);
+    if (enabled) {
+      prefBranch.setBoolPref("browser.sessionhistory.cache_subframes", true);
+    }
+    else {
+      try {
+        prefBranch.clearUserPref("browser.sessionhistory.cache_subframes");
+      } catch (e) { /* Pref didn't exist, meh */ }
+    }
+  }
+
+  function finishTest() {
+    setCachePref(false);
+    iframe.terminateWorker();
+    SimpleTest.finish();
+  }
+
+  function waitInterval() {
+    is(iframe.location, "about:blank", "Wrong url!");
+    is(suspended, true, "Not suspended?");
+    is(lastCount, oldMessageCount, "Received a message while suspended!");
+    if (++waitCount == 5) {
+      clearInterval(interval);
+      iframe.history.back();
+      resumed = true;
+    }
+  }
+
+  function badOnloadCallback() {
+    ok(false, "iframe didn't go into fastback cache!");
+    finishTest();
+  }
+
+  function suspendCallback() {
+    is(iframe.location, "about:blank", "Wrong url!");
+    is(suspended, true, "Not suspended?");
+    iframe.onload = badOnloadCallback;
+    oldMessageCount = lastCount;
+    interval = setInterval(waitInterval, 1000);
+  }
+
+  function messageCallback(data) {
+    if (!suspended) {
+      ok(lastCount === undefined || lastCount == data - 1,
+         "Data is inconsistent");
+      lastCount = data;
+      if (lastCount == 50) {
+        setCachePref(true);
+        iframe.location = "about:blank";
+        suspended = true;
+      }
+    }
+    else {
+      var newLocation =
+        window.location.toString().replace("test_suspend.html",
+                                           "suspend_iframe.html");
+      is(iframe.location, newLocation, "Wrong url!");
+      ok(suspended && resumed, "Got message before resumed!");
+      is(lastCount, data - 1, "Missed a message, suspend failed!");
+      finishTest();
+    }
+  }
+
+  function errorCallback(data) {
+    ok(false, "Iframe had an error: '" + data + "'");
+    finishTest();
+  }
+
+  function subframeLoaded() {
+    var iframeElement = document.getElementById("workerFrame");
+    iframeElement.onload = suspendCallback;
+
+    iframe = iframeElement.contentWindow;
+    ok(iframe, "No iframe?!");
+
+    iframe.startWorker(messageCallback, errorCallback);
+  }
+
+</script>
+</pre>
+</body>
+</html>