Bug 547399 - 'Workers: Don't let worker messages run if the worker is suspended'. r+sr=jst, a=beltzner.
authorBen Turner <bent.mozilla@gmail.com>
Mon, 05 Apr 2010 15:37:54 -0700
changeset 26872 5077906c9b46
parent 26871 9a06693cdbb4
child 26873 8cbc449170d5
push id2336
push userbturner@mozilla.com
push dateMon, 05 Apr 2010 22:38:31 +0000
reviewersbeltzner
bugs547399
milestone1.9.1.10pre
Bug 547399 - 'Workers: Don't let worker messages run if the worker is suspended'. r+sr=jst, a=beltzner.
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
@@ -847,16 +847,36 @@ public:
       nsDOMWorker* targetWorker = mToInner ? mWorker.get() : mWorker->mParent;
       NS_ASSERTION(currentWorker == targetWorker, "Wrong worker!");
     }
 #endif
     if (mWorker->IsCanceled()) {
       return NS_ERROR_ABORT;
     }
 
+    // Don't run message events targeted to terminated workers.
+    if (mWorker->IsTerminated() && !mToInner) {
+      nsCOMPtr<nsIWorkerMessageEvent> messageEvent(
+        do_QueryInterface(static_cast<nsIDOMEvent*>(mEvent)));
+      if (messageEvent) {
+        return NS_OK;
+      }
+    }
+
+    // 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<nsIDOMEventTarget*>(mWorker->GetInnerScope()) :
       static_cast<nsIDOMEventTarget*>(mWorker);
 
     NS_ASSERTION(target, "Null target!");
     NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);
 
     mEvent->SetTarget(target);
@@ -975,16 +995,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);
@@ -1176,16 +1197,19 @@ nsDOMWorker::InitializeInternal(nsIScrip
 
 void
 nsDOMWorker::Cancel()
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
   mCanceled = PR_TRUE;
 
   CancelFeatures();
+
+  // Make sure we kill any queued runnables that we never had a chance to run.
+  mQueuedRunnables.Clear();
 }
 
 void
 nsDOMWorker::Suspend()
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
   NS_ASSERTION(!mSuspended, "Suspended more than once!");
   mSuspended = PR_TRUE;
@@ -1196,16 +1220,23 @@ nsDOMWorker::Suspend()
 void
 nsDOMWorker::Resume()
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
   NS_ASSERTION(mSuspended, "Not suspended!");
   mSuspended = PR_FALSE;
 
   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();
 }
 
 nsresult
 nsDOMWorker::PostMessageInternal(const nsAString& aMessage,
                                  PRBool aIsJSON,
                                  PRBool aIsPrimitive,
                                  PRBool aToInner)
 {
@@ -1528,16 +1559,23 @@ nsDOMWorker::CancelFeatures()
 
 already_AddRefed<nsDOMWorker>
 nsDOMWorker::GetParent()
 {
   nsRefPtr<nsDOMWorker> parent(mParent);
   return parent.forget();
 }
 
+PRBool
+nsDOMWorker::QueueSuspendedRunnable(nsIRunnable* aRunnable)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+  return mQueuedRunnables.AppendElement(aRunnable) ? PR_TRUE : PR_FALSE;
+}
+
 /**
  * See nsIWorker
  */
 NS_IMETHODIMP
 nsDOMWorker::PostMessage(/* JSObject aMessage */)
 {
   if (mTerminated) {
     return NS_OK;
--- a/dom/src/threads/nsDOMWorker.h
+++ b/dom/src/threads/nsDOMWorker.h
@@ -58,16 +58,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 nsIWorkerScope,
                          public nsIDOMEventTarget,
                          public nsIXPCScriptable,
                          public nsIClassInfo
 {
@@ -100,25 +101,25 @@ class nsDOMWorker : public nsIWorker,
   friend class nsDOMWorkerFunctions;
   friend class nsDOMWorkerRefPtr;
   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.
-  friend class nsDOMFireEventRunnable;
 #endif
 
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIABSTRACTWORKER
   NS_DECL_NSIWORKER
   NS_FORWARD_SAFE_NSIDOMEVENTTARGET(mOuterHandler)
   NS_DECL_NSIXPCSCRIPTABLE
@@ -147,16 +148,20 @@ public:
   PRBool IsCanceled() {
     return mCanceled;
   }
 
   PRBool IsSuspended() {
     return mSuspended;
   }
 
+  PRBool IsTerminated() {
+    return mTerminated;
+  }
+
   PRBool SetGlobalForContext(JSContext* aCx);
 
   void SetPool(nsDOMWorkerPool* aPool);
 
   nsDOMWorkerPool* Pool() {
     return mPool;
   }
 
@@ -205,16 +210,18 @@ private:
   nsIURI* GetURI() {
     return mURI;
   }
 
   void SetURI(nsIURI* aURI) {
     mURI = aURI;
   }
 
+  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;
@@ -236,16 +243,18 @@ private:
 
   nsIXPConnectWrappedNative* mWrappedNative;
 
   nsCOMPtr<nsIPrincipal> mPrincipal;
   nsCOMPtr<nsIURI> mURI;
 
   PRInt32 mErrorHandlerRecursionCount;
 
+  nsTArray<nsCOMPtr<nsIRunnable> > mQueuedRunnables;
+
   PRPackedBool mCanceled;
   PRPackedBool mSuspended;
   PRPackedBool mCompileAttempted;
   PRPackedBool mTerminated;
 };
 
 /**
  * A worker "feature" holds the worker alive yet can be canceled, paused, and
--- a/dom/src/threads/test/Makefile.in
+++ b/dom/src/threads/test/Makefile.in
@@ -70,16 +70,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,140 @@
+<!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 = false;
+  var resumed = false;
+  var finished = false;
+
+  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() {
+    if (finished) {
+      return;
+    }
+    finished = true;
+    setCachePref(false);
+    iframe.terminateWorker();
+    SimpleTest.finish();
+  }
+
+  function waitInterval() {
+    if (finished) {
+      return;
+    }
+    is(iframe.location, "about:blank", "Wrong url!");
+    is(suspended, true, "Not suspended?");
+    is(resumed, false, "Already resumed?!");
+    is(lastCount, oldMessageCount, "Received a message while suspended!");
+    if (++waitCount == 5) {
+      clearInterval(interval);
+      resumed = true;
+      iframe.history.back();
+    }
+  }
+
+  function badOnloadCallback() {
+    if (finished) {
+      return;
+    }
+    ok(false, "shouldn't get here!");
+    finishTest();
+  }
+
+  function suspendCallback() {
+    if (finished) {
+      return;
+    }
+    is(iframe.location, "about:blank", "Wrong url!");
+    is(suspended, false, "Already suspended?");
+    is(resumed, false, "Already resumed?");
+    setCachePref(false);
+    suspended = true;
+    iframe.onload = badOnloadCallback;
+    oldMessageCount = lastCount;
+    interval = setInterval(waitInterval, 1000);
+  }
+
+  function messageCallback(data) {
+    if (finished) {
+      return;
+    }
+
+    if (!suspended) {
+      ok(lastCount === undefined || lastCount == data - 1,
+         "Data is inconsistent");
+      lastCount = data;
+      if (lastCount == 50) {
+        setCachePref(true);
+        iframe.location = "about:blank";
+      }
+      return;
+    }
+
+    var newLocation =
+      window.location.toString().replace("test_suspend.html",
+                                         "suspend_iframe.html");
+    is(newLocation.indexOf(iframe.location.toString()), 0, "Wrong url!");
+    is(resumed, true, "Got message before resumed!");
+    is(lastCount, data - 1, "Missed a message, suspend failed!");
+    finishTest();
+  }
+
+  function errorCallback(data) {
+    if (finished) {
+      return;
+    }
+    ok(false, "Iframe had an error: '" + data + "'");
+    finishTest();
+  }
+
+  function subframeLoaded() {
+    if (finished) {
+      return;
+    }
+    var iframeElement = document.getElementById("workerFrame");
+    iframeElement.onload = suspendCallback;
+
+    iframe = iframeElement.contentWindow;
+    ok(iframe, "No iframe?!");
+
+    iframe.startWorker(messageCallback, errorCallback);
+  }
+
+</script>
+</pre>
+</body>
+</html>