Bug 1920115 - Allow high priority tasks to run before timers r=smaug

In Gecko, unlike other runnables where the TaskController
picks one each at a time, the TimeoutManager allows multiple
timers to run sequentially without allowing other runnables
to run in between.

This patch makes the user_blocking and user_visible tasks
dispatched by Scheduler API can run before timers.

Differential Revision: https://phabricator.services.mozilla.com/D237753
This commit is contained in:
Sean Feng
2025-03-31 19:49:03 +00:00
parent d01f340fd6
commit bbc06f4b40
13 changed files with 193 additions and 52 deletions

View File

@@ -24,6 +24,7 @@
#include "mozilla/net/WebSocketEventService.h"
#include "mozilla/MediaManager.h"
#include "mozilla/dom/WorkerScope.h"
#include "mozilla/dom/WebTaskScheduler.h"
using namespace mozilla;
using namespace mozilla::dom;
@@ -976,8 +977,12 @@ void TimeoutManager::RunTimeout(const TimeStamp& aNow,
}
// Check to see if we have run out of time to execute timeout handlers.
// If we've exceeded our time budget then terminate the loop immediately.
//
// Or if there are high priority tasks dispatched by the Scheduler API,
// they should run first before timers.
TimeDuration elapsed = now - start;
if (elapsed >= totalTimeLimit) {
if (elapsed >= totalTimeLimit ||
mGlobalObject.HasScheduledNormalOrHighPriorityWebTasks()) {
// We ran out of time. Make sure to schedule the executor to
// run immediately for the next timer, if it exists. Its possible,
// however, that the last timeout handler suspended the window. If

View File

@@ -2907,6 +2907,10 @@ bool nsPIDOMWindowInner::IsCurrentInnerWindow() const {
return outer && outer->GetCurrentInnerWindow() == this;
}
bool nsGlobalWindowInner::HasScheduledNormalOrHighPriorityWebTasks() const {
return gNumNormalOrHighPriorityQueuesHaveTaskScheduledMainThread > 0;
}
bool nsPIDOMWindowInner::IsFullyActive() const {
WindowContext* wc = GetWindowContext();
if (!wc || wc->IsDiscarded() || !wc->IsCurrent()) {

View File

@@ -338,6 +338,7 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
virtual bool HasActiveIndexedDBDatabases() override;
virtual bool HasActivePeerConnections() override;
virtual bool HasOpenWebSockets() const override;
virtual bool HasScheduledNormalOrHighPriorityWebTasks() const override;
void SyncStateFromParentWindow();
// Called on the current inner window of a browsing context when its

View File

@@ -347,6 +347,10 @@ class nsIGlobalObject : public nsISupports {
virtual bool IsXPCSandbox() { return false; }
virtual bool HasScheduledNormalOrHighPriorityWebTasks() const {
return false;
}
/**
* Report a localized error message to the error console. Currently this
* amounts to a wrapper around nsContentUtils::ReportToConsole for window

View File

@@ -21,6 +21,14 @@ MOZ_RUNINIT static LinkedList<WebTaskScheduler> gWebTaskSchedulersMainThread;
static Atomic<uint64_t> gWebTaskEnqueueOrder(0);
// According to
// https://github.com/WICG/scheduling-apis/issues/113#issuecomment-2596102676,
// tasks with User_blocking or User_visible needs to run before timers.
static bool IsNormalOrHighPriority(TaskPriority aPriority) {
return aPriority == TaskPriority::User_blocking ||
aPriority == TaskPriority::User_visible;
}
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback, WebTaskQueue& aQueue,
const char* aName, uint32_t aFlags = 0) {
@@ -113,6 +121,10 @@ void WebTask::RunAbortAlgorithm() {
// was async and there's a signal.abort() call in the callback.
if (isInList()) {
remove();
MOZ_ASSERT(mScheduler);
if (HasScheduled()) {
mScheduler->NotifyTaskWillBeRunOrAborted(this);
}
}
AutoJSAPI jsapi;
@@ -134,7 +146,7 @@ bool WebTask::Run() {
MOZ_ASSERT(mScheduler);
remove();
mScheduler->RemoveEntryFromTaskQueueMapIfNeeded(mWebTaskQueueHashKey);
mScheduler->NotifyTaskWillBeRunOrAborted(this);
ClearWebTaskScheduler();
if (!mCallback) {
@@ -341,7 +353,9 @@ already_AddRefed<Promise> WebTaskScheduler::PostTask(
if (delay > 0) {
nsresult rv = SetTimeoutForDelayedTask(
task, delay, GetEventQueuePriority(finalPrioritySource->Priority()));
task, delay,
GetEventQueuePriority(finalPrioritySource->Priority(),
false /* aIsContinuation */));
if (NS_FAILED(rv)) {
promise->MaybeRejectWithUnknownError(
"Failed to setup timeout for delayed task");
@@ -349,8 +363,8 @@ already_AddRefed<Promise> WebTaskScheduler::PostTask(
return promise.forget();
}
if (!QueueTask(task,
GetEventQueuePriority(finalPrioritySource->Priority()))) {
if (!DispatchTask(task, GetEventQueuePriority(finalPrioritySource->Priority(),
false /* aIsContinuation */))) {
MOZ_ASSERT(task->isInList());
task->remove();
@@ -422,9 +436,9 @@ already_AddRefed<Promise> WebTaskScheduler::YieldImpl() {
CreateTask(optionalSignal, {}, true /* aIsContinuation */, Nothing(),
nullptr, promise);
EventQueuePriority eventQueuePriority =
GetEventQueuePriority(prioritySource->Priority());
if (!QueueTask(task, eventQueuePriority)) {
EventQueuePriority eventQueuePriority = GetEventQueuePriority(
prioritySource->Priority(), true /* aIsContinuation */);
if (!DispatchTask(task, eventQueuePriority)) {
MOZ_ASSERT(task->isInList());
// CreateTask adds the task to WebTaskScheduler's queue, so we
// need to remove from it when we failed to dispatch the runnable.
@@ -460,12 +474,23 @@ already_AddRefed<WebTask> WebTaskScheduler::CreateTask(
return task.forget();
}
bool WebTaskScheduler::QueueTask(WebTask* aTask, EventQueuePriority aPriority) {
bool WebTaskScheduler::DispatchTask(WebTask* aTask,
EventQueuePriority aPriority) {
if (!DispatchEventLoopRunnable(aPriority)) {
return false;
}
MOZ_ASSERT(!aTask->HasScheduled());
aTask->SetHasScheduled(true);
auto taskQueue = mWebTaskQueues.Lookup(aTask->TaskQueueHashKey());
MOZ_ASSERT(taskQueue);
if (IsNormalOrHighPriority(aTask->Priority()) &&
!taskQueue->HasScheduledTasks()) {
// This is the first task that is scheduled for this queue.
IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled();
}
aTask->SetHasScheduled();
return true;
}
@@ -540,7 +565,24 @@ void WebTaskScheduler::Disconnect() {
}
void WebTaskScheduler::RunTaskSignalPriorityChange(TaskSignal* aTaskSignal) {
if (auto entry = mWebTaskQueues.Lookup({aTaskSignal, false})) {
// aIsContinuation is always false because continued tasks,
// a.k.a yield(), can't change its priority.
WebTaskQueueHashKey key(aTaskSignal, false /* aIsContinuation */);
if (auto entry = mWebTaskQueues.Lookup(key)) {
if (IsNormalOrHighPriority(entry.Data().Priority()) !=
IsNormalOrHighPriority(key.Priority())) {
// The counter needs to be adjusted if it has scheduled tasks
// because this queue changes its priority.
if (entry.Data().HasScheduledTasks()) {
if (IsNormalOrHighPriority(key.Priority())) {
// Promoted from lower priority to high priority.
IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled();
} else {
// Demoted from high priority to low priority.
DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled();
}
}
}
entry.Data().SetPriority(aTaskSignal->Priority());
}
}
@@ -575,28 +617,55 @@ WebTaskScheduler::SelectedTaskQueueData WebTaskScheduler::SelectTaskQueue(
}
EventQueuePriority WebTaskScheduler::GetEventQueuePriority(
const TaskPriority& aPriority) const {
const TaskPriority& aPriority, bool aIsContinuation) const {
switch (aPriority) {
case TaskPriority::User_blocking:
return EventQueuePriority::MediumHigh;
case TaskPriority::User_visible:
return aIsContinuation ? EventQueuePriority::MediumHigh
: EventQueuePriority::Normal;
case TaskPriority::Background:
// Bug 1941888 intends to tweak the runnable priorities
// for better results.
return EventQueuePriority::Normal;
return EventQueuePriority::Low;
default:
MOZ_ASSERT_UNREACHABLE("Invalid TaskPriority");
return EventQueuePriority::Normal;
}
}
void WebTaskScheduler::RemoveEntryFromTaskQueueMapIfNeeded(
const WebTaskQueueHashKey& aHashKey) {
MOZ_ASSERT(mWebTaskQueues.Contains(aHashKey));
if (auto entry = mWebTaskQueues.Lookup(aHashKey)) {
WebTaskQueue& taskQueue = *entry;
void WebTaskScheduler::NotifyTaskWillBeRunOrAborted(const WebTask* aWebTask) {
const WebTaskQueueHashKey& hashKey = aWebTask->TaskQueueHashKey();
MOZ_ASSERT(mWebTaskQueues.Contains(hashKey));
if (auto entry = mWebTaskQueues.Lookup(hashKey)) {
const WebTaskQueue& taskQueue = *entry;
if (IsNormalOrHighPriority(taskQueue.Priority())) {
// If the taskQueue
// 1. is empty
// 2. or it's not empty but the existing tasks are
// not scheduled (delay tasks).
if (!taskQueue.HasScheduledTasks()) {
DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled();
}
}
if (taskQueue.IsEmpty()) {
DeleteEntryFromWebTaskQueueMap(aHashKey);
DeleteEntryFromWebTaskQueueMap(hashKey);
}
}
}
WebTaskQueue::~WebTaskQueue() {
MOZ_ASSERT(mScheduler);
bool hasScheduledTask = false;
for (const auto& task : mTasks) {
if (!hasScheduledTask && task->HasScheduled()) {
hasScheduledTask = true;
}
task->ClearWebTaskScheduler();
}
mTasks.clear();
if (hasScheduledTask && IsNormalOrHighPriority(Priority())) {
mScheduler->DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled();
}
}
} // namespace mozilla::dom

View File

@@ -21,6 +21,12 @@
#include "mozilla/dom/WebTaskSchedulingBinding.h"
namespace mozilla::dom {
// Keep tracks of the number of same-event-loop-high-priority-queues
// (User_blocking or User_visible) that have at least one task scheduled.
MOZ_CONSTINIT extern uint32_t
gNumNormalOrHighPriorityQueuesHaveTaskScheduledMainThread;
// https://wicg.github.io/scheduling-apis/#scheduling-state
class WebTaskSchedulingState {
public:
@@ -104,6 +110,16 @@ class WebTaskQueueHashKey : public PLDHashEntryHdr {
}
}
TaskPriority Priority() const {
return mKey.match(
[&](const StaticPriorityTaskQueueKey& aStaticKey) {
return static_cast<TaskPriority>(aStaticKey);
},
[&](const DynamicPriorityTaskQueueKey& aDynamicKey) {
return aDynamicKey->Priority();
});
}
static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
static PLDHashNumber HashKey(KeyTypePointer aKey) {
@@ -121,16 +137,6 @@ class WebTaskQueueHashKey : public PLDHashEntryHdr {
const WebTaskQueueTypeKey& GetTypeKey() const { return mKey; }
private:
TaskPriority Priority() const {
return mKey.match(
[&](const StaticPriorityTaskQueueKey& aStaticKey) {
return static_cast<TaskPriority>(aStaticKey);
},
[&](const DynamicPriorityTaskQueueKey& aDynamicKey) {
return aDynamicKey->Priority();
});
}
WebTaskQueueTypeKey mKey;
const bool mIsContinuation;
};
@@ -160,8 +166,17 @@ class WebTask : public LinkedListElement<RefPtr<WebTask>>,
void ClearWebTaskScheduler() { mScheduler = nullptr; }
const WebTaskQueueHashKey& TaskQueueHashKey() const {
return mWebTaskQueueHashKey;
}
TaskPriority Priority() const { return mWebTaskQueueHashKey.Priority(); }
private:
void SetHasScheduled(bool aHasScheduled) { mHasScheduled = aHasScheduled; }
void SetHasScheduled() {
MOZ_ASSERT(!mHasScheduled);
mHasScheduled = true;
}
uint32_t mEnqueueOrder;
@@ -188,18 +203,13 @@ class WebTaskQueue {
public:
static constexpr int EffectivePriorityCount = 6;
explicit WebTaskQueue(WebTaskScheduler* aScheduler) {
explicit WebTaskQueue(WebTaskScheduler* aScheduler) : mScheduler(aScheduler) {
MOZ_ASSERT(aScheduler);
}
WebTaskQueue(WebTaskQueue&& aWebTaskQueue) = default;
~WebTaskQueue() {
for (const auto& task : mTasks) {
task->ClearWebTaskScheduler();
}
mTasks.clear();
}
~WebTaskQueue();
TaskPriority Priority() const { return mPriority; }
void SetPriority(TaskPriority aNewPriority) { mPriority = aNewPriority; }
@@ -238,6 +248,10 @@ class WebTaskQueue {
private:
TaskPriority mPriority = TaskPriority::User_visible;
LinkedList<RefPtr<WebTask>> mTasks;
// WebTaskScheduler owns WebTaskQueue as a hashtable value, so using a raw
// pointer points to WebTaskScheduler is ok.
WebTaskScheduler* mScheduler;
};
class WebTaskSchedulerMainThread;
@@ -281,7 +295,9 @@ class WebTaskScheduler : public nsWrapperCache,
MOZ_ASSERT(result);
}
void RemoveEntryFromTaskQueueMapIfNeeded(const WebTaskQueueHashKey&);
void NotifyTaskWillBeRunOrAborted(const WebTask* aWebTask);
virtual void IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() = 0;
virtual void DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() = 0;
protected:
virtual ~WebTaskScheduler() = default;
@@ -299,7 +315,7 @@ class WebTaskScheduler : public nsWrapperCache,
const Maybe<SchedulerPostTaskCallback&>& aCallback,
WebTaskSchedulingState* aSchedulingState, Promise* aPromise);
bool QueueTask(WebTask* aTask, EventQueuePriority aPriority);
bool DispatchTask(WebTask* aTask, EventQueuePriority aPriority);
SelectedTaskQueueData SelectTaskQueue(
const Optional<OwningNonNull<AbortSignal>>& aSignal,
@@ -309,7 +325,8 @@ class WebTaskScheduler : public nsWrapperCache,
EventQueuePriority aPriority) = 0;
virtual bool DispatchEventLoopRunnable(EventQueuePriority aPriority) = 0;
EventQueuePriority GetEventQueuePriority(const TaskPriority& aPriority) const;
EventQueuePriority GetEventQueuePriority(const TaskPriority& aPriority,
bool aIsContinuation) const;
nsTHashMap<WebTaskQueueHashKey, WebTaskQueue>& GetWebTaskQueues() {
return mWebTaskQueues;
@@ -330,7 +347,7 @@ class DelayedWebTaskHandler final : public TimeoutHandler {
MOZ_CAN_RUN_SCRIPT bool Call(const char* /* unused */) override {
if (mScheduler && mWebTask) {
MOZ_ASSERT(!mWebTask->HasScheduled());
if (!mScheduler->QueueTask(mWebTask, mPriority)) {
if (!mScheduler->DispatchTask(mWebTask, mPriority)) {
return false;
}
}

View File

@@ -12,6 +12,8 @@
namespace mozilla::dom {
uint32_t gNumNormalOrHighPriorityQueuesHaveTaskScheduledMainThread = 0;
NS_IMETHODIMP WebTaskMainThreadRunnable::Run() {
if (mScheduler) {
RefPtr<WebTask> task = mScheduler->GetNextTask(true /* aIsMainThread */);
@@ -52,4 +54,16 @@ bool WebTaskSchedulerMainThread::DispatchEventLoopRunnable(
NS_DispatchToMainThreadQueue(runnable.forget(), aPriority));
return true;
}
void WebTaskSchedulerMainThread::
IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() {
++gNumNormalOrHighPriorityQueuesHaveTaskScheduledMainThread;
}
void WebTaskSchedulerMainThread::
DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() {
MOZ_DIAGNOSTIC_ASSERT(
gNumNormalOrHighPriorityQueuesHaveTaskScheduledMainThread > 0);
--gNumNormalOrHighPriorityQueuesHaveTaskScheduledMainThread;
}
} // namespace mozilla::dom

View File

@@ -32,6 +32,9 @@ class WebTaskSchedulerMainThread final : public WebTaskScheduler {
explicit WebTaskSchedulerMainThread(nsIGlobalObject* aParent)
: WebTaskScheduler(aParent) {}
void IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() override;
void DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() override;
private:
nsresult SetTimeoutForDelayedTask(WebTask* aTask, uint64_t aDelay,
EventQueuePriority aPriority) override;

View File

@@ -9,7 +9,6 @@
#include "mozilla/dom/TimeoutManager.h"
namespace mozilla::dom {
WebTaskWorkerRunnable::WebTaskWorkerRunnable(
WebTaskSchedulerWorker* aSchedulerWorker)
: WorkerSameThreadRunnable("WebTaskWorkerRunnable"),
@@ -113,4 +112,16 @@ void WebTaskSchedulerWorker::Disconnect() {
}
WebTaskScheduler::Disconnect();
}
void WebTaskSchedulerWorker::
IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() {
++mNumHighPriorityQueuesHaveTaskScheduled;
}
void WebTaskSchedulerWorker::
DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() {
MOZ_ASSERT(mNumHighPriorityQueuesHaveTaskScheduled > 0);
--mNumHighPriorityQueuesHaveTaskScheduled;
}
} // namespace mozilla::dom

View File

@@ -38,6 +38,13 @@ class WebTaskSchedulerWorker final : public WebTaskScheduler {
void Disconnect() override;
void IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() override;
void DecreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled() override;
bool HasScheduledNormalOrHighPriorityWebTasks() const {
return mNumHighPriorityQueuesHaveTaskScheduled;
}
private:
~WebTaskSchedulerWorker() = default;
@@ -47,6 +54,12 @@ class WebTaskSchedulerWorker final : public WebTaskScheduler {
RefPtr<StrongWorkerRef> mWorkerRef;
bool mWorkerIsShuttingDown{false};
// Unlike window global where multiple globals can share the
// same event loop, worker globals don't share event loops,
// so it's okay to have this counter lives inside the
// scheduler for workers.
uint32_t mNumHighPriorityQueuesHaveTaskScheduled = 0;
};
} // namespace mozilla::dom
#endif

View File

@@ -771,6 +771,13 @@ int32_t WorkerGlobalScope::SetTimeoutOrInterval(
Timeout::Reason::eTimeoutOrInterval, aRv);
}
bool WorkerGlobalScope::HasScheduledNormalOrHighPriorityWebTasks() const {
if (!mWebTaskScheduler) {
return false;
}
return mWebTaskScheduler->HasScheduledNormalOrHighPriorityWebTasks();
}
void WorkerGlobalScope::GetOrigin(nsAString& aOrigin) const {
AssertIsOnWorkerThread();
nsContentUtils::GetWebExposedOriginSerialization(

View File

@@ -387,6 +387,7 @@ class WorkerGlobalScope : public WorkerGlobalScopeBase {
WebTaskScheduler* Scheduler();
WebTaskScheduler* GetExistingScheduler() const;
void SetWebTaskSchedulingState(WebTaskSchedulingState* aState) override;
bool HasScheduledNormalOrHighPriorityWebTasks() const override;
WebTaskSchedulingState* GetWebTaskSchedulingState() const override {
return mWebTaskSchedulingState;

View File

@@ -1,8 +0,0 @@
[yield-priority-timers.any.worker.html]
[yield() with timer tasks (inherit signal)]
expected: FAIL
[yield-priority-timers.any.html]
[yield() with timer tasks (inherit signal)]
expected: FAIL