forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			439 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			439 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 | 
						|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
 | 
						|
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
#include "ThrottledEventQueue.h"
 | 
						|
 | 
						|
#include "mozilla/Atomics.h"
 | 
						|
#include "mozilla/ClearOnShutdown.h"
 | 
						|
#include "mozilla/CondVar.h"
 | 
						|
#include "mozilla/EventQueue.h"
 | 
						|
#include "mozilla/Mutex.h"
 | 
						|
#include "mozilla/Unused.h"
 | 
						|
#include "nsThreadUtils.h"
 | 
						|
 | 
						|
namespace mozilla {
 | 
						|
 | 
						|
namespace {}  // anonymous namespace
 | 
						|
 | 
						|
// The ThrottledEventQueue is designed with inner and outer objects:
 | 
						|
//
 | 
						|
//       XPCOM code     base event target
 | 
						|
//            |               |
 | 
						|
//            v               v
 | 
						|
//        +-------+       +--------+
 | 
						|
//        | Outer |   +-->|executor|
 | 
						|
//        +-------+   |   +--------+
 | 
						|
//            |       |       |
 | 
						|
//            |   +-------+   |
 | 
						|
//            +-->| Inner |<--+
 | 
						|
//                +-------+
 | 
						|
//
 | 
						|
// Client code references the outer nsIEventTarget which in turn references
 | 
						|
// an inner object, which actually holds the queue of runnables.
 | 
						|
//
 | 
						|
// Whenever the queue is non-empty (and not paused), it keeps an "executor"
 | 
						|
// runnable dispatched to the base event target. Each time the executor is run,
 | 
						|
// it draws the next event from Inner's queue and runs it. If that queue has
 | 
						|
// more events, the executor is dispatched to the base again.
 | 
						|
//
 | 
						|
// The executor holds a strong reference to the Inner object. This means that if
 | 
						|
// the outer object is dereferenced and destroyed, the Inner object will remain
 | 
						|
// live for as long as the executor exists - that is, until the Inner's queue is
 | 
						|
// empty.
 | 
						|
//
 | 
						|
// A Paused ThrottledEventQueue does not enqueue an executor when new events are
 | 
						|
// added. Any executor previously queued on the base event target draws no
 | 
						|
// events from a Paused ThrottledEventQueue, and returns without re-enqueueing
 | 
						|
// itself. Since there is no executor keeping the Inner object alive until its
 | 
						|
// queue is empty, dropping a Paused ThrottledEventQueue may drop the Inner
 | 
						|
// while it still owns events. This is the correct behavior: if there are no
 | 
						|
// references to it, it will never be Resumed, and thus it will never dispatch
 | 
						|
// events again.
 | 
						|
//
 | 
						|
// Resuming a ThrottledEventQueue must dispatch an executor, so calls to Resume
 | 
						|
// are fallible for the same reasons as calls to Dispatch.
 | 
						|
//
 | 
						|
// The xpcom shutdown process drains the main thread's event queue several
 | 
						|
// times, so if a ThrottledEventQueue is being driven by the main thread, it
 | 
						|
// should get emptied out by the time we reach the "eventq shutdown" phase.
 | 
						|
class ThrottledEventQueue::Inner final : public nsISupports {
 | 
						|
  // The runnable which is dispatched to the underlying base target.  Since
 | 
						|
  // we only execute one event at a time we just re-use a single instance
 | 
						|
  // of this class while there are events left in the queue.
 | 
						|
  class Executor final : public Runnable, public nsIRunnablePriority {
 | 
						|
    // The Inner whose runnables we execute. mInner->mExecutor points
 | 
						|
    // to this executor, forming a reference loop.
 | 
						|
    RefPtr<Inner> mInner;
 | 
						|
 | 
						|
    ~Executor() = default;
 | 
						|
 | 
						|
   public:
 | 
						|
    explicit Executor(Inner* aInner)
 | 
						|
        : Runnable("ThrottledEventQueue::Inner::Executor"), mInner(aInner) {}
 | 
						|
 | 
						|
    NS_DECL_ISUPPORTS_INHERITED
 | 
						|
 | 
						|
    NS_IMETHODIMP
 | 
						|
    Run() override {
 | 
						|
      mInner->ExecuteRunnable();
 | 
						|
      return NS_OK;
 | 
						|
    }
 | 
						|
 | 
						|
    NS_IMETHODIMP
 | 
						|
    GetPriority(uint32_t* aPriority) override {
 | 
						|
      *aPriority = mInner->mPriority;
 | 
						|
      return NS_OK;
 | 
						|
    }
 | 
						|
 | 
						|
#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
 | 
						|
    NS_IMETHODIMP
 | 
						|
    GetName(nsACString& aName) override { return mInner->CurrentName(aName); }
 | 
						|
#endif
 | 
						|
  };
 | 
						|
 | 
						|
  mutable Mutex mMutex;
 | 
						|
  mutable CondVar mIdleCondVar;
 | 
						|
 | 
						|
  // As-of-yet unexecuted runnables queued on this ThrottledEventQueue.
 | 
						|
  //
 | 
						|
  // Used from any thread; protected by mMutex. Signals mIdleCondVar when
 | 
						|
  // emptied.
 | 
						|
  EventQueueSized<64> mEventQueue;
 | 
						|
 | 
						|
  // The event target we dispatch our events (actually, just our Executor) to.
 | 
						|
  //
 | 
						|
  // Written only during construction. Readable by any thread without locking.
 | 
						|
  nsCOMPtr<nsISerialEventTarget> mBaseTarget;
 | 
						|
 | 
						|
  // The Executor that we dispatch to mBaseTarget to draw runnables from our
 | 
						|
  // queue. mExecutor->mInner points to this Inner, forming a reference loop.
 | 
						|
  //
 | 
						|
  // Used from any thread; protected by mMutex.
 | 
						|
  nsCOMPtr<nsIRunnable> mExecutor;
 | 
						|
 | 
						|
  const char* mName;
 | 
						|
 | 
						|
  const uint32_t mPriority;
 | 
						|
 | 
						|
  // True if this queue is currently paused.
 | 
						|
  // Used from any thread; protected by mMutex.
 | 
						|
  bool mIsPaused;
 | 
						|
 | 
						|
  explicit Inner(nsISerialEventTarget* aBaseTarget, const char* aName,
 | 
						|
                 uint32_t aPriority)
 | 
						|
      : mMutex("ThrottledEventQueue"),
 | 
						|
        mIdleCondVar(mMutex, "ThrottledEventQueue:Idle"),
 | 
						|
        mBaseTarget(aBaseTarget),
 | 
						|
        mName(aName),
 | 
						|
        mPriority(aPriority),
 | 
						|
        mIsPaused(false) {
 | 
						|
    MOZ_ASSERT(mName, "Must pass a valid name!");
 | 
						|
  }
 | 
						|
 | 
						|
  ~Inner() {
 | 
						|
#ifdef DEBUG
 | 
						|
    MutexAutoLock lock(mMutex);
 | 
						|
 | 
						|
    // As long as an executor exists, it had better keep us alive, since it's
 | 
						|
    // going to call ExecuteRunnable on us.
 | 
						|
    MOZ_ASSERT(!mExecutor);
 | 
						|
 | 
						|
    // If we have any events in our queue, there should be an executor queued
 | 
						|
    // for them, and that should have kept us alive. The exception is that, if
 | 
						|
    // we're paused, we don't enqueue an executor.
 | 
						|
    MOZ_ASSERT(mEventQueue.IsEmpty(lock) || IsPaused(lock));
 | 
						|
 | 
						|
    // Some runnables are only safe to drop on the main thread, so if our queue
 | 
						|
    // isn't empty, we'd better be on the main thread.
 | 
						|
    MOZ_ASSERT_IF(!mEventQueue.IsEmpty(lock), NS_IsMainThread());
 | 
						|
#endif
 | 
						|
  }
 | 
						|
 | 
						|
  // Make sure an executor has been queued on our base target. If we already
 | 
						|
  // have one, do nothing; otherwise, create and dispatch it.
 | 
						|
  nsresult EnsureExecutor(MutexAutoLock& lock) {
 | 
						|
    if (mExecutor) return NS_OK;
 | 
						|
 | 
						|
    // Note, this creates a ref cycle keeping the inner alive
 | 
						|
    // until the queue is drained.
 | 
						|
    mExecutor = new Executor(this);
 | 
						|
    nsresult rv = mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL);
 | 
						|
    if (NS_WARN_IF(NS_FAILED(rv))) {
 | 
						|
      mExecutor = nullptr;
 | 
						|
      return rv;
 | 
						|
    }
 | 
						|
 | 
						|
    return NS_OK;
 | 
						|
  }
 | 
						|
 | 
						|
  nsresult CurrentName(nsACString& aName) {
 | 
						|
    nsCOMPtr<nsIRunnable> event;
 | 
						|
 | 
						|
#ifdef DEBUG
 | 
						|
    bool currentThread = false;
 | 
						|
    mBaseTarget->IsOnCurrentThread(¤tThread);
 | 
						|
    MOZ_ASSERT(currentThread);
 | 
						|
#endif
 | 
						|
 | 
						|
    {
 | 
						|
      MutexAutoLock lock(mMutex);
 | 
						|
      event = mEventQueue.PeekEvent(lock);
 | 
						|
      // It is possible that mEventQueue wasn't empty when the executor
 | 
						|
      // was added to the queue, but someone processed events from mEventQueue
 | 
						|
      // before the executor, this is why mEventQueue is empty here
 | 
						|
      if (!event) {
 | 
						|
        aName.AssignLiteral("no runnables left in the ThrottledEventQueue");
 | 
						|
        return NS_OK;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (nsCOMPtr<nsINamed> named = do_QueryInterface(event)) {
 | 
						|
      nsresult rv = named->GetName(aName);
 | 
						|
      return rv;
 | 
						|
    }
 | 
						|
 | 
						|
    aName.AssignASCII(mName);
 | 
						|
    return NS_OK;
 | 
						|
  }
 | 
						|
 | 
						|
  void ExecuteRunnable() {
 | 
						|
    // Any thread
 | 
						|
    nsCOMPtr<nsIRunnable> event;
 | 
						|
 | 
						|
#ifdef DEBUG
 | 
						|
    bool currentThread = false;
 | 
						|
    mBaseTarget->IsOnCurrentThread(¤tThread);
 | 
						|
    MOZ_ASSERT(currentThread);
 | 
						|
#endif
 | 
						|
 | 
						|
    {
 | 
						|
      MutexAutoLock lock(mMutex);
 | 
						|
 | 
						|
      // Normally, a paused queue doesn't dispatch any executor, but we might
 | 
						|
      // have been paused after the executor was already in flight. There's no
 | 
						|
      // way to yank the executor out of the base event target, so we just check
 | 
						|
      // for a paused queue here and return without running anything. We'll
 | 
						|
      // create a new executor when we're resumed.
 | 
						|
      if (IsPaused(lock)) {
 | 
						|
        // Note, this breaks a ref cycle.
 | 
						|
        mExecutor = nullptr;
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // We only dispatch an executor runnable when we know there is something
 | 
						|
      // in the queue, so this should never fail.
 | 
						|
      event = mEventQueue.GetEvent(lock);
 | 
						|
      MOZ_ASSERT(event);
 | 
						|
 | 
						|
      // If there are more events in the queue, then dispatch the next
 | 
						|
      // executor.  We do this now, before running the event, because
 | 
						|
      // the event might spin the event loop and we don't want to stall
 | 
						|
      // the queue.
 | 
						|
      if (mEventQueue.HasReadyEvent(lock)) {
 | 
						|
        // Dispatch the next base target runnable to attempt to execute
 | 
						|
        // the next throttled event.  We must do this before executing
 | 
						|
        // the event in case the event spins the event loop.
 | 
						|
        MOZ_ALWAYS_SUCCEEDS(
 | 
						|
            mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL));
 | 
						|
      }
 | 
						|
 | 
						|
      // Otherwise the queue is empty and we can stop dispatching the
 | 
						|
      // executor.
 | 
						|
      else {
 | 
						|
        // Break the Executor::mInner / Inner::mExecutor reference loop.
 | 
						|
        mExecutor = nullptr;
 | 
						|
        mIdleCondVar.NotifyAll();
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Execute the event now that we have unlocked.
 | 
						|
    LogRunnable::Run log(event);
 | 
						|
    Unused << event->Run();
 | 
						|
 | 
						|
    // To cover the event's destructor code in the LogRunnable log
 | 
						|
    event = nullptr;
 | 
						|
  }
 | 
						|
 | 
						|
 public:
 | 
						|
  static already_AddRefed<Inner> Create(nsISerialEventTarget* aBaseTarget,
 | 
						|
                                        const char* aName, uint32_t aPriority) {
 | 
						|
    MOZ_ASSERT(NS_IsMainThread());
 | 
						|
    // FIXME: This assertion only worked when `sCurrentShutdownPhase` was not
 | 
						|
    // being updated.
 | 
						|
    // MOZ_ASSERT(ClearOnShutdown_Internal::sCurrentShutdownPhase ==
 | 
						|
    //            ShutdownPhase::NotInShutdown);
 | 
						|
 | 
						|
    RefPtr<Inner> ref = new Inner(aBaseTarget, aName, aPriority);
 | 
						|
    return ref.forget();
 | 
						|
  }
 | 
						|
 | 
						|
  bool IsEmpty() const {
 | 
						|
    // Any thread
 | 
						|
    return Length() == 0;
 | 
						|
  }
 | 
						|
 | 
						|
  uint32_t Length() const {
 | 
						|
    // Any thread
 | 
						|
    MutexAutoLock lock(mMutex);
 | 
						|
    return mEventQueue.Count(lock);
 | 
						|
  }
 | 
						|
 | 
						|
  already_AddRefed<nsIRunnable> GetEvent() {
 | 
						|
    MutexAutoLock lock(mMutex);
 | 
						|
    return mEventQueue.GetEvent(lock);
 | 
						|
  }
 | 
						|
 | 
						|
  void AwaitIdle() const {
 | 
						|
    // Any thread, except the main thread or our base target.  Blocking the
 | 
						|
    // main thread is forbidden.  Blocking the base target is guaranteed to
 | 
						|
    // produce a deadlock.
 | 
						|
    MOZ_ASSERT(!NS_IsMainThread());
 | 
						|
#ifdef DEBUG
 | 
						|
    bool onBaseTarget = false;
 | 
						|
    Unused << mBaseTarget->IsOnCurrentThread(&onBaseTarget);
 | 
						|
    MOZ_ASSERT(!onBaseTarget);
 | 
						|
#endif
 | 
						|
 | 
						|
    MutexAutoLock lock(mMutex);
 | 
						|
    while (mExecutor || IsPaused(lock)) {
 | 
						|
      mIdleCondVar.Wait();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  bool IsPaused() const {
 | 
						|
    MutexAutoLock lock(mMutex);
 | 
						|
    return IsPaused(lock);
 | 
						|
  }
 | 
						|
 | 
						|
  bool IsPaused(const MutexAutoLock& aProofOfLock) const { return mIsPaused; }
 | 
						|
 | 
						|
  nsresult SetIsPaused(bool aIsPaused) {
 | 
						|
    MutexAutoLock lock(mMutex);
 | 
						|
 | 
						|
    // If we will be unpaused, and we have events in our queue, make sure we
 | 
						|
    // have an executor queued on the base event target to run them. Do this
 | 
						|
    // before we actually change mIsPaused, since this is fallible.
 | 
						|
    if (!aIsPaused && !mEventQueue.IsEmpty(lock)) {
 | 
						|
      nsresult rv = EnsureExecutor(lock);
 | 
						|
      if (NS_FAILED(rv)) {
 | 
						|
        return rv;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    mIsPaused = aIsPaused;
 | 
						|
    return NS_OK;
 | 
						|
  }
 | 
						|
 | 
						|
  nsresult DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) {
 | 
						|
    // Any thread
 | 
						|
    nsCOMPtr<nsIRunnable> r = aEvent;
 | 
						|
    return Dispatch(r.forget(), aFlags);
 | 
						|
  }
 | 
						|
 | 
						|
  nsresult Dispatch(already_AddRefed<nsIRunnable> aEvent, uint32_t aFlags) {
 | 
						|
    MOZ_ASSERT(aFlags == NS_DISPATCH_NORMAL || aFlags == NS_DISPATCH_AT_END);
 | 
						|
 | 
						|
    // Any thread
 | 
						|
    MutexAutoLock lock(mMutex);
 | 
						|
 | 
						|
    if (!IsPaused(lock)) {
 | 
						|
      // Make sure we have an executor in flight to process events. This is
 | 
						|
      // fallible, so do it first. Our lock will prevent the executor from
 | 
						|
      // accessing the event queue before we add the event below.
 | 
						|
      nsresult rv = EnsureExecutor(lock);
 | 
						|
      if (NS_FAILED(rv)) return rv;
 | 
						|
    }
 | 
						|
 | 
						|
    // Only add the event to the underlying queue if are able to
 | 
						|
    // dispatch to our base target.
 | 
						|
    nsCOMPtr<nsIRunnable> event(aEvent);
 | 
						|
    LogRunnable::LogDispatch(event);
 | 
						|
    mEventQueue.PutEvent(event.forget(), EventQueuePriority::Normal, lock);
 | 
						|
    return NS_OK;
 | 
						|
  }
 | 
						|
 | 
						|
  nsresult DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
 | 
						|
                           uint32_t aDelay) {
 | 
						|
    // The base target may implement this, but we don't.  Always fail
 | 
						|
    // to provide consistent behavior.
 | 
						|
    return NS_ERROR_NOT_IMPLEMENTED;
 | 
						|
  }
 | 
						|
 | 
						|
  bool IsOnCurrentThread() { return mBaseTarget->IsOnCurrentThread(); }
 | 
						|
 | 
						|
  NS_DECL_THREADSAFE_ISUPPORTS
 | 
						|
};
 | 
						|
 | 
						|
NS_IMPL_ISUPPORTS(ThrottledEventQueue::Inner, nsISupports);
 | 
						|
 | 
						|
NS_IMPL_ISUPPORTS_INHERITED(ThrottledEventQueue::Inner::Executor, Runnable,
 | 
						|
                            nsIRunnablePriority)
 | 
						|
 | 
						|
NS_IMPL_ISUPPORTS(ThrottledEventQueue, ThrottledEventQueue, nsIEventTarget,
 | 
						|
                  nsISerialEventTarget);
 | 
						|
 | 
						|
ThrottledEventQueue::ThrottledEventQueue(already_AddRefed<Inner> aInner)
 | 
						|
    : mInner(aInner) {
 | 
						|
  MOZ_ASSERT(mInner);
 | 
						|
}
 | 
						|
 | 
						|
already_AddRefed<ThrottledEventQueue> ThrottledEventQueue::Create(
 | 
						|
    nsISerialEventTarget* aBaseTarget, const char* aName, uint32_t aPriority) {
 | 
						|
  MOZ_ASSERT(NS_IsMainThread());
 | 
						|
  MOZ_ASSERT(aBaseTarget);
 | 
						|
 | 
						|
  RefPtr<Inner> inner = Inner::Create(aBaseTarget, aName, aPriority);
 | 
						|
 | 
						|
  RefPtr<ThrottledEventQueue> ref = new ThrottledEventQueue(inner.forget());
 | 
						|
  return ref.forget();
 | 
						|
}
 | 
						|
 | 
						|
bool ThrottledEventQueue::IsEmpty() const { return mInner->IsEmpty(); }
 | 
						|
 | 
						|
uint32_t ThrottledEventQueue::Length() const { return mInner->Length(); }
 | 
						|
 | 
						|
// Get the next runnable from the queue
 | 
						|
already_AddRefed<nsIRunnable> ThrottledEventQueue::GetEvent() {
 | 
						|
  return mInner->GetEvent();
 | 
						|
}
 | 
						|
 | 
						|
void ThrottledEventQueue::AwaitIdle() const { return mInner->AwaitIdle(); }
 | 
						|
 | 
						|
nsresult ThrottledEventQueue::SetIsPaused(bool aIsPaused) {
 | 
						|
  return mInner->SetIsPaused(aIsPaused);
 | 
						|
}
 | 
						|
 | 
						|
bool ThrottledEventQueue::IsPaused() const { return mInner->IsPaused(); }
 | 
						|
 | 
						|
NS_IMETHODIMP
 | 
						|
ThrottledEventQueue::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) {
 | 
						|
  return mInner->DispatchFromScript(aEvent, aFlags);
 | 
						|
}
 | 
						|
 | 
						|
NS_IMETHODIMP
 | 
						|
ThrottledEventQueue::Dispatch(already_AddRefed<nsIRunnable> aEvent,
 | 
						|
                              uint32_t aFlags) {
 | 
						|
  return mInner->Dispatch(std::move(aEvent), aFlags);
 | 
						|
}
 | 
						|
 | 
						|
NS_IMETHODIMP
 | 
						|
ThrottledEventQueue::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
 | 
						|
                                     uint32_t aFlags) {
 | 
						|
  return mInner->DelayedDispatch(std::move(aEvent), aFlags);
 | 
						|
}
 | 
						|
 | 
						|
NS_IMETHODIMP
 | 
						|
ThrottledEventQueue::IsOnCurrentThread(bool* aResult) {
 | 
						|
  *aResult = mInner->IsOnCurrentThread();
 | 
						|
  return NS_OK;
 | 
						|
}
 | 
						|
 | 
						|
NS_IMETHODIMP_(bool)
 | 
						|
ThrottledEventQueue::IsOnCurrentThreadInfallible() {
 | 
						|
  return mInner->IsOnCurrentThread();
 | 
						|
}
 | 
						|
 | 
						|
}  // namespace mozilla
 |