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 file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #include "nsThreadUtils.h"
10 #include "mozilla/CycleCollectedJSContext.h"
11 #include "mozilla/RefPtr.h"
12 #include "mozilla/SchedulerGroup.h"
13 #include "mozilla/ThreadLocal.h"
14 #include "mozilla/TimeStamp.h"
16 #include "mozilla/dom/BindingDeclarations.h"
17 #include "mozilla/dom/ContentChild.h"
18 #include "mozilla/dom/Promise.h"
19 #include "mozilla/dom/PromiseBinding.h"
20 #include "mozilla/dom/PromiseDebugging.h"
21 #include "mozilla/dom/PromiseDebuggingBinding.h"
26 class FlushRejections
: public DiscardableRunnable
{
28 FlushRejections() : DiscardableRunnable("dom::FlushRejections") {}
31 if (!sDispatched
.init()) {
32 MOZ_CRASH("Could not initialize FlushRejections::sDispatched");
34 sDispatched
.set(false);
37 static void DispatchNeeded() {
38 if (sDispatched
.get()) {
39 // An instance of `FlushRejections` has already been dispatched
40 // and not run yet. No need to dispatch another one.
43 sDispatched
.set(true);
45 // Dispatch the runnable to the current thread where
46 // the Promise was rejected, e.g. workers or worklets.
47 NS_DispatchToCurrentThread(new FlushRejections());
50 static void FlushSync() {
51 sDispatched
.set(false);
53 // Call the callbacks if necessary.
54 // Note that these callbacks may in turn cause Promise to turn
55 // uncaught or consumed. Since `sDispatched` is `false`,
56 // `FlushRejections` will be called once again, on an ulterior
58 PromiseDebugging::FlushUncaughtRejectionsInternal();
61 NS_IMETHOD
Run() override
{
67 // `true` if an instance of `FlushRejections` is currently dispatched
68 // and has not been executed yet.
69 static MOZ_THREAD_LOCAL(bool) sDispatched
;
72 /* static */ MOZ_THREAD_LOCAL(bool) FlushRejections::sDispatched
;
75 void PromiseDebugging::GetState(GlobalObject
& aGlobal
,
76 JS::Handle
<JSObject
*> aPromise
,
77 PromiseDebuggingStateHolder
& aState
,
79 JSContext
* cx
= aGlobal
.Context();
80 // CheckedUnwrapStatic is fine, since we're looking for promises only.
81 JS::Rooted
<JSObject
*> obj(cx
, js::CheckedUnwrapStatic(aPromise
));
82 if (!obj
|| !JS::IsPromiseObject(obj
)) {
83 aRv
.ThrowTypeError
<MSG_IS_NOT_PROMISE
>();
86 switch (JS::GetPromiseState(obj
)) {
87 case JS::PromiseState::Pending
:
88 aState
.mState
= PromiseDebuggingState::Pending
;
90 case JS::PromiseState::Fulfilled
:
91 aState
.mState
= PromiseDebuggingState::Fulfilled
;
92 aState
.mValue
= JS::GetPromiseResult(obj
);
94 case JS::PromiseState::Rejected
:
95 aState
.mState
= PromiseDebuggingState::Rejected
;
96 aState
.mReason
= JS::GetPromiseResult(obj
);
102 void PromiseDebugging::GetPromiseID(GlobalObject
& aGlobal
,
103 JS::Handle
<JSObject
*> aPromise
,
104 nsString
& aID
, ErrorResult
& aRv
) {
105 JSContext
* cx
= aGlobal
.Context();
106 // CheckedUnwrapStatic is fine, since we're looking for promises only.
107 JS::Rooted
<JSObject
*> obj(cx
, js::CheckedUnwrapStatic(aPromise
));
108 if (!obj
|| !JS::IsPromiseObject(obj
)) {
109 aRv
.ThrowTypeError
<MSG_IS_NOT_PROMISE
>();
112 uint64_t promiseID
= JS::GetPromiseID(obj
);
114 aID
.AppendInt(promiseID
);
118 void PromiseDebugging::GetAllocationStack(GlobalObject
& aGlobal
,
119 JS::Handle
<JSObject
*> aPromise
,
120 JS::MutableHandle
<JSObject
*> aStack
,
122 JSContext
* cx
= aGlobal
.Context();
123 // CheckedUnwrapStatic is fine, since we're looking for promises only.
124 JS::Rooted
<JSObject
*> obj(cx
, js::CheckedUnwrapStatic(aPromise
));
125 if (!obj
|| !JS::IsPromiseObject(obj
)) {
126 aRv
.ThrowTypeError
<MSG_IS_NOT_PROMISE
>();
129 aStack
.set(JS::GetPromiseAllocationSite(obj
));
133 void PromiseDebugging::GetRejectionStack(GlobalObject
& aGlobal
,
134 JS::Handle
<JSObject
*> aPromise
,
135 JS::MutableHandle
<JSObject
*> aStack
,
137 JSContext
* cx
= aGlobal
.Context();
138 // CheckedUnwrapStatic is fine, since we're looking for promises only.
139 JS::Rooted
<JSObject
*> obj(cx
, js::CheckedUnwrapStatic(aPromise
));
140 if (!obj
|| !JS::IsPromiseObject(obj
)) {
141 aRv
.ThrowTypeError
<MSG_IS_NOT_PROMISE
>();
144 aStack
.set(JS::GetPromiseResolutionSite(obj
));
148 void PromiseDebugging::GetFullfillmentStack(GlobalObject
& aGlobal
,
149 JS::Handle
<JSObject
*> aPromise
,
150 JS::MutableHandle
<JSObject
*> aStack
,
152 JSContext
* cx
= aGlobal
.Context();
153 // CheckedUnwrapStatic is fine, since we're looking for promises only.
154 JS::Rooted
<JSObject
*> obj(cx
, js::CheckedUnwrapStatic(aPromise
));
155 if (!obj
|| !JS::IsPromiseObject(obj
)) {
156 aRv
.ThrowTypeError
<MSG_IS_NOT_PROMISE
>();
159 aStack
.set(JS::GetPromiseResolutionSite(obj
));
163 nsString
PromiseDebugging::sIDPrefix
;
166 void PromiseDebugging::Init() {
167 FlushRejections::Init();
169 // Generate a prefix for identifiers: "PromiseDebugging.$processid."
170 sIDPrefix
= u
"PromiseDebugging."_ns
;
171 if (XRE_IsContentProcess()) {
172 sIDPrefix
.AppendInt(ContentChild::GetSingleton()->GetID());
173 sIDPrefix
.Append('.');
175 sIDPrefix
.AppendLiteral("0.");
180 void PromiseDebugging::Shutdown() { sIDPrefix
.SetIsVoid(true); }
183 void PromiseDebugging::FlushUncaughtRejections() {
184 MOZ_ASSERT(!NS_IsMainThread());
185 FlushRejections::FlushSync();
189 void PromiseDebugging::AddUncaughtRejectionObserver(
190 GlobalObject
&, UncaughtRejectionObserver
& aObserver
) {
191 CycleCollectedJSContext
* storage
= CycleCollectedJSContext::Get();
192 nsTArray
<nsCOMPtr
<nsISupports
>>& observers
=
193 storage
->mUncaughtRejectionObservers
;
194 observers
.AppendElement(&aObserver
);
198 bool PromiseDebugging::RemoveUncaughtRejectionObserver(
199 GlobalObject
&, UncaughtRejectionObserver
& aObserver
) {
200 CycleCollectedJSContext
* storage
= CycleCollectedJSContext::Get();
201 nsTArray
<nsCOMPtr
<nsISupports
>>& observers
=
202 storage
->mUncaughtRejectionObservers
;
203 for (size_t i
= 0; i
< observers
.Length(); ++i
) {
204 UncaughtRejectionObserver
* observer
=
205 static_cast<UncaughtRejectionObserver
*>(observers
[i
].get());
206 if (*observer
== aObserver
) {
207 observers
.RemoveElementAt(i
);
215 void PromiseDebugging::AddUncaughtRejection(JS::HandleObject aPromise
) {
216 // This might OOM, but won't set a pending exception, so we'll just ignore it.
217 if (CycleCollectedJSContext::Get()->mUncaughtRejections
.append(aPromise
)) {
218 FlushRejections::DispatchNeeded();
223 void PromiseDebugging::AddConsumedRejection(JS::HandleObject aPromise
) {
224 // If the promise is in our list of uncaught rejections, we haven't yet
225 // reported it as unhandled. In that case, just remove it from the list
226 // and don't add it to the list of consumed rejections.
227 auto& uncaughtRejections
=
228 CycleCollectedJSContext::Get()->mUncaughtRejections
;
229 for (size_t i
= 0; i
< uncaughtRejections
.length(); i
++) {
230 if (uncaughtRejections
[i
] == aPromise
) {
231 // To avoid large amounts of memmoves, we don't shrink the vector here.
232 // Instead, we filter out nullptrs when iterating over the vector later.
233 uncaughtRejections
[i
].set(nullptr);
237 // This might OOM, but won't set a pending exception, so we'll just ignore it.
238 if (CycleCollectedJSContext::Get()->mConsumedRejections
.append(aPromise
)) {
239 FlushRejections::DispatchNeeded();
244 void PromiseDebugging::FlushUncaughtRejectionsInternal() {
245 CycleCollectedJSContext
* storage
= CycleCollectedJSContext::Get();
247 auto& uncaught
= storage
->mUncaughtRejections
;
248 auto& consumed
= storage
->mConsumedRejections
;
252 JSContext
* cx
= jsapi
.cx();
254 // Notify observers of uncaught Promise.
255 auto& observers
= storage
->mUncaughtRejectionObservers
;
257 for (size_t i
= 0; i
< uncaught
.length(); i
++) {
258 JS::RootedObject
promise(cx
, uncaught
[i
]);
259 // Filter out nullptrs which might've been added by
260 // PromiseDebugging::AddConsumedRejection.
265 bool suppressReporting
= false;
266 for (size_t j
= 0; j
< observers
.Length(); ++j
) {
267 RefPtr
<UncaughtRejectionObserver
> obs
=
268 static_cast<UncaughtRejectionObserver
*>(observers
[j
].get());
270 if (obs
->OnLeftUncaught(promise
, IgnoreErrors())) {
271 suppressReporting
= true;
275 if (!suppressReporting
) {
276 JSAutoRealm
ar(cx
, promise
);
277 Promise::ReportRejectedPromise(cx
, promise
);
280 storage
->mUncaughtRejections
.clear();
282 // Notify observers of consumed Promise.
284 for (size_t i
= 0; i
< consumed
.length(); i
++) {
285 JS::RootedObject
promise(cx
, consumed
[i
]);
287 for (size_t j
= 0; j
< observers
.Length(); ++j
) {
288 RefPtr
<UncaughtRejectionObserver
> obs
=
289 static_cast<UncaughtRejectionObserver
*>(observers
[j
].get());
291 obs
->OnConsumed(promise
, IgnoreErrors());
294 storage
->mConsumedRejections
.clear();
298 } // namespace mozilla