From 2077cdb41e3ef814cbbef482774d8bbec464fb1c Mon Sep 17 00:00:00 2001 From: Gaming4JC Date: Wed, 22 Jan 2020 21:07:19 -0500 Subject: Bug 1406922 - Make CycleCollectedJSContext to handle microtasks and make MutationObserver to use them Tag UXP Issue #1344 --- dom/base/nsDOMMutationObserver.cpp | 67 ++++++++++++++----------------- dom/base/nsDOMMutationObserver.h | 23 +++++++++-- dom/base/test/test_mutationobservers.html | 33 ++++++++++++++- xpcom/base/CycleCollectedJSContext.cpp | 67 +++++++++++++++++++++++++++++-- xpcom/base/CycleCollectedJSContext.h | 22 +++++++++- 5 files changed, 164 insertions(+), 48 deletions(-) diff --git a/dom/base/nsDOMMutationObserver.cpp b/dom/base/nsDOMMutationObserver.cpp index 858a30ce55..4c4731c116 100644 --- a/dom/base/nsDOMMutationObserver.cpp +++ b/dom/base/nsDOMMutationObserver.cpp @@ -32,8 +32,6 @@ using mozilla::dom::Element; AutoTArray, 4>* nsDOMMutationObserver::sScheduledMutationObservers = nullptr; -nsDOMMutationObserver* nsDOMMutationObserver::sCurrentObserver = nullptr; - uint32_t nsDOMMutationObserver::sMutationLevel = 0; uint64_t nsDOMMutationObserver::sCount = 0; @@ -597,10 +595,32 @@ nsDOMMutationObserver::ScheduleForRun() RescheduleForRun(); } +class MutationObserverMicroTask final : public MicroTaskRunnable +{ +public: + virtual void Run(AutoSlowOperation& aAso) override + { + nsDOMMutationObserver::HandleMutations(aAso); + } + + virtual bool Suppressed() override + { + return nsDOMMutationObserver::AllScheduledMutationObserversAreSuppressed(); + } +}; + void nsDOMMutationObserver::RescheduleForRun() { if (!sScheduledMutationObservers) { + CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get(); + if (!ccjs) { + return; + } + + RefPtr momt = + new MutationObserverMicroTask(); + ccjs->DispatchMicroTaskRunnable(momt.forget()); sScheduledMutationObservers = new AutoTArray, 4>; } @@ -862,36 +882,9 @@ nsDOMMutationObserver::HandleMutation() mCallback->Call(this, mutations, *this); } -class AsyncMutationHandler : public mozilla::Runnable -{ -public: - NS_IMETHOD Run() override - { - nsDOMMutationObserver::HandleMutations(); - return NS_OK; - } -}; - void -nsDOMMutationObserver::HandleMutationsInternal() +nsDOMMutationObserver::HandleMutationsInternal(AutoSlowOperation& aAso) { - if (!nsContentUtils::IsSafeToRunScript()) { - nsContentUtils::AddScriptRunner(new AsyncMutationHandler()); - return; - } - static RefPtr sCurrentObserver; - if (sCurrentObserver && !sCurrentObserver->Suppressed()) { - // In normal cases sScheduledMutationObservers will be handled - // after previous mutations are handled. But in case some - // callback calls a sync API, which spins the eventloop, we need to still - // process other mutations happening during that sync call. - // This does *not* catch all cases, but should work for stuff running - // in separate tabs. - return; - } - - mozilla::AutoSlowOperation aso; - nsTArray >* suppressedObservers = nullptr; while (sScheduledMutationObservers) { @@ -899,20 +892,21 @@ nsDOMMutationObserver::HandleMutationsInternal() sScheduledMutationObservers; sScheduledMutationObservers = nullptr; for (uint32_t i = 0; i < observers->Length(); ++i) { - sCurrentObserver = static_cast((*observers)[i]); - if (!sCurrentObserver->Suppressed()) { - sCurrentObserver->HandleMutation(); + RefPtr currentObserver = + static_cast((*observers)[i]); + if (!currentObserver->Suppressed()) { + currentObserver->HandleMutation(); } else { if (!suppressedObservers) { suppressedObservers = new nsTArray >; } - if (!suppressedObservers->Contains(sCurrentObserver)) { - suppressedObservers->AppendElement(sCurrentObserver); + if (!suppressedObservers->Contains(currentObserver)) { + suppressedObservers->AppendElement(currentObserver); } } } delete observers; - aso.CheckForInterrupt(); + aAso.CheckForInterrupt(); } if (suppressedObservers) { @@ -923,7 +917,6 @@ nsDOMMutationObserver::HandleMutationsInternal() delete suppressedObservers; suppressedObservers = nullptr; } - sCurrentObserver = nullptr; } nsDOMMutationRecord* diff --git a/dom/base/nsDOMMutationObserver.h b/dom/base/nsDOMMutationObserver.h index cde32c57b9..a8babc603a 100644 --- a/dom/base/nsDOMMutationObserver.h +++ b/dom/base/nsDOMMutationObserver.h @@ -552,13 +552,29 @@ public: } // static methods - static void HandleMutations() + static void HandleMutations(mozilla::AutoSlowOperation& aAso) { if (sScheduledMutationObservers) { - HandleMutationsInternal(); + HandleMutationsInternal(aAso); } } + static bool AllScheduledMutationObserversAreSuppressed() + { + if (sScheduledMutationObservers) { + uint32_t len = sScheduledMutationObservers->Length(); + if (len > 0) { + for (uint32_t i = 0; i < len; ++i) { + if (!(*sScheduledMutationObservers)[i]->Suppressed()) { + return false; + } + } + return true; + } + } + return false; + } + static void EnterMutationHandling(); static void LeaveMutationHandling(); @@ -594,7 +610,7 @@ protected: return false; } - static void HandleMutationsInternal(); + static void HandleMutationsInternal(mozilla::AutoSlowOperation& aAso); static void AddCurrentlyHandlingObserver(nsDOMMutationObserver* aObserver, uint32_t aMutationLevel); @@ -622,7 +638,6 @@ protected: static uint64_t sCount; static AutoTArray, 4>* sScheduledMutationObservers; - static nsDOMMutationObserver* sCurrentObserver; static uint32_t sMutationLevel; static AutoTArray, 4>, 4>* diff --git a/dom/base/test/test_mutationobservers.html b/dom/base/test/test_mutationobservers.html index a6de89595b..7e4c994235 100644 --- a/dom/base/test/test_mutationobservers.html +++ b/dom/base/test/test_mutationobservers.html @@ -362,7 +362,7 @@ function testChildList5() { is(records[5].previousSibling, c3, ""); is(records[5].nextSibling, c5, ""); observer.disconnect(); - then(testAdoptNode); + then(testNestedMutations); m = null; }); m.observe(div, { childList: true, subtree: true }); @@ -375,6 +375,37 @@ function testChildList5() { div.appendChild(emptyDF); // empty document shouldn't cause mutation records } +function testNestedMutations() { + div.textContent = null; + div.appendChild(document.createTextNode("foo")); + var m2WasCalled = false; + m = new M(function(records, observer) { + is(records[0].type, "characterData", "Should have got characterData"); + observer.disconnect(); + m = null; + m3 = new M(function(records, observer) { + ok(m2WasCalled, "m2 should have been called before m3!"); + is(records[0].type, "characterData", "Should have got characterData"); + observer.disconnect(); + then(testAdoptNode); + m3 = null; + }); + m3.observe(div, { characterData: true, subtree: true}); + div.firstChild.data = "foo"; + }); + m2 = new M(function(records, observer) { + m2WasCalled = true; + is(records[0].type, "characterData", "Should have got characterData"); + observer.disconnect(); + m2 = null; + }); + m2.observe(div, { characterData: true, subtree: true}); + div.appendChild(document.createTextNode("foo")); + m.observe(div, { characterData: true, subtree: true }); + + div.firstChild.data = "bar"; +} + function testAdoptNode() { var d1 = document.implementation.createHTMLDocument(null); var d2 = document.implementation.createHTMLDocument(null); diff --git a/xpcom/base/CycleCollectedJSContext.cpp b/xpcom/base/CycleCollectedJSContext.cpp index 0a85ae6ac6..033ca562a5 100644 --- a/xpcom/base/CycleCollectedJSContext.cpp +++ b/xpcom/base/CycleCollectedJSContext.cpp @@ -440,6 +440,7 @@ CycleCollectedJSContext::CycleCollectedJSContext() , mDoingStableStates(false) , mDisableMicroTaskCheckpoint(false) , mMicroTaskLevel(0) + , mMicroTaskRecursionDepth(0) , mOutOfMemoryState(OOMState::OK) , mLargeAllocationFailureState(OOMState::OK) { @@ -1380,8 +1381,8 @@ CycleCollectedJSContext::AfterProcessTask(uint32_t aRecursionDepth) // Step 4.1: Execute microtasks. if (!mDisableMicroTaskCheckpoint) { + PerformMicroTaskCheckPoint(); if (NS_IsMainThread()) { - PerformMainThreadMicroTaskCheckpoint(); Promise::PerformMicroTaskCheckpoint(); } else { Promise::PerformWorkerMicroTaskCheckpoint(); @@ -1661,12 +1662,70 @@ CycleCollectedJSContext::DispatchToMicroTask(already_AddRefed aRunn mPromiseMicroTaskQueue.push(runnable.forget()); } +class AsyncMutationHandler final : public mozilla::Runnable +{ +public: + NS_IMETHOD Run() override + { + CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get(); + if (ccjs) { + ccjs->PerformMicroTaskCheckPoint(); + } + return NS_OK; + } +}; + void -CycleCollectedJSContext::PerformMainThreadMicroTaskCheckpoint() +CycleCollectedJSContext::PerformMicroTaskCheckPoint() { - MOZ_ASSERT(NS_IsMainThread()); + if (mPendingMicroTaskRunnables.empty()) { + // Nothing to do, return early. + return; + } + + uint32_t currentDepth = RecursionDepth(); + if (mMicroTaskRecursionDepth >= currentDepth) { + // We are already executing microtasks for the current recursion depth. + return; + } - nsDOMMutationObserver::HandleMutations(); + if (NS_IsMainThread() && !nsContentUtils::IsSafeToRunScript()) { + // Special case for main thread where DOM mutations may happen when + // it is not safe to run scripts. + nsContentUtils::AddScriptRunner(new AsyncMutationHandler()); + return; + } + + mozilla::AutoRestore restore(mMicroTaskRecursionDepth); + MOZ_ASSERT(currentDepth > 0); + mMicroTaskRecursionDepth = currentDepth; + + AutoSlowOperation aso; + + std::queue> suppressed; + while (!mPendingMicroTaskRunnables.empty()) { + RefPtr runnable = + mPendingMicroTaskRunnables.front().forget(); + mPendingMicroTaskRunnables.pop(); + if (runnable->Suppressed()) { + suppressed.push(runnable); + } else { + runnable->Run(aso); + } + } + + // Put back the suppressed microtasks so that they will be run later. + // Note, it is possible that we end up keeping these suppressed tasks around + // for some time, but no longer than spinning the event loop nestedly + // (sync XHR, alert, etc.) + mPendingMicroTaskRunnables.swap(suppressed); +} + +void +CycleCollectedJSContext::DispatchMicroTaskRunnable( + already_AddRefed aRunnable) +{ + mPendingMicroTaskRunnables.push(aRunnable); } void diff --git a/xpcom/base/CycleCollectedJSContext.h b/xpcom/base/CycleCollectedJSContext.h index 2197eae920..4cd1479ed7 100644 --- a/xpcom/base/CycleCollectedJSContext.h +++ b/xpcom/base/CycleCollectedJSContext.h @@ -33,6 +33,7 @@ struct Class; } // namespace js namespace mozilla { +class AutoSlowOperation; class JSGCThingParticipant: public nsCycleCollectionParticipant { @@ -134,6 +135,17 @@ struct CycleCollectorResults uint32_t mNumSlices; }; +class MicroTaskRunnable +{ +public: + MicroTaskRunnable() {} + NS_INLINE_DECL_REFCOUNTING(MicroTaskRunnable) + virtual void Run(AutoSlowOperation& aAso) = 0; + virtual bool Suppressed() { return false; } +protected: + virtual ~MicroTaskRunnable() {} +}; + class CycleCollectedJSContext { friend class JSGCThingParticipant; @@ -412,7 +424,7 @@ public: void LeaveMicroTask() { if (--mMicroTaskLevel == 0) { - PerformMainThreadMicroTaskCheckpoint(); + PerformMicroTaskCheckPoint(); } } @@ -431,7 +443,9 @@ public: mMicroTaskLevel = aLevel; } - void PerformMainThreadMicroTaskCheckpoint(); + void PerformMicroTaskCheckPoint(); + + void DispatchMicroTaskRunnable(already_AddRefed aRunnable); // Storage for watching rejected promises waiting for some client to // consume their rejection. @@ -484,6 +498,10 @@ private: bool mDisableMicroTaskCheckpoint; uint32_t mMicroTaskLevel; + std::queue> mPendingMicroTaskRunnables; + + uint32_t mMicroTaskRecursionDepth; + OOMState mOutOfMemoryState; OOMState mLargeAllocationFailureState; -- cgit v1.2.3