Bug 672667 - ' IndexedDB demo causes leaks and never-ending assertions'. r=bsmedberg+smichaud+khuey.
authorBen Turner <bent.mozilla@gmail.com>
Fri, 06 Apr 2012 13:40:10 -0700
changeset 97481 58d583e7f9b1fcc813725ed495c727d1eae4dad8
parent 97480 78e5d85cb6ac54fc50df3ceff471620a5243f22f
child 97482 9b7bea577a43a00963b65f0ce6b59a0704f7e34c
push id1116
push userlsblakk@mozilla.com
push dateMon, 16 Jul 2012 19:38:18 +0000
treeherdermozilla-beta@95f959a8b4dc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbsmedberg
bugs672667
milestone15.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 672667 - ' IndexedDB demo causes leaks and never-ending assertions'. r=bsmedberg+smichaud+khuey.
dom/indexedDB/IDBTransaction.cpp
dom/indexedDB/IDBTransaction.h
widget/nsIAppShell.idl
widget/tests/Makefile.in
widget/tests/TestAppShellSteadyState.cpp
widget/xpwidgets/nsBaseAppShell.cpp
widget/xpwidgets/nsBaseAppShell.h
--- a/dom/indexedDB/IDBTransaction.cpp
+++ b/dom/indexedDB/IDBTransaction.cpp
@@ -34,42 +34,46 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "IDBTransaction.h"
 
+#include "nsIAppShell.h"
 #include "nsIScriptContext.h"
 
 #include "mozilla/storage.h"
 #include "nsContentUtils.h"
 #include "nsDOMClassInfoID.h"
 #include "nsDOMLists.h"
 #include "nsEventDispatcher.h"
 #include "nsPIDOMWindow.h"
 #include "nsProxyRelease.h"
 #include "nsThreadUtils.h"
+#include "nsWidgetsCID.h"
 
 #include "AsyncConnectionHelper.h"
 #include "DatabaseInfo.h"
 #include "IDBCursor.h"
 #include "IDBEvents.h"
 #include "IDBFactory.h"
 #include "IDBObjectStore.h"
 #include "IndexedDatabaseManager.h"
 #include "TransactionThreadPool.h"
 
 #define SAVEPOINT_NAME "savepoint"
 
 USING_INDEXEDDB_NAMESPACE
 
 namespace {
 
+NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID);
+
 PLDHashOperator
 DoomCachedStatements(const nsACString& aQuery,
                      nsCOMPtr<mozIStorageStatement>& aStatement,
                      void* aUserArg)
 {
   CommitHelper* helper = static_cast<CommitHelper*>(aUserArg);
   helper->AddDoomedObject(aStatement);
   return PL_DHASH_REMOVE;
@@ -133,29 +137,20 @@ IDBTransaction::Create(IDBDatabase* aDat
   }
 
   if (!transaction->mCachedStatements.Init()) {
     NS_ERROR("Failed to initialize hash!");
     return nsnull;
   }
 
   if (!aDispatchDelayed) {
-    nsCOMPtr<nsIThreadInternal> thread =
-      do_QueryInterface(NS_GetCurrentThread());
-    NS_ENSURE_TRUE(thread, nsnull);
+    nsCOMPtr<nsIAppShell> appShell = do_GetService(kAppShellCID);
+    NS_ENSURE_TRUE(appShell, nsnull);
 
-    // We need the current recursion depth first.
-    PRUint32 depth;
-    nsresult rv = thread->GetRecursionDepth(&depth);
-    NS_ENSURE_SUCCESS(rv, nsnull);
-
-    NS_ASSERTION(depth, "This should never be 0!");
-    transaction->mCreatedRecursionDepth = depth - 1;
-
-    rv = thread->AddObserver(transaction);
+    nsresult rv = appShell->RunBeforeNextEvent(transaction);
     NS_ENSURE_SUCCESS(rv, nsnull);
 
     transaction->mCreating = true;
   }
 
   if (aMode != IDBTransaction::VERSION_CHANGE) {
     TransactionThreadPool* pool = TransactionThreadPool::GetOrCreate();
     pool->Dispatch(transaction, &gStartTransactionRunnable, false, nsnull);
@@ -163,17 +158,16 @@ IDBTransaction::Create(IDBDatabase* aDat
 
   return transaction.forget();
 }
 
 IDBTransaction::IDBTransaction()
 : mReadyState(IDBTransaction::INITIAL),
   mMode(IDBTransaction::READ_ONLY),
   mPendingRequests(0),
-  mCreatedRecursionDepth(0),
   mSavepointCount(0),
   mAborted(false),
   mCreating(false)
 #ifdef DEBUG
   , mFiredCompleteOrAbort(false)
 #endif
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
@@ -500,17 +494,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
   NS_CYCLE_COLLECTION_UNLINK_EVENT_HANDLER(abort)
 
   tmp->mCreatedObjectStores.Clear();
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBTransaction)
   NS_INTERFACE_MAP_ENTRY(nsIIDBTransaction)
-  NS_INTERFACE_MAP_ENTRY(nsIThreadObserver)
+  NS_INTERFACE_MAP_ENTRY(nsIRunnable)
   NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(IDBTransaction)
 NS_INTERFACE_MAP_END_INHERITING(IDBWrapperCache)
 
 NS_IMPL_ADDREF_INHERITED(IDBTransaction, IDBWrapperCache)
 NS_IMPL_RELEASE_INHERITED(IDBTransaction, IDBWrapperCache)
 
 DOMCI_DATA(IDBTransaction, IDBTransaction)
 
@@ -637,62 +631,30 @@ IDBTransaction::Abort()
 nsresult
 IDBTransaction::PreHandleEvent(nsEventChainPreVisitor& aVisitor)
 {
   aVisitor.mCanHandle = true;
   aVisitor.mParentTarget = mDatabase;
   return NS_OK;
 }
 
-// XXX Once nsIThreadObserver gets split this method will disappear.
 NS_IMETHODIMP
-IDBTransaction::OnDispatchedEvent(nsIThreadInternal* aThread)
-{
-  NS_NOTREACHED("Don't call me!");
-  return NS_ERROR_NOT_IMPLEMENTED;
-}
-
-NS_IMETHODIMP
-IDBTransaction::OnProcessNextEvent(nsIThreadInternal* aThread,
-                                   bool aMayWait,
-                                   PRUint32 aRecursionDepth)
+IDBTransaction::Run()
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
-  NS_ASSERTION(aRecursionDepth > mCreatedRecursionDepth,
-               "Should be impossible!");
-  NS_ASSERTION(mCreating, "Should be true!");
-  return NS_OK;
-}
 
-NS_IMETHODIMP
-IDBTransaction::AfterProcessNextEvent(nsIThreadInternal* aThread,
-                                      PRUint32 aRecursionDepth)
-{
-  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
-  NS_ASSERTION(aThread, "This should never be null!");
-  NS_ASSERTION(aRecursionDepth >= mCreatedRecursionDepth,
-               "Should be impossible!");
-  NS_ASSERTION(mCreating, "Should be true!");
+  // We're back at the event loop, no longer newborn.
+  mCreating = false;
 
-  if (aRecursionDepth == mCreatedRecursionDepth) {
-    // We're back at the event loop, no longer newborn.
-    mCreating = false;
-
-    // Maybe set the readyState to DONE if there were no requests generated.
-    if (mReadyState == IDBTransaction::INITIAL) {
-      mReadyState = IDBTransaction::DONE;
+  // Maybe set the readyState to DONE if there were no requests generated.
+  if (mReadyState == IDBTransaction::INITIAL) {
+    mReadyState = IDBTransaction::DONE;
 
-      if (NS_FAILED(CommitOrRollback())) {
-        NS_WARNING("Failed to commit!");
-      }
-    }
-
-    // No longer need to observe thread events.
-    if(NS_FAILED(aThread->RemoveObserver(this))) {
-      NS_ERROR("Failed to remove observer!");
+    if (NS_FAILED(CommitOrRollback())) {
+      NS_WARNING("Failed to commit!");
     }
   }
 
   return NS_OK;
 }
 
 CommitHelper::CommitHelper(
               IDBTransaction* aTransaction,
--- a/dom/indexedDB/IDBTransaction.h
+++ b/dom/indexedDB/IDBTransaction.h
@@ -42,17 +42,16 @@
 
 #include "mozilla/dom/indexedDB/IndexedDatabase.h"
 
 #include "mozIStorageConnection.h"
 #include "mozIStorageStatement.h"
 #include "mozIStorageFunction.h"
 #include "nsIIDBTransaction.h"
 #include "nsIRunnable.h"
-#include "nsIThreadInternal.h"
 
 #include "nsAutoPtr.h"
 #include "nsClassHashtable.h"
 #include "nsHashKeys.h"
 #include "nsInterfaceHashtable.h"
 
 #include "mozilla/dom/indexedDB/IDBDatabase.h"
 #include "mozilla/dom/indexedDB/IDBWrapperCache.h"
@@ -74,27 +73,27 @@ public:
   NS_IMETHOD_(nsrefcnt) AddRef() = 0;
   NS_IMETHOD_(nsrefcnt) Release() = 0;
 
   virtual nsresult NotifyTransactionComplete(IDBTransaction* aTransaction) = 0;
 };
 
 class IDBTransaction : public IDBWrapperCache,
                        public nsIIDBTransaction,
-                       public nsIThreadObserver
+                       public nsIRunnable
 {
   friend class AsyncConnectionHelper;
   friend class CommitHelper;
   friend class ThreadObserver;
   friend class TransactionThreadPool;
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIIDBTRANSACTION
-  NS_DECL_NSITHREADOBSERVER
+  NS_DECL_NSIRUNNABLE
 
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBTransaction, IDBWrapperCache)
 
   enum Mode
   {
     READ_ONLY = 0,
     READ_WRITE,
     VERSION_CHANGE
@@ -185,17 +184,16 @@ private:
   nsresult CommitOrRollback();
 
   nsRefPtr<IDBDatabase> mDatabase;
   nsRefPtr<DatabaseInfo> mDatabaseInfo;
   nsTArray<nsString> mObjectStoreNames;
   ReadyState mReadyState;
   Mode mMode;
   PRUint32 mPendingRequests;
-  PRUint32 mCreatedRecursionDepth;
 
   // Only touched on the main thread.
   NS_DECL_EVENT_HANDLER(error)
   NS_DECL_EVENT_HANDLER(complete)
   NS_DECL_EVENT_HANDLER(abort)
 
   nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
     mCachedStatements;
--- a/widget/nsIAppShell.idl
+++ b/widget/nsIAppShell.idl
@@ -33,23 +33,24 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
-#include "nsIRunnable.idl"
+
+interface nsIRunnable;
 
 /**
  * Interface for the native event system layer.  This interface is designed
  * to be used on the main application thread only.
  */
-[uuid(40bc6280-ad83-471e-b197-80ab90e2065e)]
+[uuid(2d10ca53-f143-439a-bb2e-c1fbc71f6a05)]
 interface nsIAppShell : nsISupports
 {
   /**
    * Enter an event loop.  Don't leave until exit() is called.
    */
   void run();
 
   /**
@@ -107,10 +108,17 @@ interface nsIAppShell : nsISupports
    * Allows running of a "synchronous section", in the form of an nsIRunnable
    * once the event loop has reached a "stable state". We've reached a stable
    * state when the currently executing task/event has finished, see:
    * http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#synchronous-section
    * In practice this runs aRunnable once the currently executing event
    * finishes. If called multiple times per task/event, all the runnables will
    * be executed, in the order in which runInStableState() was called.
    */
-  void runInStableState(in nsIRunnable aRunnable);
+  void runInStableState(in nsIRunnable runnable);
+
+  /**
+   * Run the given runnable before the next iteration of the event loop (this
+   * includes native events too). If a nested loop is spawned within the current
+   * event then the runnable will not be run until that loop has terminated.
+   */
+  void runBeforeNextEvent(in nsIRunnable runnable);
 };
--- a/widget/tests/Makefile.in
+++ b/widget/tests/Makefile.in
@@ -55,16 +55,18 @@ ifdef NS_ENABLE_TSF
 endif
 
 # Test disabled because it requires the internal API.  Re-enabling this test is
 # bug 652123.
 #CPP_UNIT_TESTS += TestChromeMargin.cpp  \
 #                 $(NULL)
 endif
 
+CPP_UNIT_TESTS += TestAppShellSteadyState.cpp
+
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES =
 
 _CHROME_FILES =	test_bug343416.xul \
 		test_bug429954.xul \
 		window_bug429954.xul \
 		test_bug444800.xul \
new file mode 100644
--- /dev/null
+++ b/widget/tests/TestAppShellSteadyState.cpp
@@ -0,0 +1,500 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "TestHarness.h"
+
+#include "nsIAppShell.h"
+#include "nsIAppShellService.h"
+#include "nsIDocument.h"
+#include "nsIDOMEvent.h"
+#include "nsIDOMEventListener.h"
+#include "nsIDOMEventTarget.h"
+#include "nsIDOMWindow.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIRunnable.h"
+#include "nsIURI.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsIXULWindow.h"
+
+#include "nsAppShellCID.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsNetUtil.h"
+#include "nsThreadUtils.h"
+
+#ifdef XP_WIN
+#include "Windows.h"
+#endif
+
+using namespace mozilla;
+
+typedef void (*TestFunc)(nsIAppShell*);
+
+bool gStableStateEventHasRun = false;
+
+class ExitAppShellRunnable : public nsRunnable
+{
+  nsCOMPtr<nsIAppShell> mAppShell;
+
+public:
+  ExitAppShellRunnable(nsIAppShell* aAppShell)
+  : mAppShell(aAppShell)
+  { }
+
+  NS_IMETHOD
+  Run()
+  {
+    return mAppShell->Exit();
+  }
+};
+
+class StableStateRunnable : public nsRunnable
+{
+public:
+  NS_IMETHOD
+  Run()
+  {
+    if (gStableStateEventHasRun) {
+      fail("StableStateRunnable already ran");
+    }
+    gStableStateEventHasRun = true;
+    return NS_OK;
+  }
+};
+
+class CheckStableStateRunnable : public nsRunnable
+{
+  bool mShouldHaveRun;
+
+public:
+  CheckStableStateRunnable(bool aShouldHaveRun)
+  : mShouldHaveRun(aShouldHaveRun)
+  { }
+
+  NS_IMETHOD
+  Run()
+  {
+    if (mShouldHaveRun == gStableStateEventHasRun) {
+      passed("StableStateRunnable state correct (%s)",
+             mShouldHaveRun ? "true" : "false");
+    } else {
+      fail("StableStateRunnable ran at wrong time");
+    }
+    return NS_OK;
+  }
+};
+
+class ScheduleStableStateRunnable : public CheckStableStateRunnable
+{
+protected:
+  nsCOMPtr<nsIAppShell> mAppShell;
+
+public:
+  ScheduleStableStateRunnable(nsIAppShell* aAppShell)
+  : CheckStableStateRunnable(false), mAppShell(aAppShell)
+  { }
+
+  NS_IMETHOD
+  Run()
+  {
+    CheckStableStateRunnable::Run();
+
+    nsCOMPtr<nsIRunnable> runnable = new StableStateRunnable();
+    nsresult rv = mAppShell->RunBeforeNextEvent(runnable);
+    if (NS_FAILED(rv)) {
+      fail("RunBeforeNextEvent returned failure code %u", rv);
+    }
+
+    return rv;
+  }
+};
+
+class NextTestRunnable : public nsRunnable
+{
+  nsCOMPtr<nsIAppShell> mAppShell;
+
+public:
+  NextTestRunnable(nsIAppShell* aAppShell)
+  : mAppShell(aAppShell)
+  { }
+
+  NS_IMETHOD Run();
+};
+
+class ScheduleNestedStableStateRunnable : public ScheduleStableStateRunnable
+{
+public:
+  ScheduleNestedStableStateRunnable(nsIAppShell* aAppShell)
+  : ScheduleStableStateRunnable(aAppShell)
+  { }
+
+  NS_IMETHOD
+  Run()
+  {
+    ScheduleStableStateRunnable::Run();
+
+    nsCOMPtr<nsIRunnable> runnable = new CheckStableStateRunnable(false);
+    if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+      fail("Failed to dispatch check runnable");
+    }
+
+    if (NS_FAILED(NS_ProcessPendingEvents(NULL))) {
+      fail("Failed to process all pending events");
+    }
+
+    runnable = new CheckStableStateRunnable(true);
+    if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+      fail("Failed to dispatch check runnable");
+    }
+
+    runnable = new NextTestRunnable(mAppShell);
+    if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+      fail("Failed to dispatch next test runnable");
+    }
+
+    return NS_OK;
+  }
+};
+
+class EventListener : public nsIDOMEventListener
+{
+  nsCOMPtr<nsIAppShell> mAppShell;
+
+  static nsIDOMWindowUtils* sWindowUtils;
+  static nsIAppShell* sAppShell;
+
+public:
+  NS_DECL_ISUPPORTS
+
+  EventListener(nsIAppShell* aAppShell)
+  : mAppShell(aAppShell)
+  { }
+
+  NS_IMETHOD
+  HandleEvent(nsIDOMEvent* aEvent)
+  {
+    nsString type;
+    if (NS_FAILED(aEvent->GetType(type))) {
+      fail("Failed to get event type");
+      return NS_ERROR_FAILURE;
+    }
+
+    if (type.EqualsLiteral("load")) {
+      passed("Got load event");
+
+      nsCOMPtr<nsIDOMEventTarget> target;
+      if (NS_FAILED(aEvent->GetTarget(getter_AddRefs(target)))) {
+        fail("Failed to get event type");
+        return NS_ERROR_FAILURE;
+      }
+
+      nsCOMPtr<nsIDocument> document = do_QueryInterface(target);
+      if (!document) {
+        fail("Failed to QI to nsIDocument!");
+        return NS_ERROR_FAILURE;
+      }
+
+      nsCOMPtr<nsPIDOMWindow> window = document->GetWindow();
+      if (!window) {
+        fail("Failed to get window from document!");
+        return NS_ERROR_FAILURE;
+      }
+
+      nsCOMPtr<nsIDOMWindowUtils> utils = do_GetInterface(window);
+      if (!utils) {
+        fail("Failed to get DOMWindowUtils!");
+        return NS_ERROR_FAILURE;
+      }
+
+      if (!ScheduleTimer(utils)) {
+        return NS_ERROR_FAILURE;
+      }
+
+      return NS_OK;
+    }
+
+    if (type.EqualsLiteral("keypress")) {
+      passed("Got keypress event");
+
+      nsCOMPtr<nsIRunnable> runnable = new StableStateRunnable();
+      nsresult rv = mAppShell->RunBeforeNextEvent(runnable);
+      if (NS_FAILED(rv)) {
+        fail("RunBeforeNextEvent returned failure code %u", rv);
+        return NS_ERROR_FAILURE;
+      }
+
+      return NS_OK;
+    }
+
+    fail("Got an unexpected event: %s", NS_ConvertUTF16toUTF8(type).get());
+    return NS_OK;
+  }
+
+#ifdef XP_WIN
+  static VOID CALLBACK
+  TimerCallback(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)
+  {
+    if (sWindowUtils) {
+      nsCOMPtr<nsIDOMWindowUtils> utils = dont_AddRef(sWindowUtils);
+      sWindowUtils = NULL;
+
+      if (gStableStateEventHasRun) {
+        fail("StableStateRunnable ran at wrong time");
+      } else {
+        passed("StableStateRunnable state correct (false)");
+      }
+
+      PRInt32 layout = 0x409; // US
+      PRInt32 keyCode = 0x41; // VK_A
+      NS_NAMED_LITERAL_STRING(a, "a");
+
+      if (NS_FAILED(utils->SendNativeKeyEvent(layout, keyCode, 0, a, a))) {
+        fail("Failed to synthesize native event");
+      }
+
+      return;
+    }
+
+    KillTimer(NULL, idEvent);
+
+    nsCOMPtr<nsIAppShell> appShell = dont_AddRef(sAppShell);
+
+    if (!gStableStateEventHasRun) {
+      fail("StableStateRunnable didn't run yet");
+    } else {
+      passed("StableStateRunnable state correct (true)");
+    }
+
+    nsCOMPtr<nsIRunnable> runnable = new NextTestRunnable(appShell);
+    if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+      fail("Failed to dispatch next test runnable");
+    }
+
+  }
+#endif
+
+  bool
+  ScheduleTimer(nsIDOMWindowUtils* aWindowUtils)
+  {
+#ifdef XP_WIN
+    UINT_PTR timerId = SetTimer(NULL, 0, 1000, TimerCallback);
+    if (!timerId) {
+      fail("SetTimer failed!");
+      return false;
+    }
+
+    nsCOMPtr<nsIDOMWindowUtils> utils = aWindowUtils;
+    utils.forget(&sWindowUtils);
+
+    nsCOMPtr<nsIAppShell> appShell = mAppShell;
+    appShell.forget(&sAppShell);
+
+    return true;
+#else
+    return false;
+#endif
+  }
+};
+
+nsIDOMWindowUtils* EventListener::sWindowUtils = NULL;
+nsIAppShell* EventListener::sAppShell = NULL;
+
+NS_IMPL_ISUPPORTS1(EventListener, nsIDOMEventListener)
+
+already_AddRefed<nsIAppShell>
+GetAppShell()
+{
+  static const char* platforms[] = {
+    "android", "mac", "gonk", "gtk", "os2", "qt", "win"
+  };
+
+  NS_NAMED_LITERAL_CSTRING(contractPrefix, "@mozilla.org/widget/appshell/");
+  NS_NAMED_LITERAL_CSTRING(contractSuffix, ";1");
+
+  for (size_t index = 0; index < ArrayLength(platforms); index++) {
+    nsCAutoString contractID(contractPrefix);
+    contractID.AppendASCII(platforms[index]);
+    contractID.Append(contractSuffix);
+
+    nsCOMPtr<nsIAppShell> appShell = do_GetService(contractID.get());
+    if (appShell) {
+      return appShell.forget();
+    }
+  }
+
+  return NULL;
+}
+
+void
+Test1(nsIAppShell* aAppShell)
+{
+  // Schedule stable state runnable to be run before next event.
+
+  nsCOMPtr<nsIRunnable> runnable = new StableStateRunnable();
+  if (NS_FAILED(aAppShell->RunBeforeNextEvent(runnable))) {
+    fail("RunBeforeNextEvent failed");
+  }
+
+  runnable = new CheckStableStateRunnable(true);
+  if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+    fail("Failed to dispatch check runnable");
+  }
+
+  runnable = new NextTestRunnable(aAppShell);
+  if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+    fail("Failed to dispatch next test runnable");
+  }
+}
+
+void
+Test2(nsIAppShell* aAppShell)
+{
+  // Schedule stable state runnable to be run before next event from another
+  // runnable.
+
+  nsCOMPtr<nsIRunnable> runnable = new ScheduleStableStateRunnable(aAppShell);
+  if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+    fail("Failed to dispatch schedule runnable");
+  }
+
+  runnable = new CheckStableStateRunnable(true);
+  if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+    fail("Failed to dispatch check runnable");
+  }
+
+  runnable = new NextTestRunnable(aAppShell);
+  if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+    fail("Failed to dispatch next test runnable");
+  }
+}
+
+void
+Test3(nsIAppShell* aAppShell)
+{
+  // Schedule steadystate runnable to be run before next event with nested loop.
+
+  nsCOMPtr<nsIRunnable> runnable =
+    new ScheduleNestedStableStateRunnable(aAppShell);
+  if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+    fail("Failed to dispatch schedule runnable");
+  }
+}
+
+bool
+Test4Internal(nsIAppShell* aAppShell)
+{
+#ifndef XP_WIN
+  // Not sure how to test on other platforms.
+  return false;
+#endif
+
+  nsCOMPtr<nsIAppShellService> appService =
+    do_GetService(NS_APPSHELLSERVICE_CONTRACTID);
+  if (!appService) {
+    fail("Failed to get appshell service!");
+    return false;
+  }
+
+  nsCOMPtr<nsIURI> uri;
+  if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), "about:blank", NULL))) {
+    fail("Failed to create new uri");
+    return false;
+  }
+
+  PRUint32 flags = nsIWebBrowserChrome::CHROME_DEFAULT;
+
+  nsCOMPtr<nsIXULWindow> xulWindow;
+  if (NS_FAILED(appService->CreateTopLevelWindow(NULL, uri, flags, 100, 100,
+                                                 getter_AddRefs(xulWindow)))) {
+    fail("Failed to create new window");
+    return false;
+  }
+
+  nsCOMPtr<nsIDOMWindow> window = do_GetInterface(xulWindow);
+  if (!window) {
+    fail("Can't get dom window!");
+    return false;
+  }
+
+  nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(window);
+  if (!target) {
+    fail("Can't QI to nsIDOMEventTarget!");
+    return false;
+  }
+
+  nsCOMPtr<nsIDOMEventListener> listener = new EventListener(aAppShell);
+  if (NS_FAILED(target->AddEventListener(NS_LITERAL_STRING("keypress"),
+                                         listener, false, false)) ||
+      NS_FAILED(target->AddEventListener(NS_LITERAL_STRING("load"), listener,
+                                         false, false))) {
+    fail("Can't add event listeners!");
+    return false;
+  }
+
+  return true;
+}
+
+void
+Test4(nsIAppShell* aAppShell)
+{
+  if (!Test4Internal(aAppShell)) {
+    nsCOMPtr<nsIRunnable> runnable = new NextTestRunnable(aAppShell);
+    if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+      fail("Failed to dispatch next test runnable");
+    }
+  }
+}
+
+const TestFunc gTests[] = {
+  Test1, Test2, Test3, Test4
+};
+
+size_t gTestIndex = 0;
+
+NS_IMETHODIMP
+NextTestRunnable::Run()
+{
+  if (gTestIndex > 0) {
+    passed("Finished test %u", gTestIndex);
+  }
+
+  gStableStateEventHasRun = false;
+
+  if (gTestIndex < ArrayLength(gTests)) {
+    gTests[gTestIndex++](mAppShell);
+  }
+  else {
+    nsCOMPtr<nsIRunnable> exitRunnable = new ExitAppShellRunnable(mAppShell);
+
+    nsresult rv = NS_DispatchToCurrentThread(exitRunnable);
+    if (NS_FAILED(rv)) {
+      fail("Failed to dispatch exit runnable!");
+    }
+  }
+
+  return NS_OK;
+}
+
+int main(int argc, char** argv)
+{
+  ScopedLogging log;
+  ScopedXPCOM xpcom("TestAppShellSteadyState");
+
+  if (!xpcom.failed()) {
+    nsCOMPtr<nsIAppShell> appShell = GetAppShell();
+    if (!appShell) {
+      fail("Couldn't get appshell!");
+    } else {
+      nsCOMPtr<nsIRunnable> runnable = new NextTestRunnable(appShell);
+      if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) {
+        fail("Failed to dispatch next test runnable");
+      } else if (NS_FAILED(appShell->Run())) {
+        fail("Failed to run appshell");
+      }
+    }
+  }
+
+  return gFailCount != 0;
+}
--- a/widget/xpwidgets/nsBaseAppShell.cpp
+++ b/widget/xpwidgets/nsBaseAppShell.cpp
@@ -65,17 +65,17 @@ nsBaseAppShell::nsBaseAppShell()
   , mRunning(false)
   , mExiting(false)
   , mBlockNativeEvent(false)
 {
 }
 
 nsBaseAppShell::~nsBaseAppShell()
 {
-  NS_ASSERTION(mSyncSections.Count() == 0, "Must have run all sync sections");
+  NS_ASSERTION(mSyncSections.IsEmpty(), "Must have run all sync sections");
 }
 
 nsresult
 nsBaseAppShell::Init()
 {
   // Configure ourselves as an observer for the current thread:
 
   nsCOMPtr<nsIThreadInternal> threadInt =
@@ -146,34 +146,41 @@ void
 nsBaseAppShell::DoProcessMoreGeckoEvents()
 {
   OnDispatchedEvent(nsnull);
 }
 
 
 // Main thread via OnProcessNextEvent below
 bool
-nsBaseAppShell::DoProcessNextNativeEvent(bool mayWait)
+nsBaseAppShell::DoProcessNextNativeEvent(bool mayWait, PRUint32 recursionDepth)
 {
   // The next native event to be processed may trigger our NativeEventCallback,
   // in which case we do not want it to process any thread events since we'll
   // do that when this function returns.
   //
   // If the next native event is not our NativeEventCallback, then we may end
   // up recursing into this function.
   //
   // However, if the next native event is not our NativeEventCallback, but it
   // results in another native event loop, then our NativeEventCallback could
   // fire and it will see mEventloopNestingState as eEventloopOther.
   //
   EventloopNestingState prevVal = mEventloopNestingState;
   mEventloopNestingState = eEventloopXPCOM;
 
   ++mEventloopNestingLevel;
+
   bool result = ProcessNextNativeEvent(mayWait);
+
+  // Make sure that any sync sections registered during this most recent event
+  // are run now. This is not considered a stable state because we're not back
+  // to the event loop yet.
+  RunSyncSections(false, recursionDepth);
+
   --mEventloopNestingLevel;
 
   mEventloopNestingState = prevVal;
   return result;
 }
 
 //-------------------------------------------------------------------------
 // nsIAppShell methods:
@@ -298,103 +305,162 @@ nsBaseAppShell::OnProcessNextEvent(nsITh
   mProcessedGeckoEvents = false;
 
   if (mFavorPerf <= 0 && start > mSwitchTime + mStarvationDelay) {
     // Favor pending native events
     PRIntervalTime now = start;
     bool keepGoing;
     do {
       mLastNativeEventTime = now;
-      keepGoing = DoProcessNextNativeEvent(false);
+      keepGoing = DoProcessNextNativeEvent(false, recursionDepth);
     } while (keepGoing && ((now = PR_IntervalNow()) - start) < limit);
   } else {
     // Avoid starving native events completely when in performance mode
     if (start - mLastNativeEventTime > limit) {
       mLastNativeEventTime = start;
-      DoProcessNextNativeEvent(false);
+      DoProcessNextNativeEvent(false, recursionDepth);
     }
   }
 
   while (!NS_HasPendingEvents(thr) && !mProcessedGeckoEvents) {
     // If we have been asked to exit from Run, then we should not wait for
     // events to process.  Note that an inner nested event loop causes
     // 'mayWait' to become false too, through 'mBlockedWait'.
     if (mExiting)
       mayWait = false;
 
     mLastNativeEventTime = PR_IntervalNow();
-    if (!DoProcessNextNativeEvent(mayWait) || !mayWait)
+    if (!DoProcessNextNativeEvent(mayWait, recursionDepth) || !mayWait)
       break;
   }
 
   mBlockedWait = oldBlockedWait;
 
   // Make sure that the thread event queue does not block on its monitor, as
   // it normally would do if it did not have any pending events.  To avoid
   // that, we simply insert a dummy event into its queue during shutdown.
-  if (needEvent && !mExiting && !NS_HasPendingEvents(thr)) {  
-    if (!mDummyEvent)
-      mDummyEvent = new nsRunnable();
-    thr->Dispatch(mDummyEvent, NS_DISPATCH_NORMAL);
+  if (needEvent && !mExiting && !NS_HasPendingEvents(thr)) {
+    DispatchDummyEvent(thr);
   }
 
-  // We're about to run an event, so we're in a stable state. 
-  RunSyncSections();
+  // We're about to run an event, so we're in a stable state.
+  RunSyncSections(true, recursionDepth);
 
   return NS_OK;
 }
 
+bool
+nsBaseAppShell::DispatchDummyEvent(nsIThread* aTarget)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+
+  if (!mDummyEvent)
+    mDummyEvent = new nsRunnable();
+
+  return NS_SUCCEEDED(aTarget->Dispatch(mDummyEvent, NS_DISPATCH_NORMAL));
+}
+
 void
-nsBaseAppShell::RunSyncSections()
+nsBaseAppShell::RunSyncSectionsInternal(bool aStable,
+                                        PRUint32 aThreadRecursionLevel)
 {
-  if (mSyncSections.Count() == 0) {
-    return;
+  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+  NS_ASSERTION(!mSyncSections.IsEmpty(), "Nothing to do!");
+
+  // We've got synchronous sections. Run all of them that are are awaiting a
+  // stable state if aStable is true (i.e. we really are in a stable state).
+  // Also run the synchronous sections that are simply waiting for the right
+  // combination of event loop nesting level and thread recursion level.
+  // Note that a synchronous section could add another synchronous section, so
+  // we don't remove elements from mSyncSections until all sections have been
+  // run, or else we'll screw up our iteration. Any sync sections that are not
+  // ready to be run are saved for later.
+
+  nsTArray<SyncSection> pendingSyncSections;
+
+  for (PRUint32 i = 0; i < mSyncSections.Length(); i++) {
+    SyncSection& section = mSyncSections[i];
+    if ((aStable && section.mStable) ||
+        (!section.mStable &&
+         section.mEventloopNestingLevel == mEventloopNestingLevel &&
+         section.mThreadRecursionLevel == aThreadRecursionLevel)) {
+      section.mRunnable->Run();
+    }
+    else {
+      // Add to pending list.
+      SyncSection* pending = pendingSyncSections.AppendElement();
+      section.Forget(pending);
+    }
   }
-  // We've got synchronous sections awaiting a stable state. Run
-  // all the synchronous sections. Note that a synchronous section could
-  // add another synchronous section, so we don't remove elements from
-  // mSyncSections until all sections have been run, else we'll screw up
-  // our iteration.
-  for (PRInt32 i = 0; i < mSyncSections.Count(); i++) {
-    mSyncSections[i]->Run();
+
+  mSyncSections.SwapElements(pendingSyncSections);
+}
+
+void
+nsBaseAppShell::ScheduleSyncSection(nsIRunnable* aRunnable, bool aStable)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+
+  nsIThread* thread = NS_GetCurrentThread();
+
+  // Add this runnable to our list of synchronous sections.
+  SyncSection* section = mSyncSections.AppendElement();
+  section->mStable = aStable;
+  section->mRunnable = aRunnable;
+
+  // If aStable is false then this synchronous section is supposed to run before
+  // the next event at the current nesting level. Record the event loop nesting
+  // level and the thread recursion level so that the synchronous section will
+  // run at the proper time.
+  if (!aStable) {
+    section->mEventloopNestingLevel = mEventloopNestingLevel;
+
+    nsCOMPtr<nsIThreadInternal> threadInternal = do_QueryInterface(thread);
+    NS_ASSERTION(threadInternal, "This should never fail!");
+
+    PRUint32 recursionLevel;
+    if (NS_FAILED(threadInternal->GetRecursionDepth(&recursionLevel))) {
+      NS_ERROR("This should never fail!");
+    }
+
+    // Due to the weird way that the thread recursion counter is implemented we
+    // subtract one from the recursion level if we have one.
+    section->mThreadRecursionLevel = recursionLevel ? recursionLevel - 1 : 0;
   }
-  mSyncSections.Clear();
+
+  // Ensure we've got a pending event, else the callbacks will never run.
+  if (!NS_HasPendingEvents(thread) && !DispatchDummyEvent(thread)) {
+    RunSyncSections(true, 0);
+  }
 }
 
 // Called from the main thread
 NS_IMETHODIMP
 nsBaseAppShell::AfterProcessNextEvent(nsIThreadInternal *thr,
                                       PRUint32 recursionDepth)
 {
   // We've just finished running an event, so we're in a stable state. 
-  RunSyncSections();
+  RunSyncSections(true, recursionDepth);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsBaseAppShell::Observe(nsISupports *subject, const char *topic,
                         const PRUnichar *data)
 {
   NS_ASSERTION(!strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID), "oops");
   Exit();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsBaseAppShell::RunInStableState(nsIRunnable* aRunnable)
 {
-  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
-  // Record the synchronous section, and run it with any others once
-  // we reach a stable state.
-  mSyncSections.AppendObject(aRunnable);
-
-  // Ensure we've got a pending event, else the callbacks will never run.
-  nsIThread* thread = NS_GetCurrentThread(); 
-  if (!NS_HasPendingEvents(thread) &&
-       NS_FAILED(thread->Dispatch(new nsRunnable(), NS_DISPATCH_NORMAL)))
-  {
-    // Failed to dispatch dummy event to cause sync sections to run, thread
-    // is probably done processing events, just run the sync sections now.
-    RunSyncSections();
-  }
+  ScheduleSyncSection(aRunnable, true);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsBaseAppShell::RunBeforeNextEvent(nsIRunnable* aRunnable)
+{
+  ScheduleSyncSection(aRunnable, false);
+  return NS_OK;
+}
--- a/widget/xpwidgets/nsBaseAppShell.h
+++ b/widget/xpwidgets/nsBaseAppShell.h
@@ -37,18 +37,18 @@
 
 #ifndef nsBaseAppShell_h__
 #define nsBaseAppShell_h__
 
 #include "nsIAppShell.h"
 #include "nsIThreadInternal.h"
 #include "nsIObserver.h"
 #include "nsIRunnable.h"
-#include "nsCOMArray.h"
 #include "nsCOMPtr.h"
+#include "nsTArray.h"
 #include "prinrval.h"
 
 /**
  * A singleton that manages the UI thread's event queue.  Subclass this class
  * to enable platform-specific event queue support.
  */
 class nsBaseAppShell : public nsIAppShell, public nsIThreadObserver,
                        public nsIObserver
@@ -101,22 +101,51 @@ protected:
    *   This method returns "true" if a native event was processed.
    */
   virtual bool ProcessNextNativeEvent(bool mayWait) = 0;
 
   PRInt32 mSuspendNativeCount;
   PRUint32 mEventloopNestingLevel;
 
 private:
-  bool DoProcessNextNativeEvent(bool mayWait);
+  bool DoProcessNextNativeEvent(bool mayWait, PRUint32 recursionDepth);
+
+  bool DispatchDummyEvent(nsIThread* target);
 
   /**
    * Runs all synchronous sections which are queued up in mSyncSections.
    */
-  void RunSyncSections();
+  void RunSyncSectionsInternal(bool stable, PRUint32 threadRecursionLevel);
+
+  void RunSyncSections(bool stable, PRUint32 threadRecursionLevel)
+  {
+    if (!mSyncSections.IsEmpty()) {
+      RunSyncSectionsInternal(stable, threadRecursionLevel);
+    }
+  }
+
+  void ScheduleSyncSection(nsIRunnable* runnable, bool stable);
+
+  struct SyncSection {
+    SyncSection()
+    : mStable(false), mEventloopNestingLevel(0), mThreadRecursionLevel(0)
+    { }
+
+    void Forget(SyncSection* other) {
+      other->mStable = mStable;
+      other->mEventloopNestingLevel = mEventloopNestingLevel;
+      other->mThreadRecursionLevel = mThreadRecursionLevel;
+      other->mRunnable = mRunnable.forget();
+    }
+
+    bool mStable;
+    PRUint32 mEventloopNestingLevel;
+    PRUint32 mThreadRecursionLevel;
+    nsCOMPtr<nsIRunnable> mRunnable;
+  };
 
   nsCOMPtr<nsIRunnable> mDummyEvent;
   /**
    * mBlockedWait points back to a slot that controls the wait loop in
    * an outer OnProcessNextEvent invocation.  Nested calls always set
    * it to false to unblock an outer loop, since all events may
    * have been consumed by the inner event loop(s).
    */
@@ -127,17 +156,17 @@ private:
   PRIntervalTime mSwitchTime;
   PRIntervalTime mLastNativeEventTime;
   enum EventloopNestingState {
     eEventloopNone,  // top level thread execution
     eEventloopXPCOM, // innermost native event loop is ProcessNextNativeEvent
     eEventloopOther  // innermost native event loop is a native library/plugin etc
   };
   EventloopNestingState mEventloopNestingState;
-  nsCOMArray<nsIRunnable> mSyncSections;
+  nsTArray<SyncSection> mSyncSections;
   bool mRunning;
   bool mExiting;
   /**
    * mBlockNativeEvent blocks the appshell from processing native events.
    * It is set to true while a nested native event loop (eEventloopOther)
    * is processing gecko events in NativeEventCallback(), thus queuing up
    * native events until we return to that loop (bug 420148).
    * We force mBlockNativeEvent to false in case handling one of the gecko