/* -*- 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 "TaskSignal.h" #include "nsGlobalWindowInner.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/TimeoutManager.h" namespace mozilla::dom { inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCallback, WebTaskQueue& aQueue, const char* aName, uint32_t aFlags = 0) { ImplCycleCollectionTraverse(aCallback, aQueue.Tasks(), aName, aFlags); } 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_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_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) 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(); } 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()); remove(); ErrorResult error; nsIGlobalObject* global = mPromise->GetGlobalObject(); if (!global || global->IsDying()) { return false; } 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); 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; } NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebTaskScheduler, mParent, mStaticPriorityTaskQueues, mDynamicPriorityTaskQueues) /* static */ already_AddRefed WebTaskScheduler::CreateForMainThread(nsGlobalWindowInner* aWindow) { RefPtr scheduler = new WebTaskSchedulerMainThread(aWindow->AsGlobal()); return scheduler.forget(); } already_AddRefed WebTaskScheduler::CreateForWorker( WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr scheduler = new WebTaskSchedulerWorker(aWorkerPrivate); return scheduler.forget(); } WebTaskScheduler::WebTaskScheduler(nsIGlobalObject* aParent) : mParent(aParent), mNextEnqueueOrder(1) { MOZ_ASSERT(aParent); } JSObject* WebTaskScheduler::WrapObject(JSContext* cx, JS::Handle aGivenProto) { return Scheduler_Binding::Wrap(cx, this, aGivenProto); } 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(); } if (taskSignal.WasPassed()) { AbortSignal& signalValue = taskSignal.Value(); if (signalValue.Aborted()) { AutoJSAPI jsapi; if (!jsapi.Init(global)) { promise->MaybeReject(NS_ERROR_UNEXPECTED); return promise.forget(); } JSContext* cx = jsapi.cx(); JS::Rooted reason(cx); signalValue.GetReason(cx, &reason); promise->MaybeReject(reason); return promise.forget(); } } // Let queue be the result of selecting the scheduler task queue for scheduler // given signal and priority. WebTaskQueue& taskQueue = SelectTaskQueue(taskSignal, taskPriority); uint64_t delay = aOptions.mDelay; RefPtr task = CreateTask(taskQueue, taskSignal, aCallback, promise); if (delay > 0) { nsresult rv = SetTimeoutForDelayedTask(task, delay); if (NS_FAILED(rv)) { promise->MaybeRejectWithUnknownError( "Failed to setup timeout for delayed task"); } return promise.forget(); } if (!QueueTask(task)) { MOZ_ASSERT(task->isInList()); task->remove(); promise->MaybeRejectWithNotSupportedError("Unable to queue the task"); return promise.forget(); } return promise.forget(); } already_AddRefed WebTaskScheduler::CreateTask( WebTaskQueue& aQueue, const Optional>& aSignal, SchedulerPostTaskCallback& aCallback, Promise* aPromise) { uint32_t nextEnqueueOrder = mNextEnqueueOrder; ++mNextEnqueueOrder; RefPtr task = new WebTask(nextEnqueueOrder, aCallback, aPromise); aQueue.AddTask(task); if (aSignal.WasPassed()) { AbortSignal& signalValue = aSignal.Value(); task->Follow(&signalValue); } return task.forget(); } bool WebTaskScheduler::QueueTask(WebTask* aTask) { if (!DispatchEventLoopRunnable()) { return false; } MOZ_ASSERT(!aTask->HasScheduled()); aTask->SetHasScheduled(true); return true; } WebTask* WebTaskScheduler::GetNextTask() const { // We first combine queues from both mStaticPriorityTaskQueues and // mDynamicPriorityTaskQueues into a single hash map which the // keys are the priorities and the values are all the queues that // belong to this priority. // // Then From the queues which // 1. Have scheduled tasks // 2. Its priority is not less than any other queues' priority // We pick the task which has the smallest enqueue order. nsTHashMap> allQueues; for (auto iter = mStaticPriorityTaskQueues.ConstIter(); !iter.Done(); iter.Next()) { const auto& queue = iter.Data(); if (!queue->Tasks().isEmpty() && queue->GetFirstScheduledTask()) { nsTArray& queuesForThisPriority = allQueues.LookupOrInsert(static_cast(iter.Key())); queuesForThisPriority.AppendElement(queue.get()); } } for (auto iter = mDynamicPriorityTaskQueues.ConstIter(); !iter.Done(); iter.Next()) { const auto& queue = iter.Data(); if (!queue->Tasks().isEmpty() && queue->GetFirstScheduledTask()) { nsTArray& queuesForThisPriority = allQueues.LookupOrInsert( static_cast(iter.Key()->Priority())); queuesForThisPriority.AppendElement(queue.get()); } } if (allQueues.IsEmpty()) { return nullptr; } for (uint32_t priority = static_cast(TaskPriority::User_blocking); priority < static_cast(TaskPriority::EndGuard_); ++priority) { if (auto queues = allQueues.Lookup(priority)) { WebTaskQueue* oldestQueue = nullptr; MOZ_ASSERT(!queues.Data().IsEmpty()); for (auto& webTaskQueue : queues.Data()) { MOZ_ASSERT(webTaskQueue->GetFirstScheduledTask()); 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() { mStaticPriorityTaskQueues.Clear(); mDynamicPriorityTaskQueues.Clear(); } void WebTaskScheduler::RunTaskSignalPriorityChange(TaskSignal* aTaskSignal) { WebTaskQueue* const taskQueue = mDynamicPriorityTaskQueues.Get(aTaskSignal); MOZ_ASSERT(taskQueue); taskQueue->SetPriority(aTaskSignal->Priority()); } WebTaskQueue& WebTaskScheduler::SelectTaskQueue( const Optional>& aSignal, const Optional& aPriority) { bool useSignal = !aPriority.WasPassed() && aSignal.WasPassed() && aSignal.Value().IsTaskSignal(); if (useSignal) { TaskSignal* taskSignal = static_cast(&(aSignal.Value())); WebTaskQueue* const taskQueue = mDynamicPriorityTaskQueues.GetOrInsertNew(taskSignal); taskQueue->SetPriority(taskSignal->Priority()); taskSignal->SetWebTaskScheduler(this); MOZ_ASSERT(mDynamicPriorityTaskQueues.Contains(taskSignal)); return *taskQueue; } TaskPriority taskPriority = aPriority.WasPassed() ? aPriority.Value() : TaskPriority::User_visible; WebTaskQueue* const taskQueue = mStaticPriorityTaskQueues.GetOrInsertNew( static_cast(taskPriority)); taskQueue->SetPriority(taskPriority); MOZ_ASSERT( mStaticPriorityTaskQueues.Contains(static_cast(taskPriority))); return *taskQueue; } } // namespace mozilla::dom