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 93427 58d583e7f9b1fcc813725ed495c727d1eae4dad8
parent 93426 78e5d85cb6ac54fc50df3ceff471620a5243f22f
child 93428 9b7bea577a43a00963b65f0ce6b59a0704f7e34c
push idunknown
push userunknown
push dateunknown
reviewersbsmedberg
bugs672667
milestone15.0a1
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