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(¤tThread);
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