Line data Source code
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 39 : class Executor final : public Runnable
63 : {
64 : RefPtr<Inner> mInner;
65 :
66 : public:
67 14 : explicit Executor(Inner* aInner)
68 14 : : Runnable("ThrottledEventQueue::Inner::Executor")
69 14 : , mInner(aInner)
70 14 : { }
71 :
72 : NS_IMETHODIMP
73 13 : Run() override
74 : {
75 13 : mInner->ExecuteRunnable();
76 13 : return NS_OK;
77 : }
78 :
79 : NS_IMETHODIMP
80 14 : GetName(nsACString& aName) override
81 : {
82 14 : return mInner->CurrentName(aName);
83 : }
84 : };
85 :
86 : mutable Mutex mMutex;
87 : mutable CondVar mIdleCondVar;
88 :
89 : mozilla::CondVar mEventsAvailable;
90 :
91 : // any thread, protected by mutex
92 : nsEventQueue mEventQueue;
93 :
94 : // written on main thread, read on any thread
95 : nsCOMPtr<nsISerialEventTarget> mBaseTarget;
96 :
97 : // any thread, protected by mutex
98 : nsCOMPtr<nsIRunnable> mExecutor;
99 :
100 : // any thread, protected by mutex
101 : bool mShutdownStarted;
102 :
103 5 : explicit Inner(nsISerialEventTarget* aBaseTarget)
104 5 : : mMutex("ThrottledEventQueue")
105 : , mIdleCondVar(mMutex, "ThrottledEventQueue:Idle")
106 : , mEventsAvailable(mMutex, "[ThrottledEventQueue::Inner.mEventsAvailable]")
107 : , mEventQueue(mEventsAvailable, nsEventQueue::eNormalQueue)
108 : , mBaseTarget(aBaseTarget)
109 5 : , mShutdownStarted(false)
110 : {
111 5 : }
112 :
113 0 : ~Inner()
114 0 : {
115 0 : MOZ_ASSERT(!mExecutor);
116 0 : MOZ_ASSERT(mShutdownStarted);
117 0 : }
118 :
119 : nsresult
120 14 : CurrentName(nsACString& aName)
121 : {
122 28 : nsCOMPtr<nsIRunnable> event;
123 :
124 : #ifdef DEBUG
125 14 : bool currentThread = false;
126 14 : mBaseTarget->IsOnCurrentThread(¤tThread);
127 14 : MOZ_ASSERT(currentThread);
128 : #endif
129 :
130 : {
131 28 : MutexAutoLock lock(mMutex);
132 :
133 : // We only check the name of an executor runnable when we know there is something
134 : // in the queue, so this should never fail.
135 14 : MOZ_ALWAYS_TRUE(mEventQueue.PeekEvent(getter_AddRefs(event), lock));
136 : }
137 :
138 15 : if (nsCOMPtr<nsINamed> named = do_QueryInterface(event)) {
139 13 : nsresult rv = named->GetName(aName);
140 13 : return rv;
141 : }
142 :
143 1 : aName.AssignLiteral("non-nsINamed ThrottledEventQueue runnable");
144 1 : return NS_OK;
145 : }
146 :
147 : void
148 13 : ExecuteRunnable()
149 : {
150 : // Any thread
151 26 : nsCOMPtr<nsIRunnable> event;
152 13 : bool shouldShutdown = false;
153 :
154 : #ifdef DEBUG
155 13 : bool currentThread = false;
156 13 : mBaseTarget->IsOnCurrentThread(¤tThread);
157 13 : MOZ_ASSERT(currentThread);
158 : #endif
159 :
160 : {
161 26 : MutexAutoLock lock(mMutex);
162 :
163 : // We only dispatch an executor runnable when we know there is something
164 : // in the queue, so this should never fail.
165 13 : MOZ_ALWAYS_TRUE(mEventQueue.GetPendingEvent(getter_AddRefs(event), lock));
166 :
167 : // If there are more events in the queue, then dispatch the next
168 : // executor. We do this now, before running the event, because
169 : // the event might spin the event loop and we don't want to stall
170 : // the queue.
171 13 : if (mEventQueue.HasPendingEvent(lock)) {
172 : // Dispatch the next base target runnable to attempt to execute
173 : // the next throttled event. We must do this before executing
174 : // the event in case the event spins the event loop.
175 0 : MOZ_ALWAYS_SUCCEEDS(
176 : mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL));
177 : }
178 :
179 : // Otherwise the queue is empty and we can stop dispatching the
180 : // executor. We might also need to shutdown after running the
181 : // last event.
182 : else {
183 13 : shouldShutdown = mShutdownStarted;
184 : // Note, this breaks a ref cycle.
185 13 : mExecutor = nullptr;
186 13 : mIdleCondVar.NotifyAll();
187 : }
188 : }
189 :
190 : // Execute the event now that we have unlocked.
191 13 : Unused << event->Run();
192 :
193 : // If shutdown was started and the queue is now empty we can now
194 : // finalize the shutdown. This is performed separately at the end
195 : // of the method in order to wait for the event to finish running.
196 13 : if (shouldShutdown) {
197 0 : MOZ_ASSERT(IsEmpty());
198 0 : NS_DispatchToMainThread(NewRunnableMethod("ThrottledEventQueue::Inner::ShutdownComplete",
199 0 : this, &Inner::ShutdownComplete));
200 : }
201 13 : }
202 :
203 : void
204 0 : ShutdownComplete()
205 : {
206 0 : MOZ_ASSERT(NS_IsMainThread());
207 0 : MOZ_ASSERT(IsEmpty());
208 0 : nsCOMPtr<nsIObserverService> obs = GetObserverService();
209 0 : obs->RemoveObserver(this, kShutdownTopic);
210 0 : }
211 :
212 : public:
213 : static already_AddRefed<Inner>
214 5 : Create(nsISerialEventTarget* aBaseTarget)
215 : {
216 5 : MOZ_ASSERT(NS_IsMainThread());
217 :
218 5 : if (ClearOnShutdown_Internal::sCurrentShutdownPhase != ShutdownPhase::NotInShutdown) {
219 0 : return nullptr;
220 : }
221 :
222 10 : nsCOMPtr<nsIObserverService> obs = GetObserverService();
223 5 : if (NS_WARN_IF(!obs)) {
224 0 : return nullptr;
225 : }
226 :
227 10 : RefPtr<Inner> ref = new Inner(aBaseTarget);
228 :
229 5 : nsresult rv = obs->AddObserver(ref, kShutdownTopic,
230 5 : false /* means OS will hold a strong ref */);
231 5 : if (NS_WARN_IF(NS_FAILED(rv))) {
232 0 : ref->MaybeStartShutdown();
233 0 : MOZ_ASSERT(ref->IsEmpty());
234 0 : return nullptr;
235 : }
236 :
237 5 : return ref.forget();
238 : }
239 :
240 : NS_IMETHOD
241 0 : Observe(nsISupports*, const char* aTopic, const char16_t*) override
242 : {
243 0 : MOZ_ASSERT(NS_IsMainThread());
244 0 : MOZ_ASSERT(!strcmp(aTopic, kShutdownTopic));
245 :
246 0 : MaybeStartShutdown();
247 :
248 : // Once shutdown begins we set the Atomic<bool> mShutdownStarted flag.
249 : // This prevents any new runnables from being dispatched into the
250 : // TaskQueue. Therefore this loop should be finite.
251 0 : MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() -> bool {
252 : return IsEmpty();
253 : }));
254 :
255 0 : return NS_OK;
256 : }
257 :
258 : void
259 0 : MaybeStartShutdown()
260 : {
261 : // Any thread
262 0 : MutexAutoLock lock(mMutex);
263 :
264 0 : if (mShutdownStarted) {
265 0 : return;
266 : }
267 0 : mShutdownStarted = true;
268 :
269 : // We are marked for shutdown now, but we are still processing runnables.
270 : // Return for now. The shutdown will be completed once the queue is
271 : // drained.
272 0 : if (mExecutor) {
273 0 : return;
274 : }
275 :
276 : // The queue is empty, so we can complete immediately.
277 0 : NS_DispatchToMainThread(NewRunnableMethod("ThrottledEventQueue::Inner::ShutdownComplete",
278 0 : this, &Inner::ShutdownComplete));
279 : }
280 :
281 : bool
282 0 : IsEmpty() const
283 : {
284 : // Any thread
285 0 : return Length() == 0;
286 : }
287 :
288 : uint32_t
289 0 : Length() const
290 : {
291 : // Any thread
292 0 : MutexAutoLock lock(mMutex);
293 0 : return mEventQueue.Count(lock);
294 : }
295 :
296 : void
297 0 : AwaitIdle() const
298 : {
299 : // Any thread, except the main thread or our base target. Blocking the
300 : // main thread is forbidden. Blocking the base target is guaranteed to
301 : // produce a deadlock.
302 0 : MOZ_ASSERT(!NS_IsMainThread());
303 : #ifdef DEBUG
304 0 : bool onBaseTarget = false;
305 0 : Unused << mBaseTarget->IsOnCurrentThread(&onBaseTarget);
306 0 : MOZ_ASSERT(!onBaseTarget);
307 : #endif
308 :
309 0 : MutexAutoLock lock(mMutex);
310 0 : while (mExecutor) {
311 0 : mIdleCondVar.Wait();
312 : }
313 0 : }
314 :
315 : nsresult
316 0 : DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags)
317 : {
318 : // Any thread
319 0 : nsCOMPtr<nsIRunnable> r = aEvent;
320 0 : return Dispatch(r.forget(), aFlags);
321 : }
322 :
323 : nsresult
324 14 : Dispatch(already_AddRefed<nsIRunnable> aEvent, uint32_t aFlags)
325 : {
326 14 : MOZ_ASSERT(aFlags == NS_DISPATCH_NORMAL ||
327 : aFlags == NS_DISPATCH_AT_END);
328 :
329 : // Any thread
330 28 : MutexAutoLock lock(mMutex);
331 :
332 : // If we are shutting down, just fall back to our base target
333 : // directly.
334 14 : if (mShutdownStarted) {
335 0 : return mBaseTarget->Dispatch(Move(aEvent), aFlags);
336 : }
337 :
338 : // We are not currently processing events, so we must start
339 : // operating on our base target. This is fallible, so do
340 : // it first. Our lock will prevent the executor from accessing
341 : // the event queue before we add the event below.
342 14 : if (!mExecutor) {
343 : // Note, this creates a ref cycle keeping the inner alive
344 : // until the queue is drained.
345 14 : mExecutor = new Executor(this);
346 14 : nsresult rv = mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL);
347 14 : if (NS_WARN_IF(NS_FAILED(rv))) {
348 0 : mExecutor = nullptr;
349 0 : return rv;
350 : }
351 : }
352 :
353 : // Only add the event to the underlying queue if are able to
354 : // dispatch to our base target.
355 14 : mEventQueue.PutEvent(Move(aEvent), lock);
356 14 : return NS_OK;
357 : }
358 :
359 : nsresult
360 0 : DelayedDispatch(already_AddRefed<nsIRunnable> aEvent, uint32_t aDelay)
361 : {
362 : // The base target may implement this, but we don't. Always fail
363 : // to provide consistent behavior.
364 0 : return NS_ERROR_NOT_IMPLEMENTED;
365 : }
366 :
367 : bool
368 0 : IsOnCurrentThread()
369 : {
370 0 : return mBaseTarget->IsOnCurrentThread();
371 : }
372 :
373 : NS_DECL_THREADSAFE_ISUPPORTS
374 : };
375 :
376 37 : NS_IMPL_ISUPPORTS(ThrottledEventQueue::Inner, nsIObserver);
377 :
378 38 : NS_IMPL_ISUPPORTS(ThrottledEventQueue,
379 : ThrottledEventQueue,
380 : nsIEventTarget,
381 : nsISerialEventTarget);
382 :
383 5 : ThrottledEventQueue::ThrottledEventQueue(already_AddRefed<Inner> aInner)
384 5 : : mInner(aInner)
385 : {
386 5 : MOZ_ASSERT(mInner);
387 5 : }
388 :
389 0 : ThrottledEventQueue::~ThrottledEventQueue()
390 : {
391 0 : mInner->MaybeStartShutdown();
392 0 : }
393 :
394 : void
395 0 : ThrottledEventQueue::MaybeStartShutdown()
396 : {
397 0 : return mInner->MaybeStartShutdown();
398 : }
399 :
400 : already_AddRefed<ThrottledEventQueue>
401 5 : ThrottledEventQueue::Create(nsISerialEventTarget* aBaseTarget)
402 : {
403 5 : MOZ_ASSERT(NS_IsMainThread());
404 5 : MOZ_ASSERT(aBaseTarget);
405 :
406 10 : RefPtr<Inner> inner = Inner::Create(aBaseTarget);
407 5 : if (NS_WARN_IF(!inner)) {
408 0 : return nullptr;
409 : }
410 :
411 : RefPtr<ThrottledEventQueue> ref =
412 15 : new ThrottledEventQueue(inner.forget());
413 5 : return ref.forget();
414 : }
415 :
416 : bool
417 0 : ThrottledEventQueue::IsEmpty() const
418 : {
419 0 : return mInner->IsEmpty();
420 : }
421 :
422 : uint32_t
423 0 : ThrottledEventQueue::Length() const
424 : {
425 0 : return mInner->Length();
426 : }
427 :
428 : void
429 0 : ThrottledEventQueue::AwaitIdle() const
430 : {
431 0 : return mInner->AwaitIdle();
432 : }
433 :
434 : NS_IMETHODIMP
435 0 : ThrottledEventQueue::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags)
436 : {
437 0 : return mInner->DispatchFromScript(aEvent, aFlags);
438 : }
439 :
440 : NS_IMETHODIMP
441 14 : ThrottledEventQueue::Dispatch(already_AddRefed<nsIRunnable> aEvent,
442 : uint32_t aFlags)
443 : {
444 14 : return mInner->Dispatch(Move(aEvent), aFlags);
445 : }
446 :
447 : NS_IMETHODIMP
448 0 : ThrottledEventQueue::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
449 : uint32_t aFlags)
450 : {
451 0 : return mInner->DelayedDispatch(Move(aEvent), aFlags);
452 : }
453 :
454 : NS_IMETHODIMP
455 0 : ThrottledEventQueue::IsOnCurrentThread(bool* aResult)
456 : {
457 0 : *aResult = mInner->IsOnCurrentThread();
458 0 : return NS_OK;
459 : }
460 :
461 : NS_IMETHODIMP_(bool)
462 0 : ThrottledEventQueue::IsOnCurrentThreadInfallible()
463 : {
464 0 : return mInner->IsOnCurrentThread();
465 : }
466 :
467 : } // namespace mozilla
|