1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "ThrottledEventQueue.h"
8 
9 #include "mozilla/Atomics.h"
10 #include "mozilla/ClearOnShutdown.h"
11 #include "mozilla/Mutex.h"
12 #include "mozilla/Unused.h"
13 #include "nsEventQueue.h"
14 
15 namespace mozilla {
16 
17 using mozilla::services::GetObserverService;
18 
19 namespace {
20 
21 static const char kShutdownTopic[] = "xpcom-shutdown";
22 
23 } // anonymous namespace
24 
25 // The ThrottledEventQueue is designed with inner and outer objects:
26 //
27 //       XPCOM code    nsObserverService
28 //            |               |
29 //            |               |
30 //            v               |
31 //        +-------+           |
32 //        | Outer |           |
33 //        +-------+           |
34 //            |               |
35 //            |   +-------+   |
36 //            +-->| Inner |<--+
37 //                +-------+
38 //
39 // Client code references the outer nsIEventTarget which in turn references
40 // an inner object.  The inner object is also held alive by the observer
41 // service.
42 //
43 // If the outer object is dereferenced and destroyed, it will trigger a
44 // shutdown operation on the inner object.  Similarly if the observer
45 // service notifies that the browser is shutting down, then the inner
46 // object also starts shutting down.
47 //
48 // Once the queue has drained we unregister from the observer service.  If
49 // the outer object is already gone, then the inner object is free'd at this
50 // point.  If the outer object still exists then calls fall back to the
51 // ThrottledEventQueue's base target.  We just don't queue things
52 // any more.  The inner is then released once the outer object is released.
53 //
54 // Note, we must keep the inner object alive and attached to the observer
55 // service until the TaskQueue is fully shutdown and idle.  We must delay
56 // xpcom shutdown if the TaskQueue is in the middle of draining.
57 class ThrottledEventQueue::Inner final : public nsIObserver
58 {
59   // The runnable which is dispatched to the underlying base target.  Since
60   // we only execute one event at a time we just re-use a single instance
61   // of this class while there are events left in the queue.
62   class Executor final : public Runnable
63   {
64     RefPtr<Inner> mInner;
65 
66   public:
Executor(Inner * aInner)67     explicit Executor(Inner* aInner)
68       : mInner(aInner)
69     { }
70 
71     NS_IMETHODIMP
Run()72     Run()
73     {
74       mInner->ExecuteRunnable();
75       return NS_OK;
76     }
77   };
78 
79   mutable Mutex mMutex;
80   mutable CondVar mIdleCondVar;
81 
82   mozilla::CondVar mEventsAvailable;
83 
84   // any thread, protected by mutex
85   nsEventQueue mEventQueue;
86 
87   // written on main thread, read on any thread
88   nsCOMPtr<nsIEventTarget> mBaseTarget;
89 
90   // any thread, protected by mutex
91   nsCOMPtr<nsIRunnable> mExecutor;
92 
93   // any thread, atomic
94   Atomic<uint32_t> mExecutionDepth;
95 
96   // any thread, protected by mutex
97   bool mShutdownStarted;
98 
Inner(nsIEventTarget * aBaseTarget)99   explicit Inner(nsIEventTarget* aBaseTarget)
100     : mMutex("ThrottledEventQueue")
101     , mIdleCondVar(mMutex, "ThrottledEventQueue:Idle")
102     , mEventsAvailable(mMutex, "[ThrottledEventQueue::Inner.mEventsAvailable]")
103     , mEventQueue(mEventsAvailable, nsEventQueue::eNormalQueue)
104     , mBaseTarget(aBaseTarget)
105     , mExecutionDepth(0)
106     , mShutdownStarted(false)
107   {
108   }
109 
~Inner()110   ~Inner()
111   {
112     MOZ_ASSERT(!mExecutor);
113     MOZ_ASSERT(mShutdownStarted);
114   }
115 
116   void
ExecuteRunnable()117   ExecuteRunnable()
118   {
119     // Any thread
120     nsCOMPtr<nsIRunnable> event;
121     bool shouldShutdown = false;
122 
123 #ifdef DEBUG
124     bool currentThread = false;
125     mBaseTarget->IsOnCurrentThread(&currentThread);
126     MOZ_ASSERT(currentThread);
127 #endif
128 
129     {
130       MutexAutoLock lock(mMutex);
131 
132       // We only dispatch an executor runnable when we know there is something
133       // in the queue, so this should never fail.
134       MOZ_ALWAYS_TRUE(mEventQueue.GetPendingEvent(getter_AddRefs(event), lock));
135 
136       // If there are more events in the queue, then dispatch the next
137       // executor.  We do this now, before running the event, because
138       // the event might spin the event loop and we don't want to stall
139       // the queue.
140       if (mEventQueue.HasPendingEvent(lock)) {
141         // Dispatch the next base target runnable to attempt to execute
142         // the next throttled event.  We must do this before executing
143         // the event in case the event spins the event loop.
144         MOZ_ALWAYS_SUCCEEDS(
145           mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL));
146       }
147 
148       // Otherwise the queue is empty and we can stop dispatching the
149       // executor.  We might also need to shutdown after running the
150       // last event.
151       else {
152         shouldShutdown = mShutdownStarted;
153         // Note, this breaks a ref cycle.
154         mExecutor = nullptr;
155         mIdleCondVar.NotifyAll();
156       }
157     }
158 
159     // Execute the event now that we have unlocked.
160     ++mExecutionDepth;
161     Unused << event->Run();
162     --mExecutionDepth;
163 
164     // If shutdown was started and the queue is now empty we can now
165     // finalize the shutdown.  This is performed separately at the end
166     // of the method in order to wait for the event to finish running.
167     if (shouldShutdown) {
168       MOZ_ASSERT(IsEmpty());
169       NS_DispatchToMainThread(NewRunnableMethod(this, &Inner::ShutdownComplete));
170     }
171   }
172 
173   void
ShutdownComplete()174   ShutdownComplete()
175   {
176     MOZ_ASSERT(NS_IsMainThread());
177     MOZ_ASSERT(IsEmpty());
178     nsCOMPtr<nsIObserverService> obs = GetObserverService();
179     obs->RemoveObserver(this, kShutdownTopic);
180   }
181 
182 public:
183   static already_AddRefed<Inner>
Create(nsIEventTarget * aBaseTarget)184   Create(nsIEventTarget* aBaseTarget)
185   {
186     MOZ_ASSERT(NS_IsMainThread());
187 
188     if (ClearOnShutdown_Internal::sCurrentShutdownPhase != ShutdownPhase::NotInShutdown) {
189       return nullptr;
190     }
191 
192     nsCOMPtr<nsIObserverService> obs = GetObserverService();
193     if (NS_WARN_IF(!obs)) {
194       return nullptr;
195     }
196 
197     RefPtr<Inner> ref = new Inner(aBaseTarget);
198 
199     nsresult rv = obs->AddObserver(ref, kShutdownTopic,
200                                    false /* means OS will hold a strong ref */);
201     if (NS_WARN_IF(NS_FAILED(rv))) {
202       ref->MaybeStartShutdown();
203       MOZ_ASSERT(ref->IsEmpty());
204       return nullptr;
205     }
206 
207     return ref.forget();
208   }
209 
210   NS_IMETHOD
Observe(nsISupports *,const char * aTopic,const char16_t *)211   Observe(nsISupports*, const char* aTopic, const char16_t*) override
212   {
213     MOZ_ASSERT(NS_IsMainThread());
214     MOZ_ASSERT(!strcmp(aTopic, kShutdownTopic));
215 
216     MaybeStartShutdown();
217 
218     // Once shutdown begins we set the Atomic<bool> mShutdownStarted flag.
219     // This prevents any new runnables from being dispatched into the
220     // TaskQueue.  Therefore this loop should be finite.
221     while (!IsEmpty()) {
222       MOZ_ALWAYS_TRUE(NS_ProcessNextEvent());
223     }
224 
225     return NS_OK;
226   }
227 
228   void
MaybeStartShutdown()229   MaybeStartShutdown()
230   {
231     // Any thread
232     MutexAutoLock lock(mMutex);
233 
234     if (mShutdownStarted) {
235       return;
236     }
237     mShutdownStarted = true;
238 
239     // We are marked for shutdown now, but we are still processing runnables.
240     // Return for now.  The shutdown will be completed once the queue is
241     // drained.
242     if (mExecutor) {
243       return;
244     }
245 
246     // The queue is empty, so we can complete immediately.
247     NS_DispatchToMainThread(NewRunnableMethod(this, &Inner::ShutdownComplete));
248   }
249 
250   bool
IsEmpty() const251   IsEmpty() const
252   {
253     // Any thread
254     return Length() == 0;
255   }
256 
257   uint32_t
Length() const258   Length() const
259   {
260     // Any thread
261     MutexAutoLock lock(mMutex);
262     return mEventQueue.Count(lock);
263   }
264 
265   void
AwaitIdle() const266   AwaitIdle() const
267   {
268     // Any thread, except the main thread or our base target.  Blocking the
269     // main thread is forbidden.  Blocking the base target is guaranteed to
270     // produce a deadlock.
271     MOZ_ASSERT(!NS_IsMainThread());
272 #ifdef DEBUG
273     bool onBaseTarget = false;
274     Unused << mBaseTarget->IsOnCurrentThread(&onBaseTarget);
275     MOZ_ASSERT(!onBaseTarget);
276 #endif
277 
278     MutexAutoLock lock(mMutex);
279     while (mExecutor) {
280       mIdleCondVar.Wait();
281     }
282   }
283 
284   nsresult
DispatchFromScript(nsIRunnable * aEvent,uint32_t aFlags)285   DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags)
286   {
287     // Any thread
288     nsCOMPtr<nsIRunnable> r = aEvent;
289     return Dispatch(r.forget(), aFlags);
290   }
291 
292   nsresult
Dispatch(already_AddRefed<nsIRunnable> aEvent,uint32_t aFlags)293   Dispatch(already_AddRefed<nsIRunnable> aEvent, uint32_t aFlags)
294   {
295     MOZ_ASSERT(aFlags == NS_DISPATCH_NORMAL ||
296                aFlags == NS_DISPATCH_AT_END);
297 
298     // Any thread
299     MutexAutoLock lock(mMutex);
300 
301     // If we are shutting down, just fall back to our base target
302     // directly.
303     if (mShutdownStarted) {
304       return mBaseTarget->Dispatch(Move(aEvent), aFlags);
305     }
306 
307     // We are not currently processing events, so we must start
308     // operating on our base target.  This is fallible, so do
309     // it first.  Our lock will prevent the executor from accessing
310     // the event queue before we add the event below.
311     if (!mExecutor) {
312       // Note, this creates a ref cycle keeping the inner alive
313       // until the queue is drained.
314       mExecutor = new Executor(this);
315       nsresult rv = mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL);
316       if (NS_WARN_IF(NS_FAILED(rv))) {
317         mExecutor = nullptr;
318         return rv;
319       }
320     }
321 
322     // Only add the event to the underlying queue if are able to
323     // dispatch to our base target.
324     mEventQueue.PutEvent(Move(aEvent), lock);
325     return NS_OK;
326   }
327 
328   nsresult
DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,uint32_t aDelay)329   DelayedDispatch(already_AddRefed<nsIRunnable> aEvent, uint32_t aDelay)
330   {
331     // The base target may implement this, but we don't.  Always fail
332     // to provide consistent behavior.
333     return NS_ERROR_NOT_IMPLEMENTED;
334   }
335 
336   nsresult
IsOnCurrentThread(bool * aResult)337   IsOnCurrentThread(bool* aResult)
338   {
339     // Any thread
340 
341     bool shutdownAndIdle = false;
342     {
343       MutexAutoLock lock(mMutex);
344       shutdownAndIdle = mShutdownStarted && mEventQueue.Count(lock) == 0;
345     }
346 
347     bool onBaseTarget = false;
348     nsresult rv = mBaseTarget->IsOnCurrentThread(&onBaseTarget);
349     if (NS_FAILED(rv)) {
350       return rv;
351     }
352 
353     // We consider the current stack on this event target if are on
354     // the base target and one of the following is true
355     //  1) We are currently running an event OR
356     //  2) We are both shutting down and the queue is idle
357     *aResult = onBaseTarget && (mExecutionDepth || shutdownAndIdle);
358 
359     return NS_OK;
360   }
361 
362   NS_DECL_THREADSAFE_ISUPPORTS
363 };
364 
365 NS_IMPL_ISUPPORTS(ThrottledEventQueue::Inner, nsIObserver);
366 
367 NS_IMPL_ISUPPORTS(ThrottledEventQueue, nsIEventTarget);
368 
ThrottledEventQueue(already_AddRefed<Inner> aInner)369 ThrottledEventQueue::ThrottledEventQueue(already_AddRefed<Inner> aInner)
370   : mInner(aInner)
371 {
372   MOZ_ASSERT(mInner);
373 }
374 
~ThrottledEventQueue()375 ThrottledEventQueue::~ThrottledEventQueue()
376 {
377   mInner->MaybeStartShutdown();
378 }
379 
380 void
MaybeStartShutdown()381 ThrottledEventQueue::MaybeStartShutdown()
382 {
383   return mInner->MaybeStartShutdown();
384 }
385 
386 already_AddRefed<ThrottledEventQueue>
Create(nsIEventTarget * aBaseTarget)387 ThrottledEventQueue::Create(nsIEventTarget* aBaseTarget)
388 {
389   MOZ_ASSERT(NS_IsMainThread());
390   MOZ_ASSERT(aBaseTarget);
391 
392   RefPtr<Inner> inner = Inner::Create(aBaseTarget);
393   if (NS_WARN_IF(!inner)) {
394     return nullptr;
395   }
396 
397   RefPtr<ThrottledEventQueue> ref =
398     new ThrottledEventQueue(inner.forget());
399   return ref.forget();
400 }
401 
402 bool
IsEmpty() const403 ThrottledEventQueue::IsEmpty() const
404 {
405   return mInner->IsEmpty();
406 }
407 
408 uint32_t
Length() const409 ThrottledEventQueue::Length() const
410 {
411   return mInner->Length();
412 }
413 
414 void
AwaitIdle() const415 ThrottledEventQueue::AwaitIdle() const
416 {
417   return mInner->AwaitIdle();
418 }
419 
420 NS_IMETHODIMP
DispatchFromScript(nsIRunnable * aEvent,uint32_t aFlags)421 ThrottledEventQueue::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags)
422 {
423   return mInner->DispatchFromScript(aEvent, aFlags);
424 }
425 
426 NS_IMETHODIMP
Dispatch(already_AddRefed<nsIRunnable> aEvent,uint32_t aFlags)427 ThrottledEventQueue::Dispatch(already_AddRefed<nsIRunnable> aEvent,
428                                      uint32_t aFlags)
429 {
430   return mInner->Dispatch(Move(aEvent), aFlags);
431 }
432 
433 NS_IMETHODIMP
DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,uint32_t aFlags)434 ThrottledEventQueue::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
435                                             uint32_t aFlags)
436 {
437   return mInner->DelayedDispatch(Move(aEvent), aFlags);
438 }
439 
440 NS_IMETHODIMP
IsOnCurrentThread(bool * aResult)441 ThrottledEventQueue::IsOnCurrentThread(bool* aResult)
442 {
443   return mInner->IsOnCurrentThread(aResult);
444 }
445 
446 } // namespace mozilla
447