Bug 939196 - Allow nsThread to have nested event queues, r=bsmedberg.
authorBen Turner <bent.mozilla@gmail.com>
Thu, 14 Nov 2013 10:06:17 -0800
changeset 159852 499de7433a6ad869423d718f067403426fde5799
parent 159851 072161a22749bb16a76ca78c956c6997a5295bfb
child 159853 11f269e4597e92d47ee33ce9197353c20292c5e1
push idunknown
push userunknown
push dateunknown
reviewersbsmedberg
bugs939196
milestone29.0a1
Bug 939196 - Allow nsThread to have nested event queues, r=bsmedberg.
xpcom/threads/nsIThreadInternal.idl
xpcom/threads/nsThread.cpp
xpcom/threads/nsThread.h
--- a/xpcom/threads/nsIThreadInternal.idl
+++ b/xpcom/threads/nsIThreadInternal.idl
@@ -8,17 +8,17 @@
 
 interface nsIRunnable;
 interface nsIThreadObserver;
 
 /**
  * The XPCOM thread object implements this interface, which allows a consumer
  * to observe dispatch activity on the thread.
  */
-[scriptable, uuid(504e9e1f-70e1-4f33-a785-5840a4680414)]
+[scriptable, uuid(b24c5af3-43c2-4d17-be14-94d6648a305f)]
 interface nsIThreadInternal : nsIThread
 {
   /**
    * Get/set the current thread observer (may be null).  This attribute may be
    * read from any thread, but must only be set on the thread corresponding to
    * this thread object.  The observer will be released on the thread
    * corresponding to this thread object after all other events have been
    * processed during a call to Shutdown.
@@ -42,16 +42,38 @@ interface nsIThreadInternal : nsIThread
    */
   void addObserver(in nsIThreadObserver observer);
 
   /**
    * Remove an observer added via the addObserver call. Once removed the
    * observer will never be called again by the thread.
    */
   void removeObserver(in nsIThreadObserver observer);
+
+  /**
+   * This method causes any events currently enqueued on the thread to be
+   * suppressed until PopEventQueue is called, and any event dispatched to this
+   * thread's nsIEventTarget will queue as well. Calls to PushEventQueue may be
+   * nested and must each be paired with a call to PopEventQueue in order to
+   * restore the original state of the thread. The returned nsIEventTarget may
+   * be used to push events onto the nested queue. Dispatching will be disabled
+   * once the event queue is popped. The thread will only ever process pending
+   * events for the innermost event queue. Must only be called on the target
+   * thread.
+   */
+  [noscript] nsIEventTarget pushEventQueue();
+
+  /**
+   * Revert a call to PushEventQueue. When an event queue is popped, any events
+   * remaining in the queue are appended to the elder queue. This also causes
+   * the nsIEventTarget returned from PushEventQueue to stop dispatching events.
+   * Must only be called on the target thread, and with the innermost event
+   * queue.
+   */
+  [noscript] void popEventQueue(in nsIEventTarget aInnermostTarget);
 };
 
 /**
  * This interface provides the observer with hooks to implement a layered
  * event queue.  For example, it is possible to overlay processing events
  * for a GUI toolkit on top of the events for a thread:
  *
  *   var NativeQueue;
--- a/xpcom/threads/nsThread.cpp
+++ b/xpcom/threads/nsThread.cpp
@@ -260,17 +260,17 @@ nsThread::ThreadFunc(void *arg)
     // Do NS_ProcessPendingEvents but with special handling to set
     // mEventsAreDoomed atomically with the removal of the last event. The key
     // invariant here is that we will never permit PutEvent to succeed if the
     // event would be left in the queue after our final call to
     // NS_ProcessPendingEvents.
     while (true) {
       {
         MutexAutoLock lock(self->mLock);
-        if (!self->mEvents.HasPendingEvent()) {
+        if (!self->mEvents->HasPendingEvent()) {
           // No events in the queue, so we will stop now. Don't let any more
           // events be added, since they won't be processed. It is critical
           // that no PutEvent can occur between testing that the event queue is
           // empty and setting mEventsAreDoomed!
           self->mEventsAreDoomed = true;
           break;
         }
       }
@@ -294,16 +294,17 @@ nsThread::ThreadFunc(void *arg)
 //-----------------------------------------------------------------------------
 
 #ifdef MOZ_CANARY
 int sCanaryOutputFD = -1;
 #endif
 
 nsThread::nsThread(MainThreadFlag aMainThread, uint32_t aStackSize)
   : mLock("nsThread.mLock")
+  , mEvents(&mEventsRoot)
   , mPriority(PRIORITY_NORMAL)
   , mThread(nullptr)
   , mRunningEvent(0)
   , mStackSize(aStackSize)
   , mShutdownContext(nullptr)
   , mShutdownRequired(false)
   , mEventsAreDoomed(false)
   , mIsMainThread(aMainThread)
@@ -333,17 +334,17 @@ nsThread::Init()
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   // ThreadFunc will wait for this event to be run before it tries to access
   // mThread.  By delaying insertion of this event into the queue, we ensure
   // that mThread is set properly.
   {
     MutexAutoLock lock(mLock);
-    mEvents.PutEvent(startup);
+    mEventsRoot.PutEvent(startup);
   }
 
   // Wait for thread to call ThreadManager::SetupCurrentThread, which completes
   // initialization of ThreadFunc.
   startup->Wait();
   return NS_OK;
 }
 
@@ -352,75 +353,83 @@ nsThread::InitCurrentThread()
 {
   mThread = PR_GetCurrentThread();
 
   nsThreadManager::get()->RegisterCurrentThread(this);
   return NS_OK;
 }
 
 nsresult
-nsThread::PutEvent(nsIRunnable *event)
+nsThread::PutEvent(nsIRunnable *event, nsNestedEventTarget *target)
 {
   {
     MutexAutoLock lock(mLock);
-    if (mEventsAreDoomed) {
+    nsChainedEventQueue *queue = target ? target->mQueue : &mEventsRoot;
+    if (!queue || (queue == &mEventsRoot && mEventsAreDoomed)) {
       NS_WARNING("An event was posted to a thread that will never run it (rejected)");
       return NS_ERROR_UNEXPECTED;
     }
-    if (!mEvents.PutEvent(event))
+    if (!queue->PutEvent(event))
       return NS_ERROR_OUT_OF_MEMORY;
   }
 
   nsCOMPtr<nsIThreadObserver> obs = GetObserver();
   if (obs)
     obs->OnDispatchedEvent(this);
 
   return NS_OK;
 }
 
-//-----------------------------------------------------------------------------
-// nsIEventTarget
-
-NS_IMETHODIMP
-nsThread::Dispatch(nsIRunnable *event, uint32_t flags)
+nsresult
+nsThread::DispatchInternal(nsIRunnable *event, uint32_t flags,
+                           nsNestedEventTarget *target)
 {
-  LOG(("THRD(%p) Dispatch [%p %x]\n", this, event, flags));
-
   if (NS_WARN_IF(!event))
     return NS_ERROR_INVALID_ARG;
 
-  if (gXPCOMThreadsShutDown && MAIN_THREAD != mIsMainThread) {
+  if (gXPCOMThreadsShutDown && MAIN_THREAD != mIsMainThread && !target) {
     return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
   }
 
   if (flags & DISPATCH_SYNC) {
     nsThread *thread = nsThreadManager::get()->GetCurrentThread();
     if (NS_WARN_IF(!thread))
       return NS_ERROR_NOT_AVAILABLE;
 
     // XXX we should be able to do something better here... we should
     //     be able to monitor the slot occupied by this event and use
     //     that to tell us when the event has been processed.
  
     nsRefPtr<nsThreadSyncDispatch> wrapper =
         new nsThreadSyncDispatch(thread, event);
     if (!wrapper)
       return NS_ERROR_OUT_OF_MEMORY;
-    nsresult rv = PutEvent(wrapper);
+    nsresult rv = PutEvent(wrapper, target);
     // Don't wait for the event to finish if we didn't dispatch it...
     if (NS_FAILED(rv))
       return rv;
 
     while (wrapper->IsPending())
       NS_ProcessNextEvent(thread);
     return wrapper->Result();
   }
 
   NS_ASSERTION(flags == NS_DISPATCH_NORMAL, "unexpected dispatch flags");
-  return PutEvent(event);
+  return PutEvent(event, target);
+}
+
+//-----------------------------------------------------------------------------
+// nsIEventTarget
+
+NS_IMETHODIMP
+nsThread::Dispatch(nsIRunnable *event, uint32_t flags)
+{
+  LOG(("THRD(%p) Dispatch [%p %x]\n", this, event, flags));
+
+  return DispatchInternal(event, flags, nullptr);
 }
 
 NS_IMETHODIMP
 nsThread::IsOnCurrentThread(bool *result)
 {
   *result = (PR_GetCurrentThread() == mThread);
   return NS_OK;
 }
@@ -462,17 +471,17 @@ nsThread::Shutdown()
   context.shutdownAck = false;
 
   // Set mShutdownContext and wake up the thread in case it is waiting for
   // events to process.
   nsCOMPtr<nsIRunnable> event = new nsThreadShutdownEvent(this, &context);
   if (!event)
     return NS_ERROR_OUT_OF_MEMORY;
   // XXXroc What if posting the event fails due to OOM?
-  PutEvent(event);
+  PutEvent(event, nullptr);
 
   // We could still end up with other events being added after the shutdown
   // task, but that's okay because we process pending events in ThreadFunc
   // after setting mShutdownContext just before exiting.
   
   // Process events on the current thread until we receive a shutdown ACK.
   while (!context.shutdownAck)
     NS_ProcessNextEvent(context.joiningThread);
@@ -498,17 +507,17 @@ nsThread::Shutdown()
 }
 
 NS_IMETHODIMP
 nsThread::HasPendingEvents(bool *result)
 {
   if (NS_WARN_IF(PR_GetCurrentThread() != mThread))
     return NS_ERROR_NOT_SAME_THREAD;
 
-  *result = mEvents.GetEvent(false, nullptr);
+  *result = mEvents->GetEvent(false, nullptr);
   return NS_OK;
 }
 
 #ifdef MOZ_CANARY
 void canary_alarm_handler (int signum);
 
 class Canary {
 //XXX ToDo: support nested loops
@@ -609,17 +618,17 @@ nsThread::ProcessNextEvent(bool mayWait,
 
   {
     // Scope for |event| to make sure that its destructor fires while
     // mRunningEvent has been incremented, since that destructor can
     // also do work.
 
     // If we are shutting down, then do not wait for new events.
     nsCOMPtr<nsIRunnable> event;
-    mEvents.GetEvent(mayWait && !ShuttingDown(), getter_AddRefs(event));
+    mEvents->GetEvent(mayWait && !ShuttingDown(), getter_AddRefs(event));
 
     *result = (event.get() != nullptr);
 
     if (event) {
       LOG(("THRD(%p) running [%p]\n", this, event.get()));
       if (MAIN_THREAD == mIsMainThread)
         HangMonitor::NotifyActivity();
       event->Run();
@@ -749,16 +758,72 @@ nsThread::RemoveObserver(nsIThreadObserv
 
   if (observer && !mEventObservers.RemoveElement(observer)) {
     NS_WARNING("Removing an observer that was never added!");
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsThread::PushEventQueue(nsIEventTarget **result)
+{
+  if (NS_WARN_IF(PR_GetCurrentThread() != mThread))
+    return NS_ERROR_NOT_SAME_THREAD;
+
+  nsChainedEventQueue *queue = new nsChainedEventQueue();
+  queue->mEventTarget = new nsNestedEventTarget(this, queue);
+
+  {
+    MutexAutoLock lock(mLock);
+    queue->mNext = mEvents;
+    mEvents = queue;
+  }
+
+  NS_ADDREF(*result = queue->mEventTarget);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsThread::PopEventQueue(nsIEventTarget *innermostTarget)
+{
+  if (NS_WARN_IF(PR_GetCurrentThread() != mThread))
+    return NS_ERROR_NOT_SAME_THREAD;
+
+  if (NS_WARN_IF(!innermostTarget))
+    return NS_ERROR_NULL_POINTER;
+
+  // Don't delete or release anything while holding the lock.
+  nsAutoPtr<nsChainedEventQueue> queue;
+  nsRefPtr<nsNestedEventTarget> target;
+
+  {
+    MutexAutoLock lock(mLock);
+
+    // Make sure we're popping the innermost event target.
+    if (NS_WARN_IF(mEvents->mEventTarget != innermostTarget))
+      return NS_ERROR_UNEXPECTED;
+
+    MOZ_ASSERT(mEvents != &mEventsRoot);
+
+    queue = mEvents;
+    mEvents = mEvents->mNext;
+
+    nsCOMPtr<nsIRunnable> event;
+    while (queue->GetEvent(false, getter_AddRefs(event)))
+      mEvents->PutEvent(event);
+
+    // Don't let the event target post any more events.
+    queue->mEventTarget.swap(target);
+    target->mQueue = nullptr;
+  }
+
+  return NS_OK;
+}
+
 nsresult
 nsThread::SetMainThreadObserver(nsIThreadObserver* aObserver)
 {
   if (aObserver && nsThread::sMainThreadObserver) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   if (!NS_IsMainThread()) {
@@ -777,8 +842,27 @@ nsThreadSyncDispatch::Run()
   if (mSyncTask) {
     mResult = mSyncTask->Run();
     mSyncTask = nullptr;
     // unblock the origin thread
     mOrigin->Dispatch(this, NS_DISPATCH_NORMAL);
   }
   return NS_OK;
 }
+
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS1(nsThread::nsNestedEventTarget, nsIEventTarget)
+
+NS_IMETHODIMP
+nsThread::nsNestedEventTarget::Dispatch(nsIRunnable *event, uint32_t flags)
+{
+  LOG(("THRD(%p) Dispatch [%p %x] to nested loop %p\n", mThread.get(), event,
+       flags, this));
+
+  return mThread->DispatchInternal(event, flags, this);
+}
+
+NS_IMETHODIMP
+nsThread::nsNestedEventTarget::IsOnCurrentThread(bool *result)
+{
+  return mThread->IsOnCurrentThread(result);
+}
--- a/xpcom/threads/nsThread.h
+++ b/xpcom/threads/nsThread.h
@@ -10,16 +10,17 @@
 #include "mozilla/Mutex.h"
 #include "nsIThreadInternal.h"
 #include "nsISupportsPriority.h"
 #include "nsEventQueue.h"
 #include "nsThreadUtils.h"
 #include "nsString.h"
 #include "nsTObserverArray.h"
 #include "mozilla/Attributes.h"
+#include "nsAutoPtr.h"
 
 // A native thread
 class nsThread MOZ_FINAL : public nsIThreadInternal,
                            public nsISupportsPriority
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIEVENTTARGET
@@ -51,16 +52,21 @@ public:
   void ClearObservers() { mEventObservers.Clear(); }
 
   static nsresult
   SetMainThreadObserver(nsIThreadObserver* aObserver);
 
 private:
   static nsIThreadObserver* sMainThreadObserver;
 
+  class nsChainedEventQueue;
+
+  class nsNestedEventTarget;
+  friend class nsNestedEventTarget;
+
   friend class nsThreadShutdownEvent;
 
   ~nsThread();
 
   bool ShuttingDown() { return mShutdownContext != nullptr; }
 
   static void ThreadFunc(void *arg);
 
@@ -68,33 +74,81 @@ private:
   already_AddRefed<nsIThreadObserver> GetObserver() {
     nsIThreadObserver *obs;
     nsThread::GetObserver(&obs);
     return already_AddRefed<nsIThreadObserver>(obs);
   }
 
   // Wrappers for event queue methods:
   bool GetEvent(bool mayWait, nsIRunnable **event) {
-    return mEvents.GetEvent(mayWait, event);
+    return mEvents->GetEvent(mayWait, event);
   }
-  nsresult PutEvent(nsIRunnable *event);
+  nsresult PutEvent(nsIRunnable *event, nsNestedEventTarget *target);
+
+  nsresult DispatchInternal(nsIRunnable *event, uint32_t flags,
+                            nsNestedEventTarget *target);
+
+  // Wrapper for nsEventQueue that supports chaining.
+  class nsChainedEventQueue {
+  public:
+    nsChainedEventQueue()
+      : mNext(nullptr) {
+    }
+
+    bool GetEvent(bool mayWait, nsIRunnable **event) {
+      return mQueue.GetEvent(mayWait, event);
+    }
+
+    bool PutEvent(nsIRunnable *event) {
+      return mQueue.PutEvent(event);
+    }
+
+    bool HasPendingEvent() {
+      return mQueue.HasPendingEvent();
+    }
+
+    nsChainedEventQueue *mNext;
+    nsRefPtr<nsNestedEventTarget> mEventTarget;
+
+  private:
+    nsEventQueue mQueue;
+  };
+
+  class nsNestedEventTarget MOZ_FINAL : public nsIEventTarget {
+  public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_DECL_NSIEVENTTARGET
+
+    nsNestedEventTarget(nsThread *thread, nsChainedEventQueue *queue)
+      : mThread(thread), mQueue(queue) {
+    }
+
+    nsRefPtr<nsThread> mThread;
+
+    // This is protected by mThread->mLock.
+    nsChainedEventQueue* mQueue;
+
+  private:
+    ~nsNestedEventTarget() {}
+  };
 
   // This lock protects access to mObserver, mEvents and mEventsAreDoomed.
   // All of those fields are only modified on the thread itself (never from
   // another thread).  This means that we can avoid holding the lock while
   // using mObserver and mEvents on the thread itself.  When calling PutEvent
   // on mEvents, we have to hold the lock to synchronize with PopEventQueue.
   mozilla::Mutex mLock;
 
   nsCOMPtr<nsIThreadObserver> mObserver;
 
   // Only accessed on the target thread.
   nsAutoTObserverArray<nsCOMPtr<nsIThreadObserver>, 2> mEventObservers;
 
-  nsEventQueue  mEvents;
+  nsChainedEventQueue *mEvents;   // never null
+  nsChainedEventQueue  mEventsRoot;
 
   int32_t   mPriority;
   PRThread *mThread;
   uint32_t  mRunningEvent;  // counter
   uint32_t  mStackSize;
 
   struct nsThreadShutdownContext *mShutdownContext;