Commit 1eb13643 authored by Olli Pettay's avatar Olli Pettay
Browse files

Bug 1724777, optimize suppressed MicroTask handling, r=mccr8 a=RyanVM

The test is in theory racy, but trying to limit the cases when it might behave badly
by running it on opt desktop builds only. Without the patch the 'period' check takes over 400ms locally and with the
patch 1-3ms.

The changes are just trying to optimize execution, not change the behavior.
Use of SuppressedMicroTasks is perhaps a bit odd, but it helps keeping
SavedMicroTaskQueue and similar code simple.

Differential Revision: https://phabricator.services.mozilla.com/D122290
parent 8db19746
......@@ -15624,6 +15624,18 @@ nsAutoSyncOperation::~nsAutoSyncOperation() {
}
}
 
void Document::SetIsInSyncOperation(bool aSync) {
if (CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get()) {
ccjs->UpdateMicroTaskSuppressionGeneration();
}
if (aSync) {
++mInSyncOperationCount;
} else {
--mInSyncOperationCount;
}
}
gfxUserFontSet* Document::GetUserFontSet() {
if (!mFontFaceSet) {
return nullptr;
......
......@@ -3214,13 +3214,7 @@ class Document : public nsINode,
bool IsInSyncOperation() { return mInSyncOperationCount != 0; }
void SetIsInSyncOperation(bool aSync) {
if (aSync) {
++mInSyncOperationCount;
} else {
--mInSyncOperationCount;
}
}
void SetIsInSyncOperation(bool aSync);
bool CreatingStaticClone() const { return mCreatingStaticClone; }
......
......@@ -769,6 +769,8 @@ skip-if = debug == false
[test_shared_compartment2.html]
[test_structuredclone_backref.html]
[test_style_cssText.html]
[test_suppressed_microtasks.html]
skip-if = debug || asan || verify || toolkit == 'android' # The test needs to run reasonably fast.
[test_text_wholeText.html]
[test_textnode_normalize_in_selection.html]
[test_textnode_split_in_selection.html]
......
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test microtask suppression</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
<script>
SimpleTest.waitForExplicitFinish();
var previousTask = -1;
function test() {
let win = window.open("about:blank");
win.onload = function() {
win.onmessage = function() {
win.start = win.performance.now();
win.onmessage = function() {
let period = win.performance.now() - win.start;
win.opener.ok(
period < 200,
"Running a task should be fast. Took " + period + "ms.");
win.onmessage = null;
}
win.postMessage("measurementMessage", "*");
}
win.postMessage("initialMessage", "*");
const last = 500000;
for (let i = 0; i < last + 1; ++i) {
window.queueMicrotask(function() {
// Check that once microtasks are unsuppressed, they are handled in
// the correct order.
if (previousTask != i - 1) {
// Explicitly optimize out cases which pass.
ok(false, "Microtasks should be handled in order.");
}
previousTask = i;
if (i == last) {
win.close();
SimpleTest.finish();
}
});
}
// Synchronous XMLHttpRequest suppresses microtasks.
var xhr = new XMLHttpRequest();
xhr.open("GET", "slow.sjs", false);
xhr.send();
}
}
</script>
</head>
<body onload="test()">
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
</body>
</html>
......@@ -931,7 +931,7 @@ class WorkerJSContext final : public mozilla::CycleCollectedJSContext {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(runnable);
std::queue<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr;
std::deque<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr;
JSContext* cx = Context();
NS_ASSERTION(cx, "This should never be null!");
......@@ -953,7 +953,7 @@ class WorkerJSContext final : public mozilla::CycleCollectedJSContext {
}
JS::JobQueueMayNotBeEmpty(cx);
microTaskQueue->push(std::move(runnable));
microTaskQueue->push_back(std::move(runnable));
}
bool IsSystemCaller() const override {
......
......@@ -4313,7 +4313,7 @@ void WorkerPrivate::EnterDebuggerEventLoop() {
{
MutexAutoLock lock(mMutex);
std::queue<RefPtr<MicroTaskRunnable>>& debuggerMtQueue =
std::deque<RefPtr<MicroTaskRunnable>>& debuggerMtQueue =
ccjscx->GetDebuggerMicroTaskQueue();
while (mControlQueue.IsEmpty() &&
!(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) &&
......
......@@ -159,7 +159,7 @@ class WorkletJSContext final : public CycleCollectedJSContext {
#endif
JS::JobQueueMayNotBeEmpty(cx);
GetMicroTaskQueue().push(std::move(runnable));
GetMicroTaskQueue().push_back(std::move(runnable));
}
bool IsSystemCaller() const override {
......
......@@ -61,6 +61,7 @@ CycleCollectedJSContext::CycleCollectedJSContext()
mDoingStableStates(false),
mTargetedMicroTaskRecursionDepth(0),
mMicroTaskLevel(0),
mSuppressionGeneration(0),
mDebuggerRecursionDepth(0),
mMicroTaskRecursionDepth(0),
mFinalizationRegistryCleanup(this) {
......@@ -291,7 +292,7 @@ class CycleCollectedJSContext::SavedMicroTaskQueue
private:
CycleCollectedJSContext* ccjs;
std::queue<RefPtr<MicroTaskRunnable>> mQueue;
std::deque<RefPtr<MicroTaskRunnable>> mQueue;
};
js::UniquePtr<JS::JobQueue::SavedJobQueue>
......@@ -379,13 +380,13 @@ void CycleCollectedJSContext::SetPendingException(Exception* aException) {
mPendingException = aException;
}
std::queue<RefPtr<MicroTaskRunnable>>&
std::deque<RefPtr<MicroTaskRunnable>>&
CycleCollectedJSContext::GetMicroTaskQueue() {
MOZ_ASSERT(mJSContext);
return mPendingMicroTaskRunnables;
}
std::queue<RefPtr<MicroTaskRunnable>>&
std::deque<RefPtr<MicroTaskRunnable>>&
CycleCollectedJSContext::GetDebuggerMicroTaskQueue() {
MOZ_ASSERT(mJSContext);
return mDebuggerMicroTaskQueue;
......@@ -562,7 +563,7 @@ void CycleCollectedJSContext::DispatchToMicroTask(
JS::JobQueueMayNotBeEmpty(Context());
LogMicroTaskRunnable::LogDispatch(runnable.get());
mPendingMicroTaskRunnables.push(std::move(runnable));
mPendingMicroTaskRunnables.push_back(std::move(runnable));
}
class AsyncMutationHandler final : public mozilla::Runnable {
......@@ -581,6 +582,25 @@ class AsyncMutationHandler final : public mozilla::Runnable {
}
};
SuppressedMicroTasks::SuppressedMicroTasks(CycleCollectedJSContext* aContext)
: mContext(aContext),
mSuppressionGeneration(aContext->mSuppressionGeneration) {}
bool SuppressedMicroTasks::Suppressed() {
if (mSuppressionGeneration == mContext->mSuppressionGeneration) {
return true;
}
for (std::deque<RefPtr<MicroTaskRunnable>>::reverse_iterator it =
mSuppressedMicroTaskRunnables.rbegin();
it != mSuppressedMicroTaskRunnables.rend(); ++it) {
mContext->GetMicroTaskQueue().push_front(*it);
}
mContext->mSuppressedMicroTasks = nullptr;
return false;
}
bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) {
if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
AfterProcessMicrotasks();
......@@ -616,15 +636,14 @@ bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) {
bool didProcess = false;
AutoSlowOperation aso;
std::queue<RefPtr<MicroTaskRunnable>> suppressed;
for (;;) {
RefPtr<MicroTaskRunnable> runnable;
if (!mDebuggerMicroTaskQueue.empty()) {
runnable = std::move(mDebuggerMicroTaskQueue.front());
mDebuggerMicroTaskQueue.pop();
mDebuggerMicroTaskQueue.pop_front();
} else if (!mPendingMicroTaskRunnables.empty()) {
runnable = std::move(mPendingMicroTaskRunnables.front());
mPendingMicroTaskRunnables.pop();
mPendingMicroTaskRunnables.pop_front();
} else {
break;
}
......@@ -635,10 +654,16 @@ bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) {
// all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly.
MOZ_ASSERT(NS_IsMainThread());
JS::JobQueueMayNotBeEmpty(Context());
suppressed.push(runnable);
if (runnable != mSuppressedMicroTasks) {
if (!mSuppressedMicroTasks) {
mSuppressedMicroTasks = new SuppressedMicroTasks(this);
}
mSuppressedMicroTasks->mSuppressedMicroTaskRunnables.push_back(
runnable);
}
} else {
if (mPendingMicroTaskRunnables.empty() &&
mDebuggerMicroTaskQueue.empty() && suppressed.empty()) {
mDebuggerMicroTaskQueue.empty() && !mSuppressedMicroTasks) {
JS::JobQueueIsEmpty(Context());
}
didProcess = true;
......@@ -653,7 +678,9 @@ bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) {
// 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);
if (mSuppressedMicroTasks) {
mPendingMicroTaskRunnables.push_back(mSuppressedMicroTasks);
}
AfterProcessMicrotasks();
......@@ -668,7 +695,7 @@ void CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint() {
for (;;) {
// For a debugger microtask checkpoint, we always use the debugger microtask
// queue.
std::queue<RefPtr<MicroTaskRunnable>>* microtaskQueue =
std::deque<RefPtr<MicroTaskRunnable>>* microtaskQueue =
&GetDebuggerMicroTaskQueue();
if (microtaskQueue->empty()) {
......@@ -681,7 +708,7 @@ void CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint() {
LogMicroTaskRunnable::Run log(runnable.get());
// This function can re-enter, so we remove the element before calling.
microtaskQueue->pop();
microtaskQueue->pop_front();
if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
JS::JobQueueIsEmpty(Context());
......
......@@ -7,7 +7,7 @@
#ifndef mozilla_CycleCollectedJSContext_h
#define mozilla_CycleCollectedJSContext_h
#include <queue>
#include <deque>
#include "mozilla/Attributes.h"
#include "mozilla/MemoryReporting.h"
......@@ -81,6 +81,20 @@ class MicroTaskRunnable {
virtual ~MicroTaskRunnable() = default;
};
// Store the suppressed mictotasks in another microtask so that operations
// for the microtask queue as a whole keep working.
class SuppressedMicroTasks : public MicroTaskRunnable {
public:
explicit SuppressedMicroTasks(CycleCollectedJSContext* aContext);
MOZ_CAN_RUN_SCRIPT_BOUNDARY void Run(AutoSlowOperation& aAso) final {}
virtual bool Suppressed();
CycleCollectedJSContext* mContext;
uint64_t mSuppressionGeneration;
std::deque<RefPtr<MicroTaskRunnable>> mSuppressedMicroTaskRunnables;
};
// Support for JS FinalizationRegistry objects, which allow a JS callback to be
// registered that is called when objects die.
//
......@@ -117,6 +131,7 @@ class FinalizationRegistryCleanup {
class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue {
friend class CycleCollectedJSRuntime;
friend class SuppressedMicroTasks;
protected:
CycleCollectedJSContext();
......@@ -166,8 +181,8 @@ class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue {
already_AddRefed<dom::Exception> GetPendingException() const;
void SetPendingException(dom::Exception* aException);
std::queue<RefPtr<MicroTaskRunnable>>& GetMicroTaskQueue();
std::queue<RefPtr<MicroTaskRunnable>>& GetDebuggerMicroTaskQueue();
std::deque<RefPtr<MicroTaskRunnable>>& GetMicroTaskQueue();
std::deque<RefPtr<MicroTaskRunnable>>& GetDebuggerMicroTaskQueue();
JSContext* Context() const {
MOZ_ASSERT(mJSContext);
......@@ -183,6 +198,8 @@ class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue {
mTargetedMicroTaskRecursionDepth = aDepth;
}
void UpdateMicroTaskSuppressionGeneration() { ++mSuppressionGeneration; }
protected:
JSContext* MaybeContext() const { return mJSContext; }
......@@ -316,8 +333,10 @@ class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue {
uint32_t mMicroTaskLevel;
std::queue<RefPtr<MicroTaskRunnable>> mPendingMicroTaskRunnables;
std::queue<RefPtr<MicroTaskRunnable>> mDebuggerMicroTaskQueue;
std::deque<RefPtr<MicroTaskRunnable>> mPendingMicroTaskRunnables;
std::deque<RefPtr<MicroTaskRunnable>> mDebuggerMicroTaskQueue;
RefPtr<SuppressedMicroTasks> mSuppressedMicroTasks;
uint64_t mSuppressionGeneration;
// How many times the debugger has interrupted execution, possibly creating
// microtask checkpoints in places that they would not normally occur.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment