PromiseDebugging.cpp (10011B)
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/. */ 6 7 #include "mozilla/dom/PromiseDebugging.h" 8 9 #include "js/Value.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" 15 #include "mozilla/dom/BindingDeclarations.h" 16 #include "mozilla/dom/ContentChild.h" 17 #include "mozilla/dom/Promise.h" 18 #include "mozilla/dom/PromiseBinding.h" 19 #include "mozilla/dom/PromiseDebuggingBinding.h" 20 #include "nsThreadUtils.h" 21 22 namespace mozilla::dom { 23 24 class FlushRejections : public DiscardableRunnable { 25 public: 26 FlushRejections() : DiscardableRunnable("dom::FlushRejections") {} 27 28 static void Init() { 29 if (!sDispatched.init()) { 30 MOZ_CRASH("Could not initialize FlushRejections::sDispatched"); 31 } 32 sDispatched.set(false); 33 } 34 35 static void DispatchNeeded() { 36 if (sDispatched.get()) { 37 // An instance of `FlushRejections` has already been dispatched 38 // and not run yet. No need to dispatch another one. 39 return; 40 } 41 sDispatched.set(true); 42 43 // Dispatch the runnable to the current thread where 44 // the Promise was rejected, e.g. workers or worklets. 45 NS_DispatchToCurrentThread(new FlushRejections()); 46 } 47 48 static void FlushSync() { 49 sDispatched.set(false); 50 51 // Call the callbacks if necessary. 52 // Note that these callbacks may in turn cause Promise to turn 53 // uncaught or consumed. Since `sDispatched` is `false`, 54 // `FlushRejections` will be called once again, on an ulterior 55 // tick. 56 PromiseDebugging::FlushUncaughtRejectionsInternal(); 57 } 58 59 NS_IMETHOD Run() override { 60 FlushSync(); 61 return NS_OK; 62 } 63 64 private: 65 // `true` if an instance of `FlushRejections` is currently dispatched 66 // and has not been executed yet. 67 static MOZ_THREAD_LOCAL(bool) sDispatched; 68 }; 69 70 /* static */ MOZ_THREAD_LOCAL(bool) FlushRejections::sDispatched; 71 72 /* static */ 73 void PromiseDebugging::GetState(GlobalObject& aGlobal, 74 JS::Handle<JSObject*> aPromise, 75 PromiseDebuggingStateHolder& aState, 76 ErrorResult& aRv) { 77 JSContext* cx = aGlobal.Context(); 78 // CheckedUnwrapStatic is fine, since we're looking for promises only. 79 JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise)); 80 if (!obj || !JS::IsPromiseObject(obj)) { 81 aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(); 82 return; 83 } 84 switch (JS::GetPromiseState(obj)) { 85 case JS::PromiseState::Pending: 86 aState.mState = PromiseDebuggingState::Pending; 87 break; 88 case JS::PromiseState::Fulfilled: 89 aState.mState = PromiseDebuggingState::Fulfilled; 90 aState.mValue = JS::GetPromiseResult(obj); 91 break; 92 case JS::PromiseState::Rejected: 93 aState.mState = PromiseDebuggingState::Rejected; 94 aState.mReason = JS::GetPromiseResult(obj); 95 break; 96 } 97 } 98 99 /* static */ 100 void PromiseDebugging::GetPromiseID(GlobalObject& aGlobal, 101 JS::Handle<JSObject*> aPromise, 102 nsString& aID, ErrorResult& aRv) { 103 JSContext* cx = aGlobal.Context(); 104 // CheckedUnwrapStatic is fine, since we're looking for promises only. 105 JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise)); 106 if (!obj || !JS::IsPromiseObject(obj)) { 107 aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(); 108 return; 109 } 110 uint64_t promiseID = JS::GetPromiseID(obj); 111 aID = sIDPrefix; 112 aID.AppendInt(promiseID); 113 } 114 115 /* static */ 116 void PromiseDebugging::GetAllocationStack(GlobalObject& aGlobal, 117 JS::Handle<JSObject*> aPromise, 118 JS::MutableHandle<JSObject*> aStack, 119 ErrorResult& aRv) { 120 JSContext* cx = aGlobal.Context(); 121 // CheckedUnwrapStatic is fine, since we're looking for promises only. 122 JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise)); 123 if (!obj || !JS::IsPromiseObject(obj)) { 124 aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(); 125 return; 126 } 127 aStack.set(JS::GetPromiseAllocationSite(obj)); 128 } 129 130 /* static */ 131 void PromiseDebugging::GetRejectionStack(GlobalObject& aGlobal, 132 JS::Handle<JSObject*> aPromise, 133 JS::MutableHandle<JSObject*> aStack, 134 ErrorResult& aRv) { 135 JSContext* cx = aGlobal.Context(); 136 // CheckedUnwrapStatic is fine, since we're looking for promises only. 137 JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise)); 138 if (!obj || !JS::IsPromiseObject(obj)) { 139 aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(); 140 return; 141 } 142 aStack.set(JS::GetPromiseResolutionSite(obj)); 143 } 144 145 /* static */ 146 void PromiseDebugging::GetFullfillmentStack(GlobalObject& aGlobal, 147 JS::Handle<JSObject*> aPromise, 148 JS::MutableHandle<JSObject*> aStack, 149 ErrorResult& aRv) { 150 JSContext* cx = aGlobal.Context(); 151 // CheckedUnwrapStatic is fine, since we're looking for promises only. 152 JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aPromise)); 153 if (!obj || !JS::IsPromiseObject(obj)) { 154 aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(); 155 return; 156 } 157 aStack.set(JS::GetPromiseResolutionSite(obj)); 158 } 159 160 /*static */ 161 MOZ_RUNINIT nsString PromiseDebugging::sIDPrefix; 162 163 /* static */ 164 void PromiseDebugging::Init() { 165 FlushRejections::Init(); 166 167 // Generate a prefix for identifiers: "PromiseDebugging.$processid." 168 sIDPrefix = u"PromiseDebugging."_ns; 169 if (XRE_IsContentProcess()) { 170 sIDPrefix.AppendInt(ContentChild::GetSingleton()->GetID()); 171 sIDPrefix.Append('.'); 172 } else { 173 sIDPrefix.AppendLiteral("0."); 174 } 175 } 176 177 /* static */ 178 void PromiseDebugging::Shutdown() { sIDPrefix.SetIsVoid(true); } 179 180 /* static */ 181 void PromiseDebugging::FlushUncaughtRejections() { 182 MOZ_ASSERT(!NS_IsMainThread()); 183 FlushRejections::FlushSync(); 184 } 185 186 /* static */ 187 void PromiseDebugging::AddUncaughtRejectionObserver( 188 GlobalObject&, UncaughtRejectionObserver& aObserver) { 189 CycleCollectedJSContext* storage = CycleCollectedJSContext::Get(); 190 nsTArray<nsCOMPtr<nsISupports>>& observers = 191 storage->mUncaughtRejectionObservers; 192 observers.AppendElement(&aObserver); 193 } 194 195 /* static */ 196 bool PromiseDebugging::RemoveUncaughtRejectionObserver( 197 GlobalObject&, UncaughtRejectionObserver& aObserver) { 198 CycleCollectedJSContext* storage = CycleCollectedJSContext::Get(); 199 nsTArray<nsCOMPtr<nsISupports>>& observers = 200 storage->mUncaughtRejectionObservers; 201 for (size_t i = 0; i < observers.Length(); ++i) { 202 UncaughtRejectionObserver* observer = 203 static_cast<UncaughtRejectionObserver*>(observers[i].get()); 204 if (*observer == aObserver) { 205 observers.RemoveElementAt(i); 206 return true; 207 } 208 } 209 return false; 210 } 211 212 /* static */ 213 void PromiseDebugging::AddUncaughtRejection(JS::Handle<JSObject*> aPromise) { 214 // This might OOM, but won't set a pending exception, so we'll just ignore it. 215 if (CycleCollectedJSContext::Get()->mUncaughtRejections.append(aPromise)) { 216 FlushRejections::DispatchNeeded(); 217 } 218 } 219 220 /* void */ 221 void PromiseDebugging::AddConsumedRejection(JS::Handle<JSObject*> aPromise) { 222 // If the promise is in our list of uncaught rejections, we haven't yet 223 // reported it as unhandled. In that case, just remove it from the list 224 // and don't add it to the list of consumed rejections. 225 auto& uncaughtRejections = 226 CycleCollectedJSContext::Get()->mUncaughtRejections; 227 for (size_t i = 0; i < uncaughtRejections.length(); i++) { 228 if (uncaughtRejections[i] == aPromise) { 229 // To avoid large amounts of memmoves, we don't shrink the vector here. 230 // Instead, we filter out nullptrs when iterating over the vector later. 231 uncaughtRejections[i].set(nullptr); 232 return; 233 } 234 } 235 // This might OOM, but won't set a pending exception, so we'll just ignore it. 236 if (CycleCollectedJSContext::Get()->mConsumedRejections.append(aPromise)) { 237 FlushRejections::DispatchNeeded(); 238 } 239 } 240 241 /* static */ 242 void PromiseDebugging::FlushUncaughtRejectionsInternal() { 243 CycleCollectedJSContext* storage = CycleCollectedJSContext::Get(); 244 245 auto& uncaught = storage->mUncaughtRejections; 246 auto& consumed = storage->mConsumedRejections; 247 248 AutoJSAPI jsapi; 249 jsapi.Init(); 250 JSContext* cx = jsapi.cx(); 251 252 // Notify observers of uncaught Promise. 253 auto& observers = storage->mUncaughtRejectionObservers; 254 255 for (size_t i = 0; i < uncaught.length(); i++) { 256 JS::Rooted<JSObject*> promise(cx, uncaught[i]); 257 // Filter out nullptrs which might've been added by 258 // PromiseDebugging::AddConsumedRejection. 259 if (!promise) { 260 continue; 261 } 262 263 bool suppressReporting = false; 264 for (size_t j = 0; j < observers.Length(); ++j) { 265 RefPtr<UncaughtRejectionObserver> obs = 266 static_cast<UncaughtRejectionObserver*>(observers[j].get()); 267 268 if (obs->OnLeftUncaught(promise, IgnoreErrors())) { 269 suppressReporting = true; 270 } 271 } 272 273 if (!suppressReporting) { 274 JSAutoRealm ar(cx, promise); 275 Promise::ReportRejectedPromise(cx, promise); 276 } 277 } 278 storage->mUncaughtRejections.clear(); 279 280 // Notify observers of consumed Promise. 281 282 for (size_t i = 0; i < consumed.length(); i++) { 283 JS::Rooted<JSObject*> promise(cx, consumed[i]); 284 285 for (size_t j = 0; j < observers.Length(); ++j) { 286 RefPtr<UncaughtRejectionObserver> obs = 287 static_cast<UncaughtRejectionObserver*>(observers[j].get()); 288 289 obs->OnConsumed(promise, IgnoreErrors()); 290 } 291 } 292 storage->mConsumedRejections.clear(); 293 } 294 295 } // namespace mozilla::dom