Skip to content

Commit 9717c27

Browse files
committed
Bug 1695580 - In xpcom, cancel pending DelayedRunnable timers on shutdown. r=KrisWright
Because DelayedRunnables are fire-and-forget, there is no way for a targeted EventTarget to clean them up on shutdown. Thus if a timer fires after EventTarget shutdown it will fail to dispatch the timer event, and avoid releasing the timer callback because it's not on the targeted thread. This causes a leak as there is a ref-cycle between nsTimerImpl::mCallback and DelayedRunnable::mTimer. This patch adds nsIDelayedRunnableObserver for a target to observe which DelayedRunnables are relying on their timer to run them. This allows the target to schedule a shutdown task to cancel those timers and release the runnables on the target thread. Supported DelayedRunnable targets with this patch are TaskQueues, eventqueue-based nsThreads and XPCOMThreadWrappers that wrap a supported nsThread. An assertion makes sure at runtime that future new uses of DelayedRunnable target nsIDelayedRunnableObserver-supported event targets. Differential Revision: https://phabricator.services.mozilla.com/D109781
1 parent f600c8f commit 9717c27

14 files changed

+295
-10
lines changed

xpcom/build/XPCOMInit.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ nsresult ShutdownXPCOM(nsIServiceManager* aServMgr) {
621621

622622
mozilla::AppShutdown::AdvanceShutdownPhase(
623623
mozilla::ShutdownPhase::XPCOMShutdownThreads);
624+
nsThreadManager::get().CancelBackgroundDelayedRunnables();
624625
gXPCOMThreadsShutDown = true;
625626
NS_ProcessPendingEvents(thread);
626627

xpcom/threads/AbstractThread.cpp

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ MOZ_THREAD_LOCAL(AbstractThread*) AbstractThread::sCurrentThreadTLS;
3131

3232
class XPCOMThreadWrapper final : public AbstractThread,
3333
public nsIThreadObserver,
34-
public nsIDirectTaskDispatcher {
34+
public nsIDirectTaskDispatcher,
35+
public nsIDelayedRunnableObserver {
3536
public:
3637
XPCOMThreadWrapper(nsIThreadInternal* aThread, bool aRequireTailDispatch,
3738
bool aOnThread)
3839
: AbstractThread(aRequireTailDispatch),
3940
mThread(aThread),
41+
mDelayedRunnableObserver(do_QueryInterface(mThread)),
4042
mDirectTaskDispatcher(do_QueryInterface(aThread)),
4143
mOnThread(aOnThread) {
4244
MOZ_DIAGNOSTIC_ASSERT(mThread && mDirectTaskDispatcher);
@@ -158,8 +160,22 @@ class XPCOMThreadWrapper final : public AbstractThread,
158160
return mDirectTaskDispatcher->HaveDirectTasks(aResult);
159161
}
160162

163+
//-----------------------------------------------------------------------------
164+
// nsIDelayedRunnableObserver
165+
//-----------------------------------------------------------------------------
166+
void OnDelayedRunnableCreated(DelayedRunnable* aRunnable) override {
167+
mDelayedRunnableObserver->OnDelayedRunnableCreated(aRunnable);
168+
}
169+
void OnDelayedRunnableScheduled(DelayedRunnable* aRunnable) override {
170+
mDelayedRunnableObserver->OnDelayedRunnableScheduled(aRunnable);
171+
}
172+
void OnDelayedRunnableRan(DelayedRunnable* aRunnable) override {
173+
mDelayedRunnableObserver->OnDelayedRunnableRan(aRunnable);
174+
}
175+
161176
private:
162177
const RefPtr<nsIThreadInternal> mThread;
178+
const nsCOMPtr<nsIDelayedRunnableObserver> mDelayedRunnableObserver;
163179
const nsCOMPtr<nsIDirectTaskDispatcher> mDirectTaskDispatcher;
164180
Maybe<AutoTaskDispatcher> mTailDispatcher;
165181
const bool mOnThread;
@@ -215,8 +231,16 @@ class XPCOMThreadWrapper final : public AbstractThread,
215231
const RefPtr<nsIRunnable> mRunnable;
216232
};
217233
};
218-
NS_IMPL_ISUPPORTS_INHERITED(XPCOMThreadWrapper, AbstractThread,
219-
nsIThreadObserver, nsIDirectTaskDispatcher);
234+
235+
NS_INTERFACE_MAP_BEGIN(XPCOMThreadWrapper)
236+
NS_INTERFACE_MAP_ENTRY(nsIThreadObserver)
237+
NS_INTERFACE_MAP_ENTRY(nsIDirectTaskDispatcher)
238+
NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIDelayedRunnableObserver,
239+
mDelayedRunnableObserver)
240+
NS_INTERFACE_MAP_END_INHERITING(AbstractThread)
241+
242+
NS_IMPL_ADDREF_INHERITED(XPCOMThreadWrapper, AbstractThread)
243+
NS_IMPL_RELEASE_INHERITED(XPCOMThreadWrapper, AbstractThread)
220244

221245
NS_IMPL_ISUPPORTS(AbstractThread, nsIEventTarget, nsISerialEventTarget)
222246

xpcom/threads/DelayedRunnable.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,25 @@ DelayedRunnable::DelayedRunnable(already_AddRefed<nsIEventTarget> aTarget,
1313
uint32_t aDelay)
1414
: mozilla::Runnable("DelayedRunnable"),
1515
mTarget(aTarget),
16+
mObserver(do_QueryInterface(mTarget)),
1617
mWrappedRunnable(aRunnable),
1718
mDelayedFrom(TimeStamp::NowLoRes()),
18-
mDelay(aDelay) {}
19+
mDelay(aDelay) {
20+
MOZ_DIAGNOSTIC_ASSERT(mObserver,
21+
"Target must implement nsIDelayedRunnableObserver");
22+
}
1923

2024
nsresult DelayedRunnable::Init() {
25+
mObserver->OnDelayedRunnableCreated(this);
2126
return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, mDelay,
2227
nsITimer::TYPE_ONE_SHOT, mTarget);
2328
}
2429

30+
void DelayedRunnable::CancelTimer() {
31+
MOZ_ASSERT(mTarget->IsOnCurrentThread());
32+
mTimer->Cancel();
33+
}
34+
2535
NS_IMETHODIMP DelayedRunnable::Run() {
2636
MOZ_ASSERT(mTimer, "DelayedRunnable without Init?");
2737

@@ -33,6 +43,9 @@ NS_IMETHODIMP DelayedRunnable::Run() {
3343
// Are we too early?
3444
if ((mozilla::TimeStamp::NowLoRes() - mDelayedFrom).ToMilliseconds() <
3545
mDelay) {
46+
if (mObserver) {
47+
mObserver->OnDelayedRunnableScheduled(this);
48+
}
3649
return NS_OK; // Let the nsITimer run us.
3750
}
3851

@@ -44,6 +57,9 @@ NS_IMETHODIMP DelayedRunnable::Notify(nsITimer* aTimer) {
4457
// If we already ran, the timer should have been canceled.
4558
MOZ_ASSERT(mWrappedRunnable);
4659

60+
if (mObserver) {
61+
mObserver->OnDelayedRunnableRan(this);
62+
}
4763
return DoRun();
4864
}
4965

xpcom/threads/DelayedRunnable.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "mozilla/TimeStamp.h"
1111
#include "nsCOMPtr.h"
12+
#include "nsIDelayedRunnableObserver.h"
1213
#include "nsIRunnable.h"
1314
#include "nsITimer.h"
1415
#include "nsThreadUtils.h"
@@ -26,11 +27,18 @@ class DelayedRunnable : public Runnable, public nsITimerCallback {
2627

2728
nsresult Init();
2829

30+
/**
31+
* Cancels the underlying timer. Called when the target is going away, so the
32+
* runnable can be released safely on the target thread.
33+
*/
34+
void CancelTimer();
35+
2936
private:
3037
~DelayedRunnable() = default;
3138
nsresult DoRun();
3239

3340
const nsCOMPtr<nsIEventTarget> mTarget;
41+
const nsCOMPtr<nsIDelayedRunnableObserver> mObserver;
3442
nsCOMPtr<nsIRunnable> mWrappedRunnable;
3543
nsCOMPtr<nsITimer> mTimer;
3644
const TimeStamp mDelayedFrom;

xpcom/threads/TaskQueue.cpp

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "mozilla/TaskQueue.h"
88

9+
#include "mozilla/DelayedRunnable.h"
910
#include "nsThreadUtils.h"
1011

1112
namespace mozilla {
@@ -27,9 +28,11 @@ TaskQueue::TaskQueue(already_AddRefed<nsIEventTarget> aTarget,
2728
TaskQueue::~TaskQueue() {
2829
// No one is referencing this TaskQueue anymore, meaning no tasks can be
2930
// pending as all Runner hold a reference to this TaskQueue.
31+
MOZ_ASSERT(mScheduledDelayedRunnables.IsEmpty());
3032
}
3133

32-
NS_IMPL_ISUPPORTS_INHERITED(TaskQueue, AbstractThread, nsIDirectTaskDispatcher);
34+
NS_IMPL_ISUPPORTS_INHERITED(TaskQueue, AbstractThread, nsIDirectTaskDispatcher,
35+
nsIDelayedRunnableObserver);
3336

3437
TaskDispatcher& TaskQueue::TailDispatcher() {
3538
MOZ_ASSERT(IsCurrentThreadIn());
@@ -104,13 +107,60 @@ void TaskQueue::AwaitShutdownAndIdle() {
104107
AwaitIdleLocked();
105108
}
106109

110+
void TaskQueue::OnDelayedRunnableCreated(DelayedRunnable* aRunnable) {
111+
#ifdef DEBUG
112+
MonitorAutoLock mon(mQueueMonitor);
113+
MOZ_ASSERT(!mDelayedRunnablesCancelPromise);
114+
#endif
115+
}
116+
117+
void TaskQueue::OnDelayedRunnableScheduled(DelayedRunnable* aRunnable) {
118+
MOZ_ASSERT(IsOnCurrentThread());
119+
mScheduledDelayedRunnables.AppendElement(aRunnable);
120+
}
121+
122+
void TaskQueue::OnDelayedRunnableRan(DelayedRunnable* aRunnable) {
123+
MOZ_ASSERT(IsOnCurrentThread());
124+
MOZ_ALWAYS_TRUE(mScheduledDelayedRunnables.RemoveElement(aRunnable));
125+
}
126+
127+
auto TaskQueue::CancelDelayedRunnables() -> RefPtr<CancelPromise> {
128+
MonitorAutoLock mon(mQueueMonitor);
129+
return CancelDelayedRunnablesLocked();
130+
}
131+
132+
auto TaskQueue::CancelDelayedRunnablesLocked() -> RefPtr<CancelPromise> {
133+
mQueueMonitor.AssertCurrentThreadOwns();
134+
if (mDelayedRunnablesCancelPromise) {
135+
return mDelayedRunnablesCancelPromise;
136+
}
137+
mDelayedRunnablesCancelPromise =
138+
mDelayedRunnablesCancelHolder.Ensure(__func__);
139+
nsCOMPtr<nsIRunnable> cancelRunnable =
140+
NewRunnableMethod("TaskQueue::CancelDelayedRunnablesImpl", this,
141+
&TaskQueue::CancelDelayedRunnablesImpl);
142+
MOZ_ALWAYS_SUCCEEDS(DispatchLocked(/* passed by ref */ cancelRunnable,
143+
NS_DISPATCH_NORMAL, TailDispatch));
144+
return mDelayedRunnablesCancelPromise;
145+
}
146+
147+
void TaskQueue::CancelDelayedRunnablesImpl() {
148+
MOZ_ASSERT(IsOnCurrentThread());
149+
for (const auto& runnable : mScheduledDelayedRunnables) {
150+
runnable->CancelTimer();
151+
}
152+
mScheduledDelayedRunnables.Clear();
153+
mDelayedRunnablesCancelHolder.Resolve(true, __func__);
154+
}
155+
107156
RefPtr<ShutdownPromise> TaskQueue::BeginShutdown() {
108157
// Dispatch any tasks for this queue waiting in the caller's tail dispatcher,
109158
// since this is the last opportunity to do so.
110159
if (AbstractThread* currentThread = AbstractThread::GetCurrent()) {
111160
currentThread->TailDispatchTasksFor(this);
112161
}
113162
MonitorAutoLock mon(mQueueMonitor);
163+
Unused << CancelDelayedRunnablesLocked();
114164
mIsShutdown = true;
115165
RefPtr<ShutdownPromise> p = mShutdownPromise.Ensure(__func__);
116166
MaybeResolveShutdown();

xpcom/threads/TaskQueue.h

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "mozilla/MozPromise.h"
1616
#include "mozilla/RefPtr.h"
1717
#include "mozilla/TaskDispatcher.h"
18+
#include "nsIDelayedRunnableObserver.h"
1819
#include "nsIDirectTaskDispatcher.h"
1920
#include "nsThreadUtils.h"
2021

@@ -47,7 +48,9 @@ typedef MozPromise<bool, bool, false> ShutdownPromise;
4748
// A TaskQueue does not require explicit shutdown, however it provides a
4849
// BeginShutdown() method that places TaskQueue in a shut down state and returns
4950
// a promise that gets resolved once all pending tasks have completed
50-
class TaskQueue : public AbstractThread, public nsIDirectTaskDispatcher {
51+
class TaskQueue : public AbstractThread,
52+
public nsIDirectTaskDispatcher,
53+
public nsIDelayedRunnableObserver {
5154
class EventTargetWrapper;
5255

5356
public:
@@ -93,6 +96,18 @@ class TaskQueue : public AbstractThread, public nsIDirectTaskDispatcher {
9396
// So we can access nsIEventTarget::Dispatch(nsIRunnable*, uint32_t aFlags)
9497
using nsIEventTarget::Dispatch;
9598

99+
// nsIDelayedRunnableObserver
100+
void OnDelayedRunnableCreated(DelayedRunnable* aRunnable) override;
101+
void OnDelayedRunnableScheduled(DelayedRunnable* aRunnable) override;
102+
void OnDelayedRunnableRan(DelayedRunnable* aRunnable) override;
103+
104+
using CancelPromise = MozPromise<bool, bool, false>;
105+
106+
// Dispatches a task to cancel any pending DelayedRunnables. Idempotent. Only
107+
// dispatches the task on the first call. Creating DelayedRunnables after this
108+
// is called will result in assertion failures.
109+
RefPtr<CancelPromise> CancelDelayedRunnables();
110+
96111
// Puts the queue in a shutdown state and returns immediately. The queue will
97112
// remain alive at least until all the events are drained, because the Runners
98113
// hold a strong reference to the task queue, and one of them is always held
@@ -126,6 +141,11 @@ class TaskQueue : public AbstractThread, public nsIDirectTaskDispatcher {
126141
nsresult DispatchLocked(nsCOMPtr<nsIRunnable>& aRunnable, uint32_t aFlags,
127142
DispatchReason aReason = NormalDispatch);
128143

144+
RefPtr<CancelPromise> CancelDelayedRunnablesLocked();
145+
146+
// Cancels any scheduled DelayedRunnables on this TaskQueue.
147+
void CancelDelayedRunnablesImpl();
148+
129149
void MaybeResolveShutdown() {
130150
mQueueMonitor.AssertCurrentThreadOwns();
131151
if (mIsShutdown && !mIsRunning) {
@@ -136,7 +156,8 @@ class TaskQueue : public AbstractThread, public nsIDirectTaskDispatcher {
136156

137157
nsCOMPtr<nsIEventTarget> mTarget;
138158

139-
// Monitor that protects the queue and mIsRunning;
159+
// Monitor that protects the queue, mIsRunning and
160+
// mDelayedRunnablesCancelPromise;
140161
Monitor mQueueMonitor;
141162

142163
typedef struct TaskStruct {
@@ -147,6 +168,18 @@ class TaskQueue : public AbstractThread, public nsIDirectTaskDispatcher {
147168
// Queue of tasks to run.
148169
std::queue<TaskStruct> mTasks;
149170

171+
// DelayedRunnables (from DelayedDispatch) that are managed by their
172+
// respective timers, but have not yet run. Accessed only on this
173+
// TaskQueue.
174+
nsTArray<RefPtr<DelayedRunnable>> mScheduledDelayedRunnables;
175+
176+
// Manages resolving mDelayedRunnablesCancelPromise.
177+
MozPromiseHolder<CancelPromise> mDelayedRunnablesCancelHolder;
178+
179+
// Set once the task to cancel all pending DelayedRunnables has been
180+
// dispatched. Guarded by mQueueMonitor.
181+
RefPtr<CancelPromise> mDelayedRunnablesCancelPromise;
182+
150183
// The thread currently running the task queue. We store a reference
151184
// to this so that IsCurrentThreadIn() can tell if the current thread
152185
// is the thread currently running in the task queue.

xpcom/threads/ThreadEventTarget.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ ThreadEventTarget::ThreadEventTarget(ThreadTargetSink* aSink,
3333
mThread = PR_GetCurrentThread();
3434
}
3535

36+
ThreadEventTarget::~ThreadEventTarget() {
37+
MOZ_ASSERT(mScheduledDelayedRunnables.IsEmpty());
38+
}
39+
3640
void ThreadEventTarget::SetCurrentThread(PRThread* aThread) {
3741
mThread = aThread;
3842
}
3943

4044
void ThreadEventTarget::ClearCurrentThread() { mThread = nullptr; }
4145

42-
NS_IMPL_ISUPPORTS(ThreadEventTarget, nsIEventTarget, nsISerialEventTarget)
46+
NS_IMPL_ISUPPORTS(ThreadEventTarget, nsIEventTarget, nsISerialEventTarget,
47+
nsIDelayedRunnableObserver)
4348

4449
NS_IMETHODIMP
4550
ThreadEventTarget::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) {
@@ -136,3 +141,23 @@ ThreadEventTarget::IsOnCurrentThreadInfallible() {
136141
// we are called, we can never be on this thread.
137142
return false;
138143
}
144+
145+
void ThreadEventTarget::OnDelayedRunnableCreated(DelayedRunnable* aRunnable) {}
146+
147+
void ThreadEventTarget::OnDelayedRunnableScheduled(DelayedRunnable* aRunnable) {
148+
MOZ_ASSERT(IsOnCurrentThread());
149+
mScheduledDelayedRunnables.AppendElement(aRunnable);
150+
}
151+
152+
void ThreadEventTarget::OnDelayedRunnableRan(DelayedRunnable* aRunnable) {
153+
MOZ_ASSERT(IsOnCurrentThread());
154+
MOZ_ALWAYS_TRUE(mScheduledDelayedRunnables.RemoveElement(aRunnable));
155+
}
156+
157+
void ThreadEventTarget::NotifyShutdown() {
158+
MOZ_ASSERT(IsOnCurrentThread());
159+
for (const auto& runnable : mScheduledDelayedRunnables) {
160+
runnable->CancelTimer();
161+
}
162+
mScheduledDelayedRunnables.Clear();
163+
}

xpcom/threads/ThreadEventTarget.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,31 @@
1010
#include "mozilla/MemoryReporting.h"
1111
#include "mozilla/Mutex.h"
1212
#include "mozilla/SynchronizedEventQueue.h" // for ThreadTargetSink
13+
#include "nsIDelayedRunnableObserver.h"
1314
#include "nsISerialEventTarget.h"
1415

1516
namespace mozilla {
17+
class DelayedRunnable;
1618

1719
// ThreadEventTarget handles the details of posting an event to a thread. It can
1820
// be used with any ThreadTargetSink implementation.
19-
class ThreadEventTarget final : public nsISerialEventTarget {
21+
class ThreadEventTarget final : public nsISerialEventTarget,
22+
public nsIDelayedRunnableObserver {
2023
public:
2124
ThreadEventTarget(ThreadTargetSink* aSink, bool aIsMainThread);
2225

2326
NS_DECL_THREADSAFE_ISUPPORTS
2427
NS_DECL_NSIEVENTTARGET_FULL
2528

29+
// nsIDelayedRunnableObserver
30+
void OnDelayedRunnableCreated(DelayedRunnable* aRunnable) override;
31+
void OnDelayedRunnableScheduled(DelayedRunnable* aRunnable) override;
32+
void OnDelayedRunnableRan(DelayedRunnable* aRunnable) override;
33+
34+
// Notification from, and on, the owner thread that it is shutting down.
35+
// Cancels any scheduled DelayedRunnables.
36+
void NotifyShutdown();
37+
2638
// Disconnects the target so that it can no longer post events.
2739
void Disconnect(const MutexAutoLock& aProofOfLock) {
2840
mSink->Disconnect(aProofOfLock);
@@ -43,10 +55,14 @@ class ThreadEventTarget final : public nsISerialEventTarget {
4355
}
4456

4557
private:
46-
~ThreadEventTarget() = default;
58+
~ThreadEventTarget();
4759

4860
RefPtr<ThreadTargetSink> mSink;
4961
bool mIsMainThread;
62+
63+
// DelayedRunnables (from DelayedDispatch) that are managed by their
64+
// respective timers, but have not yet run. Accessed only on this nsThread.
65+
nsTArray<RefPtr<mozilla::DelayedRunnable>> mScheduledDelayedRunnables;
5066
};
5167

5268
} // namespace mozilla

0 commit comments

Comments
 (0)