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 "mozilla/CycleCollectedJSContext.h"
8
9 #include <algorithm>
10 #include <utility>
11
12 #include "js/Debug.h"
13 #include "js/GCAPI.h"
14 #include "js/Utility.h"
15 #include "jsapi.h"
16 #include "mozilla/ArrayUtils.h"
17 #include "mozilla/AsyncEventDispatcher.h"
18 #include "mozilla/AutoRestore.h"
19 #include "mozilla/CycleCollectedJSRuntime.h"
20 #include "mozilla/DebuggerOnGCRunnable.h"
21 #include "mozilla/MemoryReporting.h"
22 #include "mozilla/ProfilerMarkers.h"
23 #include "mozilla/Sprintf.h"
24 #include "mozilla/Telemetry.h"
25 #include "mozilla/TimelineConsumers.h"
26 #include "mozilla/TimelineMarker.h"
27 #include "mozilla/Unused.h"
28 #include "mozilla/dom/DOMException.h"
29 #include "mozilla/dom/DOMJSClass.h"
30 #include "mozilla/dom/FinalizationRegistryBinding.h"
31 #include "mozilla/dom/ProfileTimelineMarkerBinding.h"
32 #include "mozilla/dom/PromiseBinding.h"
33 #include "mozilla/dom/PromiseDebugging.h"
34 #include "mozilla/dom/PromiseRejectionEvent.h"
35 #include "mozilla/dom/PromiseRejectionEventBinding.h"
36 #include "mozilla/dom/RootedDictionary.h"
37 #include "mozilla/dom/ScriptSettings.h"
38 #include "mozilla/dom/UserActivation.h"
39 #include "nsContentUtils.h"
40 #include "nsCycleCollectionNoteRootCallback.h"
41 #include "nsCycleCollectionParticipant.h"
42 #include "nsCycleCollector.h"
43 #include "nsDOMJSUtils.h"
44 #include "nsDOMMutationObserver.h"
45 #include "nsJSUtils.h"
46 #include "nsPIDOMWindow.h"
47 #include "nsStringBuffer.h"
48 #include "nsThread.h"
49 #include "nsThreadUtils.h"
50 #include "nsWrapperCache.h"
51 #include "xpcpublic.h"
52
53 using namespace mozilla;
54 using namespace mozilla::dom;
55
56 namespace mozilla {
57
CycleCollectedJSContext()58 CycleCollectedJSContext::CycleCollectedJSContext()
59 : mRuntime(nullptr),
60 mJSContext(nullptr),
61 mDoingStableStates(false),
62 mTargetedMicroTaskRecursionDepth(0),
63 mMicroTaskLevel(0),
64 mSuppressionGeneration(0),
65 mDebuggerRecursionDepth(0),
66 mMicroTaskRecursionDepth(0),
67 mFinalizationRegistryCleanup(this) {
68 MOZ_COUNT_CTOR(CycleCollectedJSContext);
69
70 nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
71 mOwningThread = thread.forget().downcast<nsThread>().take();
72 MOZ_RELEASE_ASSERT(mOwningThread);
73 }
74
~CycleCollectedJSContext()75 CycleCollectedJSContext::~CycleCollectedJSContext() {
76 MOZ_COUNT_DTOR(CycleCollectedJSContext);
77 // If the allocation failed, here we are.
78 if (!mJSContext) {
79 return;
80 }
81
82 JS::SetHostCleanupFinalizationRegistryCallback(mJSContext, nullptr, nullptr);
83
84 JS_SetContextPrivate(mJSContext, nullptr);
85
86 mRuntime->SetContext(nullptr);
87 mRuntime->Shutdown(mJSContext);
88
89 // Last chance to process any events.
90 CleanupIDBTransactions(mBaseRecursionDepth);
91 MOZ_ASSERT(mPendingIDBTransactions.IsEmpty());
92
93 ProcessStableStateQueue();
94 MOZ_ASSERT(mStableStateEvents.IsEmpty());
95
96 // Clear mPendingException first, since it might be cycle collected.
97 mPendingException = nullptr;
98
99 MOZ_ASSERT(mDebuggerMicroTaskQueue.empty());
100 MOZ_ASSERT(mPendingMicroTaskRunnables.empty());
101
102 mUncaughtRejections.reset();
103 mConsumedRejections.reset();
104
105 mAboutToBeNotifiedRejectedPromises.Clear();
106 mPendingUnhandledRejections.Clear();
107
108 mFinalizationRegistryCleanup.Destroy();
109
110 JS_DestroyContext(mJSContext);
111 mJSContext = nullptr;
112
113 nsCycleCollector_forgetJSContext();
114
115 mozilla::dom::DestroyScriptSettings();
116
117 mOwningThread->SetScriptObserver(nullptr);
118 NS_RELEASE(mOwningThread);
119
120 delete mRuntime;
121 mRuntime = nullptr;
122 }
123
Initialize(JSRuntime * aParentRuntime,uint32_t aMaxBytes)124 nsresult CycleCollectedJSContext::Initialize(JSRuntime* aParentRuntime,
125 uint32_t aMaxBytes) {
126 MOZ_ASSERT(!mJSContext);
127
128 mozilla::dom::InitScriptSettings();
129 mJSContext = JS_NewContext(aMaxBytes, aParentRuntime);
130 if (!mJSContext) {
131 return NS_ERROR_OUT_OF_MEMORY;
132 }
133
134 mRuntime = CreateRuntime(mJSContext);
135 mRuntime->SetContext(this);
136
137 mOwningThread->SetScriptObserver(this);
138 // The main thread has a base recursion depth of 0, workers of 1.
139 mBaseRecursionDepth = RecursionDepth();
140
141 NS_GetCurrentThread()->SetCanInvokeJS(true);
142
143 JS::SetJobQueue(mJSContext, this);
144 JS::SetPromiseRejectionTrackerCallback(mJSContext,
145 PromiseRejectionTrackerCallback, this);
146 mUncaughtRejections.init(mJSContext,
147 JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>(
148 js::SystemAllocPolicy()));
149 mConsumedRejections.init(mJSContext,
150 JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>(
151 js::SystemAllocPolicy()));
152
153 mFinalizationRegistryCleanup.Init();
154
155 // Cast to PerThreadAtomCache for dom::GetAtomCache(JSContext*).
156 JS_SetContextPrivate(mJSContext, static_cast<PerThreadAtomCache*>(this));
157
158 nsCycleCollector_registerJSContext(this);
159
160 return NS_OK;
161 }
162
163 /* static */
GetFor(JSContext * aCx)164 CycleCollectedJSContext* CycleCollectedJSContext::GetFor(JSContext* aCx) {
165 // Cast from void* matching JS_SetContextPrivate.
166 auto atomCache = static_cast<PerThreadAtomCache*>(JS_GetContextPrivate(aCx));
167 // Down cast.
168 return static_cast<CycleCollectedJSContext*>(atomCache);
169 }
170
SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const171 size_t CycleCollectedJSContext::SizeOfExcludingThis(
172 MallocSizeOf aMallocSizeOf) const {
173 return 0;
174 }
175
176 class PromiseJobRunnable final : public MicroTaskRunnable {
177 public:
PromiseJobRunnable(JS::HandleObject aPromise,JS::HandleObject aCallback,JS::HandleObject aCallbackGlobal,JS::HandleObject aAllocationSite,nsIGlobalObject * aIncumbentGlobal)178 PromiseJobRunnable(JS::HandleObject aPromise, JS::HandleObject aCallback,
179 JS::HandleObject aCallbackGlobal,
180 JS::HandleObject aAllocationSite,
181 nsIGlobalObject* aIncumbentGlobal)
182 : mCallback(new PromiseJobCallback(aCallback, aCallbackGlobal,
183 aAllocationSite, aIncumbentGlobal)),
184 mPropagateUserInputEventHandling(false) {
185 MOZ_ASSERT(js::IsFunctionObject(aCallback));
186
187 if (aPromise) {
188 JS::PromiseUserInputEventHandlingState state =
189 JS::GetPromiseUserInputEventHandlingState(aPromise);
190 mPropagateUserInputEventHandling =
191 state ==
192 JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation;
193 }
194 }
195
196 virtual ~PromiseJobRunnable() = default;
197
198 protected:
199 MOZ_CAN_RUN_SCRIPT
Run(AutoSlowOperation & aAso)200 virtual void Run(AutoSlowOperation& aAso) override {
201 JSObject* callback = mCallback->CallbackPreserveColor();
202 nsIGlobalObject* global = callback ? xpc::NativeGlobal(callback) : nullptr;
203 if (global && !global->IsDying()) {
204 // Propagate the user input event handling bit if needed.
205 nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global);
206 RefPtr<Document> doc;
207 if (win) {
208 doc = win->GetExtantDoc();
209 }
210 AutoHandlingUserInputStatePusher userInpStatePusher(
211 mPropagateUserInputEventHandling);
212
213 mCallback->Call("promise callback");
214 aAso.CheckForInterrupt();
215 }
216 // Now that mCallback is no longer needed, clear any pointers it contains to
217 // JS GC things. This removes any storebuffer entries associated with those
218 // pointers, which can cause problems by taking up memory and by triggering
219 // minor GCs. This otherwise would not happen until the next minor GC or
220 // cycle collection.
221 mCallback->Reset();
222 }
223
Suppressed()224 virtual bool Suppressed() override {
225 JSObject* callback = mCallback->CallbackPreserveColor();
226 nsIGlobalObject* global = callback ? xpc::NativeGlobal(callback) : nullptr;
227 return global && global->IsInSyncOperation();
228 }
229
230 private:
231 const RefPtr<PromiseJobCallback> mCallback;
232 bool mPropagateUserInputEventHandling;
233 };
234
getIncumbentGlobal(JSContext * aCx)235 JSObject* CycleCollectedJSContext::getIncumbentGlobal(JSContext* aCx) {
236 nsIGlobalObject* global = mozilla::dom::GetIncumbentGlobal();
237 if (global) {
238 return global->GetGlobalJSObject();
239 }
240 return nullptr;
241 }
242
enqueuePromiseJob(JSContext * aCx,JS::HandleObject aPromise,JS::HandleObject aJob,JS::HandleObject aAllocationSite,JS::HandleObject aIncumbentGlobal)243 bool CycleCollectedJSContext::enqueuePromiseJob(
244 JSContext* aCx, JS::HandleObject aPromise, JS::HandleObject aJob,
245 JS::HandleObject aAllocationSite, JS::HandleObject aIncumbentGlobal) {
246 MOZ_ASSERT(aCx == Context());
247 MOZ_ASSERT(Get() == this);
248
249 nsIGlobalObject* global = nullptr;
250 if (aIncumbentGlobal) {
251 global = xpc::NativeGlobal(aIncumbentGlobal);
252 }
253 JS::RootedObject jobGlobal(aCx, JS::CurrentGlobalOrNull(aCx));
254 RefPtr<PromiseJobRunnable> runnable = new PromiseJobRunnable(
255 aPromise, aJob, jobGlobal, aAllocationSite, global);
256 DispatchToMicroTask(runnable.forget());
257 return true;
258 }
259
260 // Used only by the SpiderMonkey Debugger API, and even then only via
261 // JS::AutoDebuggerJobQueueInterruption, to ensure that the debuggee's queue is
262 // not affected; see comments in js/public/Promise.h.
runJobs(JSContext * aCx)263 void CycleCollectedJSContext::runJobs(JSContext* aCx) {
264 MOZ_ASSERT(aCx == Context());
265 MOZ_ASSERT(Get() == this);
266 PerformMicroTaskCheckPoint();
267 }
268
empty() const269 bool CycleCollectedJSContext::empty() const {
270 // This is our override of JS::JobQueue::empty. Since that interface is only
271 // concerned with the ordinary microtask queue, not the debugger microtask
272 // queue, we only report on the former.
273 return mPendingMicroTaskRunnables.empty();
274 }
275
276 // Preserve a debuggee's microtask queue while it is interrupted by the
277 // debugger. See the comments for JS::AutoDebuggerJobQueueInterruption.
278 class CycleCollectedJSContext::SavedMicroTaskQueue
279 : public JS::JobQueue::SavedJobQueue {
280 public:
SavedMicroTaskQueue(CycleCollectedJSContext * ccjs)281 explicit SavedMicroTaskQueue(CycleCollectedJSContext* ccjs) : ccjs(ccjs) {
282 ccjs->mDebuggerRecursionDepth++;
283 ccjs->mPendingMicroTaskRunnables.swap(mQueue);
284 }
285
~SavedMicroTaskQueue()286 ~SavedMicroTaskQueue() {
287 MOZ_RELEASE_ASSERT(ccjs->mPendingMicroTaskRunnables.empty());
288 MOZ_RELEASE_ASSERT(ccjs->mDebuggerRecursionDepth);
289 ccjs->mDebuggerRecursionDepth--;
290 ccjs->mPendingMicroTaskRunnables.swap(mQueue);
291 }
292
293 private:
294 CycleCollectedJSContext* ccjs;
295 std::deque<RefPtr<MicroTaskRunnable>> mQueue;
296 };
297
298 js::UniquePtr<JS::JobQueue::SavedJobQueue>
saveJobQueue(JSContext * cx)299 CycleCollectedJSContext::saveJobQueue(JSContext* cx) {
300 auto saved = js::MakeUnique<SavedMicroTaskQueue>(this);
301 if (!saved) {
302 // When MakeUnique's allocation fails, the SavedMicroTaskQueue constructor
303 // is never called, so mPendingMicroTaskRunnables is still initialized.
304 JS_ReportOutOfMemory(cx);
305 return nullptr;
306 }
307
308 return saved;
309 }
310
311 /* static */
PromiseRejectionTrackerCallback(JSContext * aCx,bool aMutedErrors,JS::HandleObject aPromise,JS::PromiseRejectionHandlingState state,void * aData)312 void CycleCollectedJSContext::PromiseRejectionTrackerCallback(
313 JSContext* aCx, bool aMutedErrors, JS::HandleObject aPromise,
314 JS::PromiseRejectionHandlingState state, void* aData) {
315 CycleCollectedJSContext* self = static_cast<CycleCollectedJSContext*>(aData);
316
317 MOZ_ASSERT(aCx == self->Context());
318 MOZ_ASSERT(Get() == self);
319
320 // TODO: Bug 1549351 - Promise rejection event should not be sent for
321 // cross-origin scripts
322
323 PromiseArray& aboutToBeNotified = self->mAboutToBeNotifiedRejectedPromises;
324 PromiseHashtable& unhandled = self->mPendingUnhandledRejections;
325 uint64_t promiseID = JS::GetPromiseID(aPromise);
326
327 if (state == JS::PromiseRejectionHandlingState::Unhandled) {
328 PromiseDebugging::AddUncaughtRejection(aPromise);
329 if (!aMutedErrors) {
330 RefPtr<Promise> promise =
331 Promise::CreateFromExisting(xpc::NativeGlobal(aPromise), aPromise);
332 aboutToBeNotified.AppendElement(promise);
333 unhandled.InsertOrUpdate(promiseID, std::move(promise));
334 }
335 } else {
336 PromiseDebugging::AddConsumedRejection(aPromise);
337 for (size_t i = 0; i < aboutToBeNotified.Length(); i++) {
338 if (aboutToBeNotified[i] &&
339 aboutToBeNotified[i]->PromiseObj() == aPromise) {
340 // To avoid large amounts of memmoves, we don't shrink the vector
341 // here. Instead, we filter out nullptrs when iterating over the
342 // vector later.
343 aboutToBeNotified[i] = nullptr;
344 DebugOnly<bool> isFound = unhandled.Remove(promiseID);
345 MOZ_ASSERT(isFound);
346 return;
347 }
348 }
349 RefPtr<Promise> promise;
350 unhandled.Remove(promiseID, getter_AddRefs(promise));
351 if (!promise && !aMutedErrors) {
352 nsIGlobalObject* global = xpc::NativeGlobal(aPromise);
353 if (nsCOMPtr<EventTarget> owner = do_QueryInterface(global)) {
354 RootedDictionary<PromiseRejectionEventInit> init(aCx);
355 init.mPromise = Promise::CreateFromExisting(global, aPromise);
356 init.mReason = JS::GetPromiseResult(aPromise);
357
358 RefPtr<PromiseRejectionEvent> event =
359 PromiseRejectionEvent::Constructor(owner, u"rejectionhandled"_ns,
360 init);
361
362 RefPtr<AsyncEventDispatcher> asyncDispatcher =
363 new AsyncEventDispatcher(owner, event);
364 asyncDispatcher->PostDOMEvent();
365 }
366 }
367 }
368 }
369
GetPendingException() const370 already_AddRefed<Exception> CycleCollectedJSContext::GetPendingException()
371 const {
372 MOZ_ASSERT(mJSContext);
373
374 nsCOMPtr<Exception> out = mPendingException;
375 return out.forget();
376 }
377
SetPendingException(Exception * aException)378 void CycleCollectedJSContext::SetPendingException(Exception* aException) {
379 MOZ_ASSERT(mJSContext);
380 mPendingException = aException;
381 }
382
383 std::deque<RefPtr<MicroTaskRunnable>>&
GetMicroTaskQueue()384 CycleCollectedJSContext::GetMicroTaskQueue() {
385 MOZ_ASSERT(mJSContext);
386 return mPendingMicroTaskRunnables;
387 }
388
389 std::deque<RefPtr<MicroTaskRunnable>>&
GetDebuggerMicroTaskQueue()390 CycleCollectedJSContext::GetDebuggerMicroTaskQueue() {
391 MOZ_ASSERT(mJSContext);
392 return mDebuggerMicroTaskQueue;
393 }
394
ProcessStableStateQueue()395 void CycleCollectedJSContext::ProcessStableStateQueue() {
396 MOZ_ASSERT(mJSContext);
397 MOZ_RELEASE_ASSERT(!mDoingStableStates);
398 mDoingStableStates = true;
399
400 // When run, one event can add another event to the mStableStateEvents, as
401 // such you can't use iterators here.
402 for (uint32_t i = 0; i < mStableStateEvents.Length(); ++i) {
403 nsCOMPtr<nsIRunnable> event = std::move(mStableStateEvents[i]);
404 event->Run();
405 }
406
407 mStableStateEvents.Clear();
408 mDoingStableStates = false;
409 }
410
CleanupIDBTransactions(uint32_t aRecursionDepth)411 void CycleCollectedJSContext::CleanupIDBTransactions(uint32_t aRecursionDepth) {
412 MOZ_ASSERT(mJSContext);
413 MOZ_RELEASE_ASSERT(!mDoingStableStates);
414 mDoingStableStates = true;
415
416 nsTArray<PendingIDBTransactionData> localQueue =
417 std::move(mPendingIDBTransactions);
418
419 localQueue.RemoveLastElements(
420 localQueue.end() -
421 std::remove_if(localQueue.begin(), localQueue.end(),
422 [aRecursionDepth](PendingIDBTransactionData& data) {
423 if (data.mRecursionDepth != aRecursionDepth) {
424 return false;
425 }
426
427 {
428 nsCOMPtr<nsIRunnable> transaction =
429 std::move(data.mTransaction);
430 transaction->Run();
431 }
432
433 return true;
434 }));
435
436 // If mPendingIDBTransactions has events in it now, they were added from
437 // something we called, so they belong at the end of the queue.
438 localQueue.AppendElements(std::move(mPendingIDBTransactions));
439 mPendingIDBTransactions = std::move(localQueue);
440 mDoingStableStates = false;
441 }
442
BeforeProcessTask(bool aMightBlock)443 void CycleCollectedJSContext::BeforeProcessTask(bool aMightBlock) {
444 // If ProcessNextEvent was called during a microtask callback, we
445 // must process any pending microtasks before blocking in the event loop,
446 // otherwise we may deadlock until an event enters the queue later.
447 if (aMightBlock && PerformMicroTaskCheckPoint()) {
448 // If any microtask was processed, we post a dummy event in order to
449 // force the ProcessNextEvent call not to block. This is required
450 // to support nested event loops implemented using a pattern like
451 // "while (condition) thread.processNextEvent(true)", in case the
452 // condition is triggered here by a Promise "then" callback.
453 NS_DispatchToMainThread(new Runnable("BeforeProcessTask"));
454 }
455 }
456
AfterProcessTask(uint32_t aRecursionDepth)457 void CycleCollectedJSContext::AfterProcessTask(uint32_t aRecursionDepth) {
458 MOZ_ASSERT(mJSContext);
459
460 // See HTML 6.1.4.2 Processing model
461
462 // Step 4.1: Execute microtasks.
463 PerformMicroTaskCheckPoint();
464
465 // Step 4.2 Execute any events that were waiting for a stable state.
466 ProcessStableStateQueue();
467
468 // This should be a fast test so that it won't affect the next task
469 // processing.
470 IsIdleGCTaskNeeded();
471 }
472
AfterProcessMicrotasks()473 void CycleCollectedJSContext::AfterProcessMicrotasks() {
474 MOZ_ASSERT(mJSContext);
475 // Notify unhandled promise rejections:
476 // https://html.spec.whatwg.org/multipage/webappapis.html#notify-about-rejected-promises
477 if (mAboutToBeNotifiedRejectedPromises.Length()) {
478 RefPtr<NotifyUnhandledRejections> runnable = new NotifyUnhandledRejections(
479 this, std::move(mAboutToBeNotifiedRejectedPromises));
480 NS_DispatchToCurrentThread(runnable);
481 }
482 // Cleanup Indexed Database transactions:
483 // https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
484 CleanupIDBTransactions(RecursionDepth());
485
486 // Clear kept alive objects in JS WeakRef.
487 // https://whatpr.org/html/4571/webappapis.html#perform-a-microtask-checkpoint
488 //
489 // ECMAScript implementations are expected to call ClearKeptObjects when a
490 // synchronous sequence of ECMAScript execution completes.
491 //
492 // https://tc39.es/proposal-weakrefs/#sec-clear-kept-objects
493 JS::ClearKeptObjects(mJSContext);
494 }
495
IsIdleGCTaskNeeded() const496 void CycleCollectedJSContext::IsIdleGCTaskNeeded() const {
497 class IdleTimeGCTaskRunnable : public mozilla::IdleRunnable {
498 public:
499 using mozilla::IdleRunnable::IdleRunnable;
500
501 public:
502 NS_IMETHOD Run() override {
503 CycleCollectedJSRuntime* ccrt = CycleCollectedJSRuntime::Get();
504 if (ccrt) {
505 ccrt->RunIdleTimeGCTask();
506 }
507 return NS_OK;
508 }
509 };
510
511 if (Runtime()->IsIdleGCTaskNeeded()) {
512 nsCOMPtr<nsIRunnable> gc_task = new IdleTimeGCTaskRunnable();
513 NS_DispatchToCurrentThreadQueue(gc_task.forget(), EventQueuePriority::Idle);
514 Runtime()->SetPendingIdleGCTask();
515 }
516 }
517
RecursionDepth() const518 uint32_t CycleCollectedJSContext::RecursionDepth() const {
519 // Debugger interruptions are included in the recursion depth so that debugger
520 // microtask checkpoints do not run IDB transactions which were initiated
521 // before the interruption.
522 return mOwningThread->RecursionDepth() + mDebuggerRecursionDepth;
523 }
524
RunInStableState(already_AddRefed<nsIRunnable> && aRunnable)525 void CycleCollectedJSContext::RunInStableState(
526 already_AddRefed<nsIRunnable>&& aRunnable) {
527 MOZ_ASSERT(mJSContext);
528 mStableStateEvents.AppendElement(std::move(aRunnable));
529 }
530
AddPendingIDBTransaction(already_AddRefed<nsIRunnable> && aTransaction)531 void CycleCollectedJSContext::AddPendingIDBTransaction(
532 already_AddRefed<nsIRunnable>&& aTransaction) {
533 MOZ_ASSERT(mJSContext);
534
535 PendingIDBTransactionData data;
536 data.mTransaction = aTransaction;
537
538 MOZ_ASSERT(mOwningThread);
539 data.mRecursionDepth = RecursionDepth();
540
541 // There must be an event running to get here.
542 #ifndef MOZ_WIDGET_COCOA
543 MOZ_ASSERT(data.mRecursionDepth > mBaseRecursionDepth);
544 #else
545 // XXX bug 1261143
546 // Recursion depth should be greater than mBaseRecursionDepth,
547 // or the runnable will stay in the queue forever.
548 if (data.mRecursionDepth <= mBaseRecursionDepth) {
549 data.mRecursionDepth = mBaseRecursionDepth + 1;
550 }
551 #endif
552
553 mPendingIDBTransactions.AppendElement(std::move(data));
554 }
555
DispatchToMicroTask(already_AddRefed<MicroTaskRunnable> aRunnable)556 void CycleCollectedJSContext::DispatchToMicroTask(
557 already_AddRefed<MicroTaskRunnable> aRunnable) {
558 RefPtr<MicroTaskRunnable> runnable(aRunnable);
559
560 MOZ_ASSERT(NS_IsMainThread());
561 MOZ_ASSERT(runnable);
562
563 JS::JobQueueMayNotBeEmpty(Context());
564
565 LogMicroTaskRunnable::LogDispatch(runnable.get());
566 mPendingMicroTaskRunnables.push_back(std::move(runnable));
567 }
568
569 class AsyncMutationHandler final : public mozilla::Runnable {
570 public:
AsyncMutationHandler()571 AsyncMutationHandler() : mozilla::Runnable("AsyncMutationHandler") {}
572
573 // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
574 // bug 1535398.
575 MOZ_CAN_RUN_SCRIPT_BOUNDARY
Run()576 NS_IMETHOD Run() override {
577 CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
578 if (ccjs) {
579 ccjs->PerformMicroTaskCheckPoint();
580 }
581 return NS_OK;
582 }
583 };
584
SuppressedMicroTasks(CycleCollectedJSContext * aContext)585 SuppressedMicroTasks::SuppressedMicroTasks(CycleCollectedJSContext* aContext)
586 : mContext(aContext),
587 mSuppressionGeneration(aContext->mSuppressionGeneration) {}
588
Suppressed()589 bool SuppressedMicroTasks::Suppressed() {
590 if (mSuppressionGeneration == mContext->mSuppressionGeneration) {
591 return true;
592 }
593
594 for (std::deque<RefPtr<MicroTaskRunnable>>::reverse_iterator it =
595 mSuppressedMicroTaskRunnables.rbegin();
596 it != mSuppressedMicroTaskRunnables.rend(); ++it) {
597 mContext->GetMicroTaskQueue().push_front(*it);
598 }
599 mContext->mSuppressedMicroTasks = nullptr;
600
601 return false;
602 }
603
PerformMicroTaskCheckPoint(bool aForce)604 bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) {
605 if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
606 AfterProcessMicrotasks();
607 // Nothing to do, return early.
608 return false;
609 }
610
611 uint32_t currentDepth = RecursionDepth();
612 if (mMicroTaskRecursionDepth >= currentDepth && !aForce) {
613 // We are already executing microtasks for the current recursion depth.
614 return false;
615 }
616
617 if (mTargetedMicroTaskRecursionDepth != 0 &&
618 mTargetedMicroTaskRecursionDepth + mDebuggerRecursionDepth !=
619 currentDepth) {
620 return false;
621 }
622
623 if (NS_IsMainThread() && !nsContentUtils::IsSafeToRunScript()) {
624 // Special case for main thread where DOM mutations may happen when
625 // it is not safe to run scripts.
626 nsContentUtils::AddScriptRunner(new AsyncMutationHandler());
627 return false;
628 }
629
630 mozilla::AutoRestore<uint32_t> restore(mMicroTaskRecursionDepth);
631 MOZ_ASSERT(aForce ? currentDepth == 0 : currentDepth > 0);
632 mMicroTaskRecursionDepth = currentDepth;
633
634 AUTO_PROFILER_TRACING_MARKER("JS", "Perform microtasks", JS);
635
636 bool didProcess = false;
637 AutoSlowOperation aso;
638
639 for (;;) {
640 RefPtr<MicroTaskRunnable> runnable;
641 if (!mDebuggerMicroTaskQueue.empty()) {
642 runnable = std::move(mDebuggerMicroTaskQueue.front());
643 mDebuggerMicroTaskQueue.pop_front();
644 } else if (!mPendingMicroTaskRunnables.empty()) {
645 runnable = std::move(mPendingMicroTaskRunnables.front());
646 mPendingMicroTaskRunnables.pop_front();
647 } else {
648 break;
649 }
650
651 if (runnable->Suppressed()) {
652 // Microtasks in worker shall never be suppressed.
653 // Otherwise, mPendingMicroTaskRunnables will be replaced later with
654 // all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly.
655 MOZ_ASSERT(NS_IsMainThread());
656 JS::JobQueueMayNotBeEmpty(Context());
657 if (runnable != mSuppressedMicroTasks) {
658 if (!mSuppressedMicroTasks) {
659 mSuppressedMicroTasks = new SuppressedMicroTasks(this);
660 }
661 mSuppressedMicroTasks->mSuppressedMicroTaskRunnables.push_back(
662 runnable);
663 }
664 } else {
665 if (mPendingMicroTaskRunnables.empty() &&
666 mDebuggerMicroTaskQueue.empty() && !mSuppressedMicroTasks) {
667 JS::JobQueueIsEmpty(Context());
668 }
669 didProcess = true;
670
671 LogMicroTaskRunnable::Run log(runnable.get());
672 runnable->Run(aso);
673 runnable = nullptr;
674 }
675 }
676
677 // Put back the suppressed microtasks so that they will be run later.
678 // Note, it is possible that we end up keeping these suppressed tasks around
679 // for some time, but no longer than spinning the event loop nestedly
680 // (sync XHR, alert, etc.)
681 if (mSuppressedMicroTasks) {
682 mPendingMicroTaskRunnables.push_back(mSuppressedMicroTasks);
683 }
684
685 AfterProcessMicrotasks();
686
687 return didProcess;
688 }
689
PerformDebuggerMicroTaskCheckpoint()690 void CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint() {
691 // Don't do normal microtask handling checks here, since whoever is calling
692 // this method is supposed to know what they are doing.
693
694 AutoSlowOperation aso;
695 for (;;) {
696 // For a debugger microtask checkpoint, we always use the debugger microtask
697 // queue.
698 std::deque<RefPtr<MicroTaskRunnable>>* microtaskQueue =
699 &GetDebuggerMicroTaskQueue();
700
701 if (microtaskQueue->empty()) {
702 break;
703 }
704
705 RefPtr<MicroTaskRunnable> runnable = std::move(microtaskQueue->front());
706 MOZ_ASSERT(runnable);
707
708 LogMicroTaskRunnable::Run log(runnable.get());
709
710 // This function can re-enter, so we remove the element before calling.
711 microtaskQueue->pop_front();
712
713 if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
714 JS::JobQueueIsEmpty(Context());
715 }
716 runnable->Run(aso);
717 runnable = nullptr;
718 }
719
720 AfterProcessMicrotasks();
721 }
722
Run()723 NS_IMETHODIMP CycleCollectedJSContext::NotifyUnhandledRejections::Run() {
724 for (size_t i = 0; i < mUnhandledRejections.Length(); ++i) {
725 RefPtr<Promise>& promise = mUnhandledRejections[i];
726 if (!promise) {
727 continue;
728 }
729
730 JS::RootingContext* cx = mCx->RootingCx();
731 JS::RootedObject promiseObj(cx, promise->PromiseObj());
732 MOZ_ASSERT(JS::IsPromiseObject(promiseObj));
733
734 // Only fire unhandledrejection if the promise is still not handled;
735 uint64_t promiseID = JS::GetPromiseID(promiseObj);
736 if (!JS::GetPromiseIsHandled(promiseObj)) {
737 if (nsCOMPtr<EventTarget> target =
738 do_QueryInterface(promise->GetParentObject())) {
739 RootedDictionary<PromiseRejectionEventInit> init(cx);
740 init.mPromise = promise;
741 init.mReason = JS::GetPromiseResult(promiseObj);
742 init.mCancelable = true;
743
744 RefPtr<PromiseRejectionEvent> event =
745 PromiseRejectionEvent::Constructor(target, u"unhandledrejection"_ns,
746 init);
747 // We don't use the result of dispatching event here to check whether to
748 // report the Promise to console.
749 target->DispatchEvent(*event);
750 }
751 }
752
753 if (!JS::GetPromiseIsHandled(promiseObj)) {
754 DebugOnly<bool> isFound =
755 mCx->mPendingUnhandledRejections.Remove(promiseID);
756 MOZ_ASSERT(isFound);
757 }
758
759 // If a rejected promise is being handled in "unhandledrejection" event
760 // handler, it should be removed from the table in
761 // PromiseRejectionTrackerCallback.
762 MOZ_ASSERT(!mCx->mPendingUnhandledRejections.Lookup(promiseID));
763 }
764 return NS_OK;
765 }
766
Cancel()767 nsresult CycleCollectedJSContext::NotifyUnhandledRejections::Cancel() {
768 for (size_t i = 0; i < mUnhandledRejections.Length(); ++i) {
769 RefPtr<Promise>& promise = mUnhandledRejections[i];
770 if (!promise) {
771 continue;
772 }
773
774 JS::RootedObject promiseObj(mCx->RootingCx(), promise->PromiseObj());
775 mCx->mPendingUnhandledRejections.Remove(JS::GetPromiseID(promiseObj));
776 }
777 return NS_OK;
778 }
779
780 class FinalizationRegistryCleanup::CleanupRunnable
781 : public DiscardableRunnable {
782 public:
CleanupRunnable(FinalizationRegistryCleanup * aCleanupWork)783 explicit CleanupRunnable(FinalizationRegistryCleanup* aCleanupWork)
784 : DiscardableRunnable("CleanupRunnable"), mCleanupWork(aCleanupWork) {}
785
786 // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
787 // bug 1535398.
788 MOZ_CAN_RUN_SCRIPT_BOUNDARY
Run()789 NS_IMETHOD Run() override {
790 mCleanupWork->DoCleanup();
791 return NS_OK;
792 }
793
794 private:
795 FinalizationRegistryCleanup* mCleanupWork;
796 };
797
FinalizationRegistryCleanup(CycleCollectedJSContext * aContext)798 FinalizationRegistryCleanup::FinalizationRegistryCleanup(
799 CycleCollectedJSContext* aContext)
800 : mContext(aContext) {}
801
Destroy()802 void FinalizationRegistryCleanup::Destroy() {
803 // This must happen before the CycleCollectedJSContext destructor calls
804 // JS_DestroyContext().
805 mCallbacks.reset();
806 }
807
Init()808 void FinalizationRegistryCleanup::Init() {
809 JSContext* cx = mContext->Context();
810 mCallbacks.init(cx);
811 JS::SetHostCleanupFinalizationRegistryCallback(cx, QueueCallback, this);
812 }
813
814 /* static */
QueueCallback(JSFunction * aDoCleanup,JSObject * aIncumbentGlobal,void * aData)815 void FinalizationRegistryCleanup::QueueCallback(JSFunction* aDoCleanup,
816 JSObject* aIncumbentGlobal,
817 void* aData) {
818 FinalizationRegistryCleanup* cleanup =
819 static_cast<FinalizationRegistryCleanup*>(aData);
820 cleanup->QueueCallback(aDoCleanup, aIncumbentGlobal);
821 }
822
QueueCallback(JSFunction * aDoCleanup,JSObject * aIncumbentGlobal)823 void FinalizationRegistryCleanup::QueueCallback(JSFunction* aDoCleanup,
824 JSObject* aIncumbentGlobal) {
825 bool firstCallback = mCallbacks.empty();
826
827 MOZ_ALWAYS_TRUE(mCallbacks.append(Callback{aDoCleanup, aIncumbentGlobal}));
828
829 if (firstCallback) {
830 RefPtr<CleanupRunnable> cleanup = new CleanupRunnable(this);
831 NS_DispatchToCurrentThread(cleanup.forget());
832 }
833 }
834
DoCleanup()835 void FinalizationRegistryCleanup::DoCleanup() {
836 if (mCallbacks.empty()) {
837 return;
838 }
839
840 JS::RootingContext* cx = mContext->RootingCx();
841
842 JS::Rooted<CallbackVector> callbacks(cx);
843 std::swap(callbacks.get(), mCallbacks.get());
844
845 for (const Callback& callback : callbacks) {
846 JS::RootedObject functionObj(
847 cx, JS_GetFunctionObject(callback.mCallbackFunction));
848 JS::RootedObject globalObj(cx, JS::GetNonCCWObjectGlobal(functionObj));
849
850 nsIGlobalObject* incumbentGlobal =
851 xpc::NativeGlobal(callback.mIncumbentGlobal);
852 if (!incumbentGlobal) {
853 continue;
854 }
855
856 RefPtr<FinalizationRegistryCleanupCallback> cleanupCallback(
857 new FinalizationRegistryCleanupCallback(functionObj, globalObj, nullptr,
858 incumbentGlobal));
859
860 nsIGlobalObject* global =
861 xpc::NativeGlobal(cleanupCallback->CallbackPreserveColor());
862 if (global) {
863 cleanupCallback->Call("FinalizationRegistryCleanup::DoCleanup");
864 }
865 }
866 }
867
trace(JSTracer * trc)868 void FinalizationRegistryCleanup::Callback::trace(JSTracer* trc) {
869 JS::UnsafeTraceRoot(trc, &mCallbackFunction, "mCallbackFunction");
870 JS::UnsafeTraceRoot(trc, &mIncumbentGlobal, "mIncumbentGlobal");
871 }
872
873 } // namespace mozilla
874