/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsTHashMap.h" #include "WebTaskScheduler.h" #include "WebTaskSchedulerWorker.h" #include "WebTaskSchedulerMainThread.h" #include "nsGlobalWindowInner.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/TimeoutManager.h" namespace mozilla::dom { // Keeps track of all the existings schedulers that // share the same event loop. MOZ_RUNINIT static LinkedList gWebTaskSchedulersMainThread; static Atomic 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) { ImplCycleCollectionTraverse(aCallback, aQueue.Tasks(), aName, aFlags); } inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCallback, const WebTaskQueueHashKey& aField, const char* aName, uint32_t aFlags = 0) { const WebTaskQueueHashKey::WebTaskQueueTypeKey& typeKey = aField.GetTypeKey(); if (typeKey.is>()) { ImplCycleCollectionTraverse(aCallback, typeKey.as>(), aName, aFlags); } } inline void ImplCycleCollectionUnlink(WebTaskQueueHashKey& aField) { WebTaskQueueHashKey::WebTaskQueueTypeKey& typeKey = aField.GetTypeKey(); if (typeKey.is>()) { ImplCycleCollectionUnlink(typeKey.as>()); } } NS_IMPL_CYCLE_COLLECTION_CLASS(WebTaskSchedulingState) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WebTaskSchedulingState) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbortSource, mPrioritySource); NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WebTaskSchedulingState) NS_IMPL_CYCLE_COLLECTION_UNLINK(mAbortSource, mPrioritySource); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_CLASS(WebTask) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WebTask) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWebTaskQueueHashKey) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSchedulingState) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WebTask) NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback) NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) NS_IMPL_CYCLE_COLLECTION_UNLINK(mWebTaskQueueHashKey) NS_IMPL_CYCLE_COLLECTION_UNLINK(mSchedulingState) NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTING_ADDREF(WebTask) NS_IMPL_CYCLE_COLLECTING_RELEASE(WebTask) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebTask) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTION(DelayedWebTaskHandler) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DelayedWebTaskHandler) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(DelayedWebTaskHandler) NS_IMPL_CYCLE_COLLECTING_RELEASE(DelayedWebTaskHandler) WebTask::WebTask(uint32_t aEnqueueOrder, const Maybe& aCallback, WebTaskSchedulingState* aSchedlingState, Promise* aPromise, WebTaskScheduler* aWebTaskScheduler, const WebTaskQueueHashKey& aHashKey) : mEnqueueOrder(aEnqueueOrder), mPromise(aPromise), mHasScheduled(false), mSchedulingState(aSchedlingState), mScheduler(aWebTaskScheduler), mWebTaskQueueHashKey(aHashKey) { if (aCallback.isSome()) { mCallback = &aCallback.ref(); } } void WebTask::RunAbortAlgorithm() { // no-op if WebTask::Run has been called already if (mPromise->State() == Promise::PromiseState::Pending) { // There are two things that can keep a WebTask alive, either the abort // signal or WebTaskQueue. // It's possible that this task get cleared out from the WebTaskQueue first, // and then the abort signal get aborted. For example, the callback function // 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; if (!jsapi.Init(mPromise->GetGlobalObject())) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); } else { JSContext* cx = jsapi.cx(); JS::Rooted reason(cx); Signal()->GetReason(cx, &reason); mPromise->MaybeReject(reason); } } MOZ_ASSERT(!isInList()); } bool WebTask::Run() { MOZ_ASSERT(HasScheduled()); MOZ_ASSERT(mScheduler); remove(); mScheduler->NotifyTaskWillBeRunOrAborted(this); ClearWebTaskScheduler(); if (!mCallback) { // Scheduler.yield mPromise->MaybeResolveWithUndefined(); MOZ_ASSERT(!isInList()); return true; } MOZ_ASSERT(mSchedulingState); ErrorResult error; nsIGlobalObject* global = mPromise->GetGlobalObject(); if (!global || global->IsDying()) { return false; } // 11.2.2 Set event loop’s current scheduling state to state. global->SetWebTaskSchedulingState(mSchedulingState); AutoJSAPI jsapi; if (!jsapi.Init(global)) { return false; } JS::Rooted returnVal(jsapi.cx()); MOZ_ASSERT(mPromise->State() == Promise::PromiseState::Pending); MOZ_KnownLive(mCallback)->Call(&returnVal, error, "WebTask", CallbackFunction::eRethrowExceptions); // 11.2.4 Set event loop’s current scheduling state to null. global->SetWebTaskSchedulingState(nullptr); error.WouldReportJSException(); #ifdef DEBUG Promise::PromiseState promiseState = mPromise->State(); // If the state is Rejected, it means the above Call triggers the // RunAbortAlgorithm method and rejected the promise MOZ_ASSERT_IF(promiseState != Promise::PromiseState::Pending, promiseState == Promise::PromiseState::Rejected); #endif if (error.Failed()) { if (!error.IsUncatchableException()) { mPromise->MaybeReject(std::move(error)); } else { error.SuppressException(); } } else { mPromise->MaybeResolve(returnVal); } MOZ_ASSERT(!isInList()); return true; } inline void ImplCycleCollectionUnlink( nsTHashMap& aField) { aField.Clear(); } inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCallback, nsTHashMap& aField, const char* aName, uint32_t aFlags = 0) { for (auto& entry : aField) { ImplCycleCollectionTraverse( aCallback, entry.GetKey(), "nsTHashMap::WebTaskQueueHashKey", aFlags); ImplCycleCollectionTraverse( aCallback, *entry.GetModifiableData(), "nsTHashMap::WebTaskQueue", aFlags); } } NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebTaskScheduler, mParent, mWebTaskQueues) /* static */ already_AddRefed WebTaskScheduler::CreateForMainThread(nsGlobalWindowInner* aWindow) { RefPtr scheduler = new WebTaskSchedulerMainThread(aWindow->AsGlobal()); gWebTaskSchedulersMainThread.insertBack(scheduler); return scheduler.forget(); } already_AddRefed WebTaskScheduler::CreateForWorker( WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr scheduler = WebTaskSchedulerWorker::Create(aWorkerPrivate); return scheduler.forget(); } WebTaskScheduler::WebTaskScheduler(nsIGlobalObject* aParent) : mParent(aParent) { MOZ_ASSERT(aParent); } JSObject* WebTaskScheduler::WrapObject(JSContext* cx, JS::Handle aGivenProto) { return Scheduler_Binding::Wrap(cx, this, aGivenProto); } static bool ShouldRejectPromiseWithReasonCausedByAbortSignal( AbortSignal& aAbortSignal, nsIGlobalObject* aGlobal, Promise& aPromise) { MOZ_ASSERT(aGlobal); if (!aAbortSignal.Aborted()) { return false; } AutoJSAPI jsapi; if (!jsapi.Init(aGlobal)) { aPromise.MaybeRejectWithNotSupportedError( "Failed to initialize the JS context"); return true; } JSContext* cx = jsapi.cx(); JS::Rooted reason(cx); aAbortSignal.GetReason(cx, &reason); aPromise.MaybeReject(reason); return true; } // https://wicg.github.io/scheduling-apis/#sec-scheduler-alg-scheduling-tasks-and-continuations already_AddRefed WebTaskScheduler::PostTask( SchedulerPostTaskCallback& aCallback, const SchedulerPostTaskOptions& aOptions) { const Optional>& taskSignal = aOptions.mSignal; const Optional& taskPriority = aOptions.mPriority; ErrorResult rv; // Instead of making WebTaskScheduler::PostTask throws, we always // create the promise and return it. This is because we need to // create the promise explicitly to be able to reject it with // signal's reason. RefPtr promise = Promise::Create(mParent, rv); if (rv.Failed()) { return nullptr; } nsIGlobalObject* global = GetParentObject(); if (!global || global->IsDying()) { promise->MaybeRejectWithNotSupportedError("Current window is detached"); return promise.forget(); } // 4. Let state be a new scheduling state. RefPtr newState = new WebTaskSchedulingState(); AbortSignal* signalValue = nullptr; if (taskSignal.WasPassed()) { signalValue = &taskSignal.Value(); // 3. If signal is not null and it is aborted, then reject result with // signal’s abort reason and return result. if (ShouldRejectPromiseWithReasonCausedByAbortSignal(*signalValue, global, *promise)) { return promise.forget(); } // 5. Set state’s abort source to signal. newState->SetAbortSource(signalValue); } if (taskPriority.WasPassed()) { // 6. If options["priority"] exists, then set state’s priority source to the // result of creating a fixed priority unabortable task signal given // options["priority"] newState->SetPrioritySource( TaskSignal::Create(GetParentObject(), taskPriority.Value())); } else if (signalValue && signalValue->IsTaskSignal()) { // 7. Otherwise if signal is not null and implements the TaskSignal // interface, then set state’s priority source to signal. newState->SetPrioritySource( do_AddRef(static_cast(signalValue))); } if (!newState->GetPrioritySource()) { // 8. If state’s priority source is null, then set state’s priority // source to the result of creating a fixed priority unabortable task // signal given "user-visible". newState->SetPrioritySource( TaskSignal::Create(GetParentObject(), TaskPriority::User_visible)); } MOZ_ASSERT(newState->GetPrioritySource()); // 9. Let handle be the result of creating a task handle given result and // signal. // 10. If signal is not null, then add handle’s abort steps to signal. // 11. Let enqueueSteps be the following steps... RefPtr task = CreateTask(signalValue, newState->GetPrioritySource(), taskPriority, false /* aIsContinuation */, SomeRef(aCallback), newState, promise); const TaskSignal* finalPrioritySource = newState->GetPrioritySource(); // 12. Let delay be options["delay"]. const uint64_t delay = aOptions.mDelay; // 13. If delay is greater than 0, then run steps after a timeout given // scheduler’s relevant global object, "scheduler-postTask", delay, and the // following steps... if (delay > 0) { nsresult rv = SetTimeoutForDelayedTask( task, delay, GetEventQueuePriority(finalPrioritySource->Priority(), false /* aIsContinuation */)); if (NS_FAILED(rv)) { promise->MaybeRejectWithUnknownError( "Failed to setup timeout for delayed task"); } return promise.forget(); } // 14. Otherwise, run enqueueSteps. if (!DispatchTask(task, GetEventQueuePriority(finalPrioritySource->Priority(), false /* aIsContinuation */))) { MOZ_ASSERT(task->isInList()); task->remove(); promise->MaybeRejectWithNotSupportedError("Unable to queue the task"); return promise.forget(); } return promise.forget(); } // https://wicg.github.io/scheduling-apis/#schedule-a-yield-continuation already_AddRefed WebTaskScheduler::YieldImpl() { ErrorResult rv; // 1. Let result be a new promise. RefPtr promise = Promise::Create(mParent, rv); if (rv.Failed()) { return nullptr; } nsIGlobalObject* global = GetParentObject(); if (!global || global->IsDying()) { promise->MaybeRejectWithNotSupportedError("Current window is detached"); return promise.forget(); } RefPtr abortSource; RefPtr prioritySource; // 2. Let inheritedState be the scheduler’s relevant agent's event loop's // current scheduling state. if (auto* schedulingState = global->GetWebTaskSchedulingState()) { // 3. Let abortSource be inheritedState’s abort source if inheritedState is // not null, or otherwise null. abortSource = schedulingState->GetAbortSource(); // 5. Let prioritySource be inheritedState’s priority source if // inheritedState is not null, or otherwise null. prioritySource = schedulingState->GetPrioritySource(); } if (abortSource) { // 4. If abortSource is not null and abortSource is aborted, then reject // result with abortSource’s abort reason and return result. if (ShouldRejectPromiseWithReasonCausedByAbortSignal(*abortSource, global, *promise)) { return promise.forget(); } } if (!prioritySource) { // 6. If prioritySource is null, then set prioritySource to the result of // creating a fixed priority unabortable task signal given "user-visible". prioritySource = TaskSignal::Create(GetParentObject(), TaskPriority::User_visible); } // 7. Let handle be the result of creating a task handle given result and // abortSource. // 8. If abortSource is not null, then add handle’s abort steps to // abortSource. // 9. Set handle’s queue to the result of selecting the scheduler task queue // for scheduler given prioritySource and true. // 10. Schedule a task to invoke an algorithm for scheduler given handle and // the following steps: RefPtr task = CreateTask(abortSource, prioritySource, {}, true /* aIsContinuation */, Nothing(), nullptr, promise); 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. task->remove(); promise->MaybeRejectWithNotSupportedError("Unable to queue the task"); return promise.forget(); } return promise.forget(); } already_AddRefed WebTaskScheduler::CreateTask( AbortSignal* aAbortSignal, TaskSignal* aTaskSignal, const Optional& aPriority, bool aIsContinuation, const Maybe& aCallback, WebTaskSchedulingState* aSchedulingState, Promise* aPromise) { WebTaskScheduler::SelectedTaskQueueData selectedTaskQueueData = SelectTaskQueue(aTaskSignal, aPriority, aIsContinuation); gWebTaskEnqueueOrder += 1; RefPtr task = new WebTask(gWebTaskEnqueueOrder, aCallback, aSchedulingState, aPromise, this, selectedTaskQueueData.mSelectedQueueHashKey); selectedTaskQueueData.mSelectedTaskQueue.AddTask(task); if (aAbortSignal) { task->Follow(aAbortSignal); } return task.forget(); } bool WebTaskScheduler::DispatchTask(WebTask* aTask, EventQueuePriority aPriority) { if (!DispatchEventLoopRunnable(aPriority)) { return false; } MOZ_ASSERT(!aTask->HasScheduled()); auto taskQueue = mWebTaskQueues.Lookup(aTask->TaskQueueHashKey()); MOZ_DIAGNOSTIC_ASSERT(taskQueue); if (IsNormalOrHighPriority(aTask->Priority()) && !taskQueue->HasScheduledTasks()) { // This is the first task that is scheduled for this queue. IncreaseNumNormalOrHighPriorityQueuesHaveTaskScheduled(); } aTask->SetHasScheduled(); return true; } // https://wicg.github.io/scheduling-apis/#select-the-next-scheduler-task-queue-from-all-schedulers WebTask* WebTaskScheduler::GetNextTask(bool aIsMainThread) { // 1. Let queues be an empty set. AutoTArray, WebTaskQueue::EffectivePriorityCount> allQueues; allQueues.SetLength(WebTaskQueue::EffectivePriorityCount); auto processScheduler = [&](WebTaskScheduler& aScheduler) { for (auto iter = aScheduler.GetWebTaskQueues().Iter(); !iter.Done(); iter.Next()) { auto& queue = iter.Data(); if (queue.HasScheduledTasks()) { const WebTaskQueueHashKey& key = iter.Key(); nsTArray& queuesForThisPriority = allQueues[key.EffectivePriority()]; queuesForThisPriority.AppendElement(&queue); } } }; // 3. For each scheduler in schedulers, extend queues with the result of // getting the runnable task queues for scheduler. if (aIsMainThread) { // 2. Let schedulers be the set of all Scheduler objects whose relevant // agent’s event loop is event loop and that have a runnable task. for (const auto& scheduler : gWebTaskSchedulersMainThread) { processScheduler(*scheduler); } } else { // Workers don't share the same event loop. processScheduler(*this); } if (allQueues.IsEmpty()) { return nullptr; } // Reverse checking the queues, so it starts with the highest priority for (auto& queues : Reversed(allQueues)) { if (queues.IsEmpty()) { continue; } WebTaskQueue* oldestQueue = nullptr; for (auto& webTaskQueue : queues) { MOZ_ASSERT(webTaskQueue->HasScheduledTasks()); if (!oldestQueue) { oldestQueue = webTaskQueue; } else { WebTask* firstScheduledRunnableForCurrentQueue = webTaskQueue->GetFirstScheduledTask(); WebTask* firstScheduledRunnableForOldQueue = oldestQueue->GetFirstScheduledTask(); if (firstScheduledRunnableForOldQueue->EnqueueOrder() > firstScheduledRunnableForCurrentQueue->EnqueueOrder()) { oldestQueue = webTaskQueue; } } } MOZ_ASSERT(oldestQueue); return oldestQueue->GetFirstScheduledTask(); } return nullptr; } void WebTaskScheduler::Disconnect() { if (isInList()) { remove(); } mWebTaskQueues.Clear(); } void WebTaskScheduler::RunTaskSignalPriorityChange(TaskSignal* aTaskSignal) { // 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()); } } WebTaskScheduler::SelectedTaskQueueData WebTaskScheduler::SelectTaskQueue( TaskSignal* aTaskSignal, const Optional& aPriority, const bool aIsContinuation) { bool useSignal = !aPriority.WasPassed() && aTaskSignal; if (useSignal) { WebTaskQueueHashKey signalHashKey(aTaskSignal, aIsContinuation); WebTaskQueue& taskQueue = mWebTaskQueues.LookupOrInsert(signalHashKey, this); taskQueue.SetPriority(aTaskSignal->Priority()); aTaskSignal->SetWebTaskScheduler(this); return SelectedTaskQueueData{WebTaskQueueHashKey(signalHashKey), taskQueue}; } TaskPriority taskPriority = aPriority.WasPassed() ? aPriority.Value() : TaskPriority::User_visible; uint32_t staticTaskQueueMapKey = static_cast(taskPriority); WebTaskQueueHashKey staticHashKey(staticTaskQueueMapKey, aIsContinuation); WebTaskQueue& taskQueue = mWebTaskQueues.LookupOrInsert(staticHashKey, this); taskQueue.SetPriority(taskPriority); return SelectedTaskQueueData{WebTaskQueueHashKey(staticHashKey), taskQueue}; } EventQueuePriority WebTaskScheduler::GetEventQueuePriority( 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: return EventQueuePriority::Low; default: MOZ_ASSERT_UNREACHABLE("Invalid TaskPriority"); return EventQueuePriority::Normal; } } 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(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