/* -*- 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 "vm/HelperThreads.h" #include "mozilla/ReverseIterator.h" // mozilla::Reversed(...) #include "mozilla/ScopeExit.h" #include "mozilla/Span.h" // mozilla::Span #include "mozilla/Utf8.h" // mozilla::Utf8Unit #include #include "frontend/CompilationStencil.h" // frontend::CompilationStencil #include "gc/GC.h" #include "jit/Ion.h" #include "jit/IonCompileTask.h" #include "jit/JitRuntime.h" #include "jit/JitScript.h" #include "js/CompileOptions.h" // JS::PrefableCompileOptions, JS::ReadOnlyCompileOptions #include "js/experimental/CompileScript.h" // JS::ThreadStackQuotaForSize #include "js/friend/StackLimits.h" // js::ReportOverRecursed #include "js/HelperThreadAPI.h" #include "js/Stack.h" #include "js/UniquePtr.h" #include "js/Utility.h" #include "threading/CpuCount.h" #include "vm/ErrorReporting.h" #include "vm/HelperThreadState.h" #include "vm/InternalThreadPool.h" #include "vm/MutexIDs.h" #include "wasm/WasmGenerator.h" using namespace js; using mozilla::TimeDuration; using mozilla::TimeStamp; using mozilla::Utf8Unit; using JS::DispatchReason; namespace js { Mutex gHelperThreadLock(mutexid::GlobalHelperThreadState); GlobalHelperThreadState* gHelperThreadState = nullptr; } // namespace js bool js::CreateHelperThreadsState() { MOZ_ASSERT(!gHelperThreadState); gHelperThreadState = js_new(); return gHelperThreadState; } void js::DestroyHelperThreadsState() { AutoLockHelperThreadState lock; if (!gHelperThreadState) { return; } gHelperThreadState->finish(lock); js_delete(gHelperThreadState); gHelperThreadState = nullptr; } bool js::EnsureHelperThreadsInitialized() { MOZ_ASSERT(gHelperThreadState); return gHelperThreadState->ensureInitialized(); } static size_t ClampDefaultCPUCount(size_t cpuCount) { // It's extremely rare for SpiderMonkey to have more than a few cores worth // of work. At higher core counts, performance can even decrease due to NUMA // (and SpiderMonkey's lack of NUMA-awareness), contention, and general lack // of optimization for high core counts. So to avoid wasting thread stack // resources (and cluttering gdb and core dumps), clamp to 8 cores for now. return std::min(cpuCount, 8); } static size_t ThreadCountForCPUCount(size_t cpuCount) { // We need at least two threads for tier-2 wasm compilations, because // there's a master task that holds a thread while other threads do the // compilation. return std::max(cpuCount, 2); } bool js::SetFakeCPUCount(size_t count) { HelperThreadState().setCpuCount(count); return true; } void GlobalHelperThreadState::setCpuCount(size_t count) { // This must be called before any threads have been initialized. AutoLockHelperThreadState lock; MOZ_ASSERT(!isInitialized(lock)); // We can't do this if an external thread pool is in use. MOZ_ASSERT(!dispatchTaskCallback); cpuCount = count; threadCount = ThreadCountForCPUCount(count); } size_t js::GetHelperThreadCount() { return HelperThreadState().threadCount; } size_t js::GetHelperThreadCPUCount() { return HelperThreadState().cpuCount; } size_t js::GetMaxWasmCompilationThreads() { return HelperThreadState().maxWasmCompilationThreads(); } void JS::SetProfilingThreadCallbacks( JS::RegisterThreadCallback registerThread, JS::UnregisterThreadCallback unregisterThread) { HelperThreadState().registerThread = registerThread; HelperThreadState().unregisterThread = unregisterThread; } // Bug 1630189: Without MOZ_NEVER_INLINE, Windows PGO builds have a linking // error for HelperThreadTaskCallback. JS_PUBLIC_API MOZ_NEVER_INLINE void JS::SetHelperThreadTaskCallback( HelperThreadTaskCallback callback, size_t threadCount, size_t stackSize) { AutoLockHelperThreadState lock; HelperThreadState().setDispatchTaskCallback(callback, threadCount, stackSize, lock); } void GlobalHelperThreadState::setDispatchTaskCallback( JS::HelperThreadTaskCallback callback, size_t threadCount, size_t stackSize, const AutoLockHelperThreadState& lock) { MOZ_ASSERT(!isInitialized(lock)); MOZ_ASSERT(!dispatchTaskCallback); MOZ_ASSERT(threadCount != 0); MOZ_ASSERT(stackSize >= 16 * 1024); dispatchTaskCallback = callback; this->threadCount = threadCount; this->stackQuota = JS::ThreadStackQuotaForSize(stackSize); } bool js::StartOffThreadWasmCompile(wasm::CompileTask* task, wasm::CompileMode mode) { return HelperThreadState().submitTask(task, mode); } bool GlobalHelperThreadState::submitTask(wasm::CompileTask* task, wasm::CompileMode mode) { AutoLockHelperThreadState lock; if (!wasmWorklist(lock, mode).pushBack(task)) { return false; } dispatch(DispatchReason::NewTask, lock); return true; } size_t js::RemovePendingWasmCompileTasks( const wasm::CompileTaskState& taskState, wasm::CompileMode mode, const AutoLockHelperThreadState& lock) { wasm::CompileTaskPtrFifo& worklist = HelperThreadState().wasmWorklist(lock, mode); return worklist.eraseIf([&taskState](wasm::CompileTask* task) { return &task->state == &taskState; }); } void js::StartOffThreadWasmTier2Generator(wasm::UniqueTier2GeneratorTask task) { (void)HelperThreadState().submitTask(std::move(task)); } bool GlobalHelperThreadState::submitTask(wasm::UniqueTier2GeneratorTask task) { AutoLockHelperThreadState lock; MOZ_ASSERT(isInitialized(lock)); if (!wasmTier2GeneratorWorklist(lock).append(task.get())) { return false; } (void)task.release(); dispatch(DispatchReason::NewTask, lock); return true; } static void CancelOffThreadWasmTier2GeneratorLocked( AutoLockHelperThreadState& lock) { if (!HelperThreadState().isInitialized(lock)) { return; } // Remove pending tasks from the tier2 generator worklist and cancel and // delete them. { wasm::Tier2GeneratorTaskPtrVector& worklist = HelperThreadState().wasmTier2GeneratorWorklist(lock); for (size_t i = 0; i < worklist.length(); i++) { wasm::Tier2GeneratorTask* task = worklist[i]; HelperThreadState().remove(worklist, &i); js_delete(task); } } // There is at most one running Tier2Generator task and we assume that // below. static_assert(GlobalHelperThreadState::MaxTier2GeneratorTasks == 1, "code must be generalized"); // If there is a running Tier2 generator task, shut it down in a predictable // way. The task will be deleted by the normal deletion logic. for (auto* helper : HelperThreadState().helperTasks(lock)) { if (helper->is()) { // Set a flag that causes compilation to shortcut itself. helper->as()->cancel(); // Wait for the generator task to finish. This avoids a shutdown race // where the shutdown code is trying to shut down helper threads and the // ongoing tier2 compilation is trying to finish, which requires it to // have access to helper threads. uint32_t oldFinishedCount = HelperThreadState().wasmTier2GeneratorsFinished(lock); while (HelperThreadState().wasmTier2GeneratorsFinished(lock) == oldFinishedCount) { HelperThreadState().wait(lock); } // At most one of these tasks. break; } } } void js::CancelOffThreadWasmTier2Generator() { AutoLockHelperThreadState lock; CancelOffThreadWasmTier2GeneratorLocked(lock); } bool js::StartOffThreadIonCompile(jit::IonCompileTask* task, const AutoLockHelperThreadState& lock) { return HelperThreadState().submitTask(task, lock); } bool GlobalHelperThreadState::submitTask( jit::IonCompileTask* task, const AutoLockHelperThreadState& locked) { MOZ_ASSERT(isInitialized(locked)); if (!ionWorklist(locked).append(task)) { return false; } // The build is moving off-thread. Freeze the LifoAlloc to prevent any // unwanted mutations. task->alloc().lifoAlloc()->setReadOnly(); dispatch(DispatchReason::NewTask, locked); return true; } bool js::AutoStartIonFreeTask::addIonCompileToFreeTaskBatch( jit::IonCompileTask* task) { return jitRuntime_->addIonCompileToFreeTaskBatch(task); } js::AutoStartIonFreeTask::~AutoStartIonFreeTask() { jitRuntime_->maybeStartIonFreeTask(force_); } void jit::JitRuntime::maybeStartIonFreeTask(bool force) { IonFreeCompileTasks& tasks = ionFreeTaskBatch_.ref(); if (tasks.empty()) { return; } // Start an IonFreeTask if we have at least eight tasks. If |force| is true we // always start an IonFreeTask. if (!force) { constexpr size_t MinBatchSize = 8; static_assert(IonFreeCompileTasks::InlineLength >= MinBatchSize, "Minimum batch size shouldn't require malloc"); if (tasks.length() < MinBatchSize) { return; } } auto freeTask = js::MakeUnique(std::move(tasks)); if (!freeTask) { // Free compilation data on the main thread instead. MOZ_ASSERT(!tasks.empty(), "shouldn't have moved tasks on OOM"); jit::FreeIonCompileTasks(tasks); tasks.clearAndFree(); return; } AutoLockHelperThreadState lock; if (!HelperThreadState().submitTask(std::move(freeTask), lock)) { // If submitTask OOMs, then freeTask hasn't been moved so we can still use // its task list. jit::FreeIonCompileTasks(freeTask->compileTasks()); } tasks.clearAndFree(); } bool GlobalHelperThreadState::submitTask( UniquePtr&& task, const AutoLockHelperThreadState& locked) { MOZ_ASSERT(isInitialized(locked)); if (!ionFreeList(locked).append(std::move(task))) { return false; } dispatch(DispatchReason::NewTask, locked); return true; } /* * Move an IonCompilationTask for which compilation has either finished, failed, * or been cancelled into the global finished compilation list. All off thread * compilations which are started must eventually be finished. */ void js::FinishOffThreadIonCompile(jit::IonCompileTask* task, const AutoLockHelperThreadState& lock) { AutoEnterOOMUnsafeRegion oomUnsafe; if (!HelperThreadState().ionFinishedList(lock).append(task)) { oomUnsafe.crash("FinishOffThreadIonCompile"); } task->script() ->runtimeFromAnyThread() ->jitRuntime() ->numFinishedOffThreadTasksRef(lock)++; } static JSRuntime* GetSelectorRuntime(const CompilationSelector& selector) { struct Matcher { JSRuntime* operator()(JSScript* script) { return script->runtimeFromMainThread(); } JSRuntime* operator()(Zone* zone) { return zone->runtimeFromMainThread(); } JSRuntime* operator()(ZonesInState zbs) { return zbs.runtime; } JSRuntime* operator()(JSRuntime* runtime) { return runtime; } }; return selector.match(Matcher()); } static bool JitDataStructuresExist(const CompilationSelector& selector) { struct Matcher { bool operator()(JSScript* script) { return !!script->zone()->jitZone(); } bool operator()(Zone* zone) { return !!zone->jitZone(); } bool operator()(ZonesInState zbs) { return zbs.runtime->hasJitRuntime(); } bool operator()(JSRuntime* runtime) { return runtime->hasJitRuntime(); } }; return selector.match(Matcher()); } static bool IonCompileTaskMatches(const CompilationSelector& selector, jit::IonCompileTask* task) { struct TaskMatches { jit::IonCompileTask* task_; bool operator()(JSScript* script) { return script == task_->script(); } bool operator()(Zone* zone) { return zone == task_->script()->zoneFromAnyThread(); } bool operator()(JSRuntime* runtime) { return runtime == task_->script()->runtimeFromAnyThread(); } bool operator()(ZonesInState zbs) { return zbs.runtime == task_->script()->runtimeFromAnyThread() && zbs.state == task_->script()->zoneFromAnyThread()->gcState(); } }; return selector.match(TaskMatches{task}); } // If we're canceling Ion compilations for a zone/runtime, force a new // IonFreeTask even if there are just a few tasks. This lets us free as much // memory as possible. static bool ShouldForceIonFreeTask(const CompilationSelector& selector) { struct Matcher { bool operator()(JSScript* script) { return false; } bool operator()(Zone* zone) { return true; } bool operator()(ZonesInState zbs) { return true; } bool operator()(JSRuntime* runtime) { return true; } }; return selector.match(Matcher()); } void js::CancelOffThreadIonCompile(const CompilationSelector& selector) { if (!JitDataStructuresExist(selector)) { return; } if (jit::IsPortableBaselineInterpreterEnabled()) { return; } jit::JitRuntime* jitRuntime = GetSelectorRuntime(selector)->jitRuntime(); MOZ_ASSERT(jitRuntime); AutoStartIonFreeTask freeTask(jitRuntime, ShouldForceIonFreeTask(selector)); { AutoLockHelperThreadState lock; if (!HelperThreadState().isInitialized(lock)) { return; } /* Cancel any pending entries for which processing hasn't started. */ GlobalHelperThreadState::IonCompileTaskVector& worklist = HelperThreadState().ionWorklist(lock); for (size_t i = 0; i < worklist.length(); i++) { jit::IonCompileTask* task = worklist[i]; if (IonCompileTaskMatches(selector, task)) { // Once finished, tasks are added to a Linked list which is // allocated with the IonCompileTask class. The IonCompileTask is // allocated in the LifoAlloc so we need the LifoAlloc to be mutable. worklist[i]->alloc().lifoAlloc()->setReadWrite(); FinishOffThreadIonCompile(task, lock); HelperThreadState().remove(worklist, &i); } } /* Wait for in progress entries to finish up. */ bool cancelled; do { cancelled = false; for (auto* helper : HelperThreadState().helperTasks(lock)) { if (!helper->is()) { continue; } jit::IonCompileTask* ionCompileTask = helper->as(); if (IonCompileTaskMatches(selector, ionCompileTask)) { ionCompileTask->mirGen().cancel(); cancelled = true; } } if (cancelled) { HelperThreadState().wait(lock); } } while (cancelled); /* Cancel code generation for any completed entries. */ GlobalHelperThreadState::IonCompileTaskVector& finished = HelperThreadState().ionFinishedList(lock); for (size_t i = 0; i < finished.length(); i++) { jit::IonCompileTask* task = finished[i]; if (IonCompileTaskMatches(selector, task)) { JSRuntime* rt = task->script()->runtimeFromAnyThread(); rt->jitRuntime()->numFinishedOffThreadTasksRef(lock)--; jit::FinishOffThreadTask(rt, freeTask, task); HelperThreadState().remove(finished, &i); } } } /* Cancel lazy linking for pending tasks (attached to the ionScript). */ JSRuntime* runtime = GetSelectorRuntime(selector); jit::IonCompileTask* task = runtime->jitRuntime()->ionLazyLinkList(runtime).getFirst(); while (task) { jit::IonCompileTask* next = task->getNext(); if (IonCompileTaskMatches(selector, task)) { jit::FinishOffThreadTask(runtime, freeTask, task); } task = next; } } #ifdef DEBUG bool js::HasOffThreadIonCompile(Zone* zone) { if (jit::IsPortableBaselineInterpreterEnabled()) { return false; } AutoLockHelperThreadState lock; if (!HelperThreadState().isInitialized(lock)) { return false; } GlobalHelperThreadState::IonCompileTaskVector& worklist = HelperThreadState().ionWorklist(lock); for (size_t i = 0; i < worklist.length(); i++) { jit::IonCompileTask* task = worklist[i]; if (task->script()->zoneFromAnyThread() == zone) { return true; } } for (auto* helper : HelperThreadState().helperTasks(lock)) { if (!helper->is()) { continue; } JSScript* script = helper->as()->script(); if (script->zoneFromAnyThread() == zone) { return true; } } GlobalHelperThreadState::IonCompileTaskVector& finished = HelperThreadState().ionFinishedList(lock); for (size_t i = 0; i < finished.length(); i++) { jit::IonCompileTask* task = finished[i]; if (task->script()->zoneFromAnyThread() == zone) { return true; } } JSRuntime* rt = zone->runtimeFromMainThread(); if (rt->hasJitRuntime()) { jit::IonCompileTask* task = rt->jitRuntime()->ionLazyLinkList(rt).getFirst(); while (task) { if (task->script()->zone() == zone) { return true; } task = task->getNext(); } } return false; } #endif void js::StartOffThreadDelazification( JSContext* maybeCx, const JS::ReadOnlyCompileOptions& options, const frontend::CompilationStencil& stencil) { // Skip delazify tasks if we parse everything on-demand or ahead. auto strategy = options.eagerDelazificationStrategy(); if (strategy == JS::DelazificationOption::OnDemandOnly || strategy == JS::DelazificationOption::ParseEverythingEagerly) { return; } // Skip delazify task if code coverage is enabled. if (maybeCx && maybeCx->realm()->collectCoverageForDebug()) { return; } if (!CanUseExtraThreads()) { return; } JSRuntime* maybeRuntime = maybeCx ? maybeCx->runtime() : nullptr; UniquePtr task; task = DelazifyTask::Create(maybeRuntime, options, stencil); if (!task) { return; } // Schedule delazification task if there is any function to delazify. if (!task->done()) { AutoLockHelperThreadState lock; HelperThreadState().submitTask(task.release(), lock); } } UniquePtr DelazifyTask::Create( JSRuntime* maybeRuntime, const JS::ReadOnlyCompileOptions& options, const frontend::CompilationStencil& stencil) { UniquePtr task; task.reset(js_new(maybeRuntime, options.prefableOptions())); if (!task) { return nullptr; } if (!task->init(options, stencil)) { // In case of errors, skip this and delazify on-demand. return nullptr; } return task; } DelazifyTask::DelazifyTask( JSRuntime* maybeRuntime, const JS::PrefableCompileOptions& initialPrefableOptions) : maybeRuntime(maybeRuntime), delazificationCx(initialPrefableOptions, HelperThreadState().stackQuota) { } DelazifyTask::~DelazifyTask() { // The LinkedListElement destructor will remove us from any list we are part // of without synchronization, so ensure that doesn't happen. MOZ_DIAGNOSTIC_ASSERT(!isInList()); } bool DelazifyTask::init(const JS::ReadOnlyCompileOptions& options, const frontend::CompilationStencil& stencil) { return delazificationCx.init(options, stencil); } size_t DelazifyTask::sizeOfExcludingThis( mozilla::MallocSizeOf mallocSizeOf) const { return delazificationCx.sizeOfExcludingThis(mallocSizeOf); } void DelazifyTask::runHelperThreadTask(AutoLockHelperThreadState& lock) { { AutoUnlockHelperThreadState unlock(lock); // NOTE: We do not report errors beyond this scope, as there is no where // to report these errors to. In the mean time, prevent the eager // delazification from running after any kind of errors. (void)runTask(); } // If we should continue to delazify even more functions, then re-add this // task to the vector of delazification tasks. This might happen when the // DelazifyTask is interrupted by a higher priority task. (see // mozilla::TaskController & mozilla::Task) if (!delazificationCx.done()) { HelperThreadState().submitTask(this, lock); } else { UniquePtr freeTask(js_new(this)); if (freeTask) { HelperThreadState().submitTask(std::move(freeTask), lock); } } } bool DelazifyTask::runTask() { return delazificationCx.delazify(); } bool DelazifyTask::done() const { return delazificationCx.done(); } void FreeDelazifyTask::runHelperThreadTask(AutoLockHelperThreadState& locked) { { AutoUnlockHelperThreadState unlock(locked); js_delete(task); task = nullptr; } js_delete(this); } static void CancelPendingDelazifyTask(JSRuntime* rt, AutoLockHelperThreadState& lock) { auto& delazifyList = HelperThreadState().delazifyWorklist(lock); auto end = delazifyList.end(); for (auto iter = delazifyList.begin(); iter != end;) { DelazifyTask* task = *iter; ++iter; if (task->runtimeMatchesOrNoRuntime(rt)) { task->removeFrom(delazifyList); js_delete(task); } } } static void WaitUntilCancelledDelazifyTasks(JSRuntime* rt, AutoLockHelperThreadState& lock) { if (!HelperThreadState().isInitialized(lock)) { return; } while (true) { CancelPendingDelazifyTask(rt, lock); // If running tasks are delazifying any functions, then we have to wait // until they complete to remove them from the pending list. DelazifyTask // are inserting themself back to be processed once more after delazifying a // function. bool inProgress = false; for (auto* helper : HelperThreadState().helperTasks(lock)) { if (helper->is() && helper->as()->runtimeMatchesOrNoRuntime(rt)) { inProgress = true; break; } } if (!inProgress) { break; } HelperThreadState().wait(lock); } #ifdef DEBUG for (DelazifyTask* task : HelperThreadState().delazifyWorklist(lock)) { MOZ_ASSERT(!task->runtimeMatchesOrNoRuntime(rt)); } for (auto* helper : HelperThreadState().helperTasks(lock)) { MOZ_ASSERT_IF(helper->is(), !helper->as()->runtimeMatchesOrNoRuntime(rt)); } #endif } static void WaitUntilEmptyFreeDelazifyTaskVector( AutoLockHelperThreadState& lock) { if (!HelperThreadState().isInitialized(lock)) { return; } while (true) { bool inProgress = false; auto& freeList = HelperThreadState().freeDelazifyTaskVector(lock); if (!freeList.empty()) { inProgress = true; } // If running tasks are delazifying any functions, then we have to wait // until they complete to remove them from the pending list. DelazifyTask // are inserting themself back to be processed once more after delazifying a // function. for (auto* helper : HelperThreadState().helperTasks(lock)) { if (helper->is()) { inProgress = true; break; } } if (!inProgress) { break; } HelperThreadState().wait(lock); } } void js::CancelOffThreadDelazify(JSRuntime* runtime) { AutoLockHelperThreadState lock; // Cancel all Delazify tasks from the given runtime, and wait if tasks are // from the given runtime are being executed. WaitUntilCancelledDelazifyTasks(runtime, lock); // Empty the free list of delazify task, in case one of the delazify task // ended and therefore did not returned to the pending list of delazify tasks. WaitUntilEmptyFreeDelazifyTaskVector(lock); } static bool HasAnyDelazifyTask(JSRuntime* rt, AutoLockHelperThreadState& lock) { auto& delazifyList = HelperThreadState().delazifyWorklist(lock); for (auto task : delazifyList) { if (task->runtimeMatchesOrNoRuntime(rt)) { return true; } } for (auto* helper : HelperThreadState().helperTasks(lock)) { if (helper->is() && helper->as()->runtimeMatchesOrNoRuntime(rt)) { return true; } } return false; } void js::WaitForAllDelazifyTasks(JSRuntime* rt) { AutoLockHelperThreadState lock; if (!HelperThreadState().isInitialized(lock)) { return; } while (true) { if (!HasAnyDelazifyTask(rt, lock)) { break; } HelperThreadState().wait(lock); } } void GlobalHelperThreadState::submitTask( DelazifyTask* task, const AutoLockHelperThreadState& locked) { delazifyWorklist(locked).insertBack(task); dispatch(DispatchReason::NewTask, locked); } bool GlobalHelperThreadState::submitTask( UniquePtr task, const AutoLockHelperThreadState& locked) { if (!freeDelazifyTaskVector(locked).append(std::move(task))) { return false; } dispatch(DispatchReason::NewTask, locked); return true; } bool GlobalHelperThreadState::ensureInitialized() { MOZ_ASSERT(CanUseExtraThreads()); MOZ_ASSERT(this == &HelperThreadState()); AutoLockHelperThreadState lock; if (isInitialized(lock)) { return true; } for (size_t& i : runningTaskCount) { i = 0; } useInternalThreadPool_ = !dispatchTaskCallback; if (useInternalThreadPool(lock)) { if (!InternalThreadPool::Initialize(threadCount, lock)) { return false; } } MOZ_ASSERT(dispatchTaskCallback); if (!ensureThreadCount(threadCount, lock)) { finishThreads(lock); return false; } MOZ_ASSERT(threadCount != 0); isInitialized_ = true; return true; } bool GlobalHelperThreadState::ensureThreadCount( size_t count, AutoLockHelperThreadState& lock) { if (!helperTasks_.reserve(count)) { return false; } if (useInternalThreadPool(lock)) { InternalThreadPool& pool = InternalThreadPool::Get(); if (pool.threadCount(lock) < count) { if (!pool.ensureThreadCount(count, lock)) { return false; } threadCount = pool.threadCount(lock); } } return true; } GlobalHelperThreadState::GlobalHelperThreadState() : cpuCount(0), threadCount(0), totalCountRunningTasks(0), registerThread(nullptr), unregisterThread(nullptr), wasmTier2GeneratorsFinished_(0) { MOZ_ASSERT(!gHelperThreadState); cpuCount = ClampDefaultCPUCount(GetCPUCount()); threadCount = ThreadCountForCPUCount(cpuCount); MOZ_ASSERT(cpuCount > 0, "GetCPUCount() seems broken"); } void GlobalHelperThreadState::finish(AutoLockHelperThreadState& lock) { if (!isInitialized(lock)) { return; } finishThreads(lock); // Make sure there are no Ion free tasks left. We check this here because, // unlike the other tasks, we don't explicitly block on this when // destroying a runtime. auto& freeList = ionFreeList(lock); while (!freeList.empty()) { UniquePtr task = std::move(freeList.back()); freeList.popBack(); jit::FreeIonCompileTasks(task->compileTasks()); } } void GlobalHelperThreadState::finishThreads(AutoLockHelperThreadState& lock) { waitForAllTasksLocked(lock); terminating_ = true; if (InternalThreadPool::IsInitialized()) { InternalThreadPool::ShutDown(lock); } } #ifdef DEBUG void GlobalHelperThreadState::assertIsLockedByCurrentThread() const { gHelperThreadLock.assertOwnedByCurrentThread(); } #endif // DEBUG void GlobalHelperThreadState::dispatch( DispatchReason reason, const AutoLockHelperThreadState& locked) { if (canStartTasks(locked) && tasksPending_ < threadCount) { // This doesn't guarantee that we don't dispatch more tasks to the external // pool than necessary if tasks are taking a long time to start, but it does // limit the number. tasksPending_++; // The hazard analysis can't tell that the callback doesn't GC. JS::AutoSuppressGCAnalysis nogc; dispatchTaskCallback(reason); } } void GlobalHelperThreadState::wait( AutoLockHelperThreadState& locked, TimeDuration timeout /* = TimeDuration::Forever() */) { consumerWakeup.wait_for(locked, timeout); } void GlobalHelperThreadState::notifyAll(const AutoLockHelperThreadState&) { consumerWakeup.notify_all(); } void GlobalHelperThreadState::notifyOne(const AutoLockHelperThreadState&) { consumerWakeup.notify_one(); } bool GlobalHelperThreadState::hasActiveThreads( const AutoLockHelperThreadState& lock) { return !helperTasks(lock).empty(); } void js::WaitForAllHelperThreads() { HelperThreadState().waitForAllTasks(); } void js::WaitForAllHelperThreads(AutoLockHelperThreadState& lock) { HelperThreadState().waitForAllTasksLocked(lock); } void GlobalHelperThreadState::waitForAllTasks() { AutoLockHelperThreadState lock; waitForAllTasksLocked(lock); } void GlobalHelperThreadState::waitForAllTasksLocked( AutoLockHelperThreadState& lock) { CancelOffThreadWasmTier2GeneratorLocked(lock); while (canStartTasks(lock) || tasksPending_ || hasActiveThreads(lock)) { wait(lock); } MOZ_ASSERT(gcParallelWorklist().isEmpty(lock)); MOZ_ASSERT(ionWorklist(lock).empty()); MOZ_ASSERT(wasmWorklist(lock, wasm::CompileMode::Tier1).empty()); MOZ_ASSERT(promiseHelperTasks(lock).empty()); MOZ_ASSERT(compressionWorklist(lock).empty()); MOZ_ASSERT(ionFreeList(lock).empty()); MOZ_ASSERT(wasmWorklist(lock, wasm::CompileMode::Tier2).empty()); MOZ_ASSERT(wasmTier2GeneratorWorklist(lock).empty()); MOZ_ASSERT(!tasksPending_); MOZ_ASSERT(!hasActiveThreads(lock)); } // A task can be a "master" task, ie, it will block waiting for other worker // threads that perform work on its behalf. If so it must not take the last // available thread; there must always be at least one worker thread able to do // the actual work. (Or the system may deadlock.) // // If a task is a master task it *must* pass isMaster=true here, or perform a // similar calculation to avoid deadlock from starvation. // // isMaster should only be true if the thread calling checkTaskThreadLimit() is // a helper thread. // // NOTE: Calling checkTaskThreadLimit() from a helper thread in the dynamic // region after currentTask.emplace() and before currentTask.reset() may cause // it to return a different result than if it is called outside that dynamic // region, as the predicate inspects the values of the threads' currentTask // members. bool GlobalHelperThreadState::checkTaskThreadLimit( ThreadType threadType, size_t maxThreads, bool isMaster, const AutoLockHelperThreadState& lock) const { MOZ_ASSERT(maxThreads > 0); if (!isMaster && maxThreads >= threadCount) { return true; } size_t count = runningTaskCount[threadType]; if (count >= maxThreads) { return false; } MOZ_ASSERT(threadCount >= totalCountRunningTasks); size_t idle = threadCount - totalCountRunningTasks; // It is possible for the number of idle threads to be zero here, because // checkTaskThreadLimit() can be called from non-helper threads. Notably, // the compression task scheduler invokes it, and runs off a helper thread. if (idle == 0) { return false; } // A master thread that's the last available thread must not be allowed to // run. if (isMaster && idle == 1) { return false; } return true; } static inline bool IsHelperThreadSimulatingOOM(js::ThreadType threadType) { #if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) return js::oom::simulator.targetThread() == threadType; #else return false; #endif } void GlobalHelperThreadState::addSizeOfIncludingThis( JS::GlobalStats* stats, const AutoLockHelperThreadState& lock) const { #ifdef DEBUG assertIsLockedByCurrentThread(); #endif mozilla::MallocSizeOf mallocSizeOf = stats->mallocSizeOf_; JS::HelperThreadStats& htStats = stats->helperThread; htStats.stateData += mallocSizeOf(this); if (InternalThreadPool::IsInitialized()) { htStats.stateData += InternalThreadPool::Get().sizeOfIncludingThis(mallocSizeOf, lock); } // Report memory used by various containers htStats.stateData += ionWorklist_.sizeOfExcludingThis(mallocSizeOf) + ionFinishedList_.sizeOfExcludingThis(mallocSizeOf) + ionFreeList_.sizeOfExcludingThis(mallocSizeOf) + wasmWorklist_tier1_.sizeOfExcludingThis(mallocSizeOf) + wasmWorklist_tier2_.sizeOfExcludingThis(mallocSizeOf) + wasmTier2GeneratorWorklist_.sizeOfExcludingThis(mallocSizeOf) + promiseHelperTasks_.sizeOfExcludingThis(mallocSizeOf) + compressionPendingList_.sizeOfExcludingThis(mallocSizeOf) + compressionWorklist_.sizeOfExcludingThis(mallocSizeOf) + compressionFinishedList_.sizeOfExcludingThis(mallocSizeOf) + gcParallelWorklist_.sizeOfExcludingThis(mallocSizeOf, lock) + helperTasks_.sizeOfExcludingThis(mallocSizeOf); // Report IonCompileTasks on wait lists for (auto task : ionWorklist_) { htStats.ionCompileTask += task->sizeOfExcludingThis(mallocSizeOf); } for (auto task : ionFinishedList_) { htStats.ionCompileTask += task->sizeOfExcludingThis(mallocSizeOf); } for (const auto& task : ionFreeList_) { for (auto* compileTask : task->compileTasks()) { htStats.ionCompileTask += compileTask->sizeOfExcludingThis(mallocSizeOf); } } // Report wasm::CompileTasks on wait lists for (auto task : wasmWorklist_tier1_) { htStats.wasmCompile += task->sizeOfExcludingThis(mallocSizeOf); } for (auto task : wasmWorklist_tier2_) { htStats.wasmCompile += task->sizeOfExcludingThis(mallocSizeOf); } // Report number of helper threads. MOZ_ASSERT(htStats.idleThreadCount == 0); MOZ_ASSERT(threadCount >= totalCountRunningTasks); htStats.activeThreadCount = totalCountRunningTasks; htStats.idleThreadCount = threadCount - totalCountRunningTasks; } size_t GlobalHelperThreadState::maxIonCompilationThreads() const { if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_ION)) { return 1; } return threadCount; } size_t GlobalHelperThreadState::maxIonFreeThreads() const { // IonFree tasks are low priority. Limit to one thread to help avoid jemalloc // lock contention. return 1; } size_t GlobalHelperThreadState::maxWasmCompilationThreads() const { if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_WASM_COMPILE_TIER1) || IsHelperThreadSimulatingOOM(js::THREAD_TYPE_WASM_COMPILE_TIER2)) { return 1; } return std::min(cpuCount, threadCount); } size_t GlobalHelperThreadState::maxWasmTier2GeneratorThreads() const { return MaxTier2GeneratorTasks; } size_t GlobalHelperThreadState::maxPromiseHelperThreads() const { if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_PROMISE_TASK)) { return 1; } return std::min(cpuCount, threadCount); } size_t GlobalHelperThreadState::maxDelazifyThreads() const { if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_DELAZIFY)) { return 1; } return std::min(cpuCount, threadCount); } size_t GlobalHelperThreadState::maxCompressionThreads() const { if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_COMPRESS)) { return 1; } // Compression is triggered on major GCs to compress ScriptSources. It is // considered low priority work. return 1; } size_t GlobalHelperThreadState::maxGCParallelThreads() const { if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_GCPARALLEL)) { return 1; } return threadCount; } HelperThreadTask* GlobalHelperThreadState::maybeGetWasmTier1CompileTask( const AutoLockHelperThreadState& lock) { return maybeGetWasmCompile(lock, wasm::CompileMode::Tier1); } HelperThreadTask* GlobalHelperThreadState::maybeGetWasmTier2CompileTask( const AutoLockHelperThreadState& lock) { return maybeGetWasmCompile(lock, wasm::CompileMode::Tier2); } HelperThreadTask* GlobalHelperThreadState::maybeGetWasmCompile( const AutoLockHelperThreadState& lock, wasm::CompileMode mode) { if (!canStartWasmCompile(lock, mode)) { return nullptr; } return wasmWorklist(lock, mode).popCopyFront(); } bool GlobalHelperThreadState::canStartWasmTier1CompileTask( const AutoLockHelperThreadState& lock) { return canStartWasmCompile(lock, wasm::CompileMode::Tier1); } bool GlobalHelperThreadState::canStartWasmTier2CompileTask( const AutoLockHelperThreadState& lock) { return canStartWasmCompile(lock, wasm::CompileMode::Tier2); } bool GlobalHelperThreadState::canStartWasmCompile( const AutoLockHelperThreadState& lock, wasm::CompileMode mode) { if (wasmWorklist(lock, mode).empty()) { return false; } // Parallel compilation and background compilation should be disabled on // unicore systems. MOZ_RELEASE_ASSERT(cpuCount > 1); // If Tier2 is very backlogged we must give priority to it, since the Tier2 // queue holds onto Tier1 tasks. Indeed if Tier2 is backlogged we will // devote more resources to Tier2 and not start any Tier1 work at all. bool tier2oversubscribed = wasmTier2GeneratorWorklist(lock).length() > 20; // For Tier1 and Once compilation, honor the maximum allowed threads to // compile wasm jobs at once, to avoid oversaturating the machine. // // For Tier2 compilation we need to allow other things to happen too, so we // do not allow all logical cores to be used for background work; instead we // wish to use a fraction of the physical cores. We can't directly compute // the physical cores from the logical cores, but 1/3 of the logical cores // is a safe estimate for the number of physical cores available for // background work. size_t physCoresAvailable = size_t(ceil(cpuCount / 3.0)); size_t threads; ThreadType threadType; if (mode == wasm::CompileMode::Tier2) { if (tier2oversubscribed) { threads = maxWasmCompilationThreads(); } else { threads = physCoresAvailable; } threadType = THREAD_TYPE_WASM_COMPILE_TIER2; } else { if (tier2oversubscribed) { threads = 0; } else { threads = maxWasmCompilationThreads(); } threadType = THREAD_TYPE_WASM_COMPILE_TIER1; } return threads != 0 && checkTaskThreadLimit(threadType, threads, lock); } HelperThreadTask* GlobalHelperThreadState::maybeGetWasmTier2GeneratorTask( const AutoLockHelperThreadState& lock) { if (!canStartWasmTier2GeneratorTask(lock)) { return nullptr; } return wasmTier2GeneratorWorklist(lock).popCopy(); } bool GlobalHelperThreadState::canStartWasmTier2GeneratorTask( const AutoLockHelperThreadState& lock) { return !wasmTier2GeneratorWorklist(lock).empty() && checkTaskThreadLimit(THREAD_TYPE_WASM_GENERATOR_TIER2, maxWasmTier2GeneratorThreads(), /*isMaster=*/true, lock); } HelperThreadTask* GlobalHelperThreadState::maybeGetPromiseHelperTask( const AutoLockHelperThreadState& lock) { if (!canStartPromiseHelperTask(lock)) { return nullptr; } return promiseHelperTasks(lock).popCopy(); } bool GlobalHelperThreadState::canStartPromiseHelperTask( const AutoLockHelperThreadState& lock) { // PromiseHelperTasks can be wasm compilation tasks that in turn block on // wasm compilation so set isMaster = true. return !promiseHelperTasks(lock).empty() && checkTaskThreadLimit(THREAD_TYPE_PROMISE_TASK, maxPromiseHelperThreads(), /*isMaster=*/true, lock); } static bool IonCompileTaskHasHigherPriority(jit::IonCompileTask* first, jit::IonCompileTask* second) { // Return true if priority(first) > priority(second). // // This method can return whatever it wants, though it really ought to be a // total order. The ordering is allowed to race (change on the fly), however. // A higher warm-up counter indicates a higher priority. jit::JitScript* firstJitScript = first->script()->jitScript(); jit::JitScript* secondJitScript = second->script()->jitScript(); return firstJitScript->warmUpCount() / first->script()->length() > secondJitScript->warmUpCount() / second->script()->length(); } HelperThreadTask* GlobalHelperThreadState::maybeGetIonCompileTask( const AutoLockHelperThreadState& lock) { if (!canStartIonCompileTask(lock)) { return nullptr; } return highestPriorityPendingIonCompile(lock, /* checkExecutionStatus */ true); } HelperThreadTask* GlobalHelperThreadState::maybeGetLowPrioIonCompileTask( const AutoLockHelperThreadState& lock) { if (!canStartIonCompileTask(lock)) { return nullptr; } return highestPriorityPendingIonCompile(lock, /* checkExecutionStatus */ false); } bool GlobalHelperThreadState::canStartIonCompileTask( const AutoLockHelperThreadState& lock) { return !ionWorklist(lock).empty() && checkTaskThreadLimit(THREAD_TYPE_ION, maxIonCompilationThreads(), lock); } HelperThreadTask* GlobalHelperThreadState::maybeGetIonFreeTask( const AutoLockHelperThreadState& lock) { if (!canStartIonFreeTask(lock)) { return nullptr; } UniquePtr task = std::move(ionFreeList(lock).back()); ionFreeList(lock).popBack(); return task.release(); } bool GlobalHelperThreadState::canStartIonFreeTask( const AutoLockHelperThreadState& lock) { return !ionFreeList(lock).empty() && checkTaskThreadLimit(THREAD_TYPE_ION_FREE, maxIonFreeThreads(), lock); } jit::IonCompileTask* GlobalHelperThreadState::highestPriorityPendingIonCompile( const AutoLockHelperThreadState& lock, bool checkExecutionStatus) { auto& worklist = ionWorklist(lock); MOZ_ASSERT(!worklist.empty()); // Get the highest priority IonCompileTask which has not started compilation // yet. size_t index = worklist.length(); for (size_t i = 0; i < worklist.length(); i++) { if (checkExecutionStatus && !worklist[i]->isMainThreadRunningJS()) { continue; } if (i < index || IonCompileTaskHasHigherPriority(worklist[i], worklist[index])) { index = i; } } if (index == worklist.length()) { return nullptr; } jit::IonCompileTask* task = worklist[index]; worklist.erase(&worklist[index]); return task; } HelperThreadTask* GlobalHelperThreadState::maybeGetFreeDelazifyTask( const AutoLockHelperThreadState& lock) { auto& freeList = freeDelazifyTaskVector(lock); if (!freeList.empty()) { UniquePtr task = std::move(freeList.back()); freeList.popBack(); return task.release(); } return nullptr; } bool GlobalHelperThreadState::canStartFreeDelazifyTask( const AutoLockHelperThreadState& lock) { return !freeDelazifyTaskVector(lock).empty() && checkTaskThreadLimit(THREAD_TYPE_DELAZIFY_FREE, maxDelazifyThreads(), /*isMaster=*/true, lock); } HelperThreadTask* GlobalHelperThreadState::maybeGetDelazifyTask( const AutoLockHelperThreadState& lock) { // NOTE: We want to span all cores availables with delazification tasks, in // order to parse a maximum number of functions ahead of their executions. // Thus, as opposed to parse task which have a higher priority, we are not // exclusively executing these task on parse threads. auto& worklist = delazifyWorklist(lock); if (worklist.isEmpty()) { return nullptr; } return worklist.popFirst(); } bool GlobalHelperThreadState::canStartDelazifyTask( const AutoLockHelperThreadState& lock) { return !delazifyWorklist(lock).isEmpty() && checkTaskThreadLimit(THREAD_TYPE_DELAZIFY, maxDelazifyThreads(), /*isMaster=*/true, lock); } HelperThreadTask* GlobalHelperThreadState::maybeGetCompressionTask( const AutoLockHelperThreadState& lock) { if (!canStartCompressionTask(lock)) { return nullptr; } auto& worklist = compressionWorklist(lock); UniquePtr task = std::move(worklist.back()); worklist.popBack(); return task.release(); } bool GlobalHelperThreadState::canStartCompressionTask( const AutoLockHelperThreadState& lock) { return !compressionWorklist(lock).empty() && checkTaskThreadLimit(THREAD_TYPE_COMPRESS, maxCompressionThreads(), lock); } void GlobalHelperThreadState::startHandlingCompressionTasks( ScheduleCompressionTask schedule, JSRuntime* maybeRuntime, const AutoLockHelperThreadState& lock) { MOZ_ASSERT((schedule == ScheduleCompressionTask::GC) == (maybeRuntime != nullptr)); auto& pending = compressionPendingList(lock); for (size_t i = 0; i < pending.length(); i++) { UniquePtr& task = pending[i]; if (schedule == ScheduleCompressionTask::API || (task->runtimeMatches(maybeRuntime) && task->shouldStart())) { // OOMing during appending results in the task not being scheduled // and deleted. (void)submitTask(std::move(task), lock); remove(pending, &i); } } } bool GlobalHelperThreadState::submitTask( UniquePtr task, const AutoLockHelperThreadState& locked) { if (!compressionWorklist(locked).append(std::move(task))) { return false; } dispatch(DispatchReason::NewTask, locked); return true; } bool GlobalHelperThreadState::submitTask( GCParallelTask* task, const AutoLockHelperThreadState& locked) { gcParallelWorklist().insertBack(task, locked); dispatch(DispatchReason::NewTask, locked); return true; } HelperThreadTask* GlobalHelperThreadState::maybeGetGCParallelTask( const AutoLockHelperThreadState& lock) { if (!canStartGCParallelTask(lock)) { return nullptr; } return gcParallelWorklist().popFirst(lock); } bool GlobalHelperThreadState::canStartGCParallelTask( const AutoLockHelperThreadState& lock) { return !gcParallelWorklist().isEmpty(lock) && checkTaskThreadLimit(THREAD_TYPE_GCPARALLEL, maxGCParallelThreads(), lock); } bool js::EnqueueOffThreadCompression(JSContext* cx, UniquePtr task) { AutoLockHelperThreadState lock; auto& pending = HelperThreadState().compressionPendingList(lock); if (!pending.append(std::move(task))) { ReportOutOfMemory(cx); return false; } return true; } void js::StartHandlingCompressionsOnGC(JSRuntime* runtime) { AutoLockHelperThreadState lock; HelperThreadState().startHandlingCompressionTasks( GlobalHelperThreadState::ScheduleCompressionTask::GC, runtime, lock); } template static void ClearCompressionTaskList(T& list, JSRuntime* runtime) { for (size_t i = 0; i < list.length(); i++) { if (list[i]->runtimeMatches(runtime)) { HelperThreadState().remove(list, &i); } } } void js::CancelOffThreadCompressions(JSRuntime* runtime) { if (!CanUseExtraThreads()) { return; } AutoLockHelperThreadState lock; // Cancel all pending compression tasks. ClearCompressionTaskList(HelperThreadState().compressionPendingList(lock), runtime); ClearCompressionTaskList(HelperThreadState().compressionWorklist(lock), runtime); // Cancel all in-process compression tasks and wait for them to join so we // clean up the finished tasks. while (true) { bool inProgress = false; for (auto* helper : HelperThreadState().helperTasks(lock)) { if (!helper->is()) { continue; } if (helper->as()->runtimeMatches(runtime)) { inProgress = true; } } if (!inProgress) { break; } HelperThreadState().wait(lock); } // Clean up finished tasks. ClearCompressionTaskList(HelperThreadState().compressionFinishedList(lock), runtime); } void js::AttachFinishedCompressions(JSRuntime* runtime, AutoLockHelperThreadState& lock) { auto& finished = HelperThreadState().compressionFinishedList(lock); for (size_t i = 0; i < finished.length(); i++) { if (finished[i]->runtimeMatches(runtime)) { UniquePtr compressionTask(std::move(finished[i])); HelperThreadState().remove(finished, &i); compressionTask->complete(); } } } void js::SweepPendingCompressions(AutoLockHelperThreadState& lock) { auto& pending = HelperThreadState().compressionPendingList(lock); for (size_t i = 0; i < pending.length(); i++) { if (pending[i]->shouldCancel()) { HelperThreadState().remove(pending, &i); } } } void js::RunPendingSourceCompressions(JSRuntime* runtime) { if (!CanUseExtraThreads()) { return; } AutoLockHelperThreadState lock; HelperThreadState().startHandlingCompressionTasks( GlobalHelperThreadState::ScheduleCompressionTask::API, nullptr, lock); // Wait until all tasks have started compression. while (!HelperThreadState().compressionWorklist(lock).empty()) { HelperThreadState().wait(lock); } // Wait for all in-process compression tasks to complete. HelperThreadState().waitForAllTasksLocked(lock); AttachFinishedCompressions(runtime, lock); } void PromiseHelperTask::executeAndResolveAndDestroy(JSContext* cx) { execute(); run(cx, JS::Dispatchable::NotShuttingDown); } void PromiseHelperTask::runHelperThreadTask(AutoLockHelperThreadState& lock) { { AutoUnlockHelperThreadState unlock(lock); execute(); } // Don't release the lock between dispatching the resolve and destroy // operation (which may start immediately on another thread) and returning // from this method. dispatchResolveAndDestroy(lock); } bool js::StartOffThreadPromiseHelperTask(JSContext* cx, UniquePtr task) { // Execute synchronously if there are no helper threads. if (!CanUseExtraThreads()) { task.release()->executeAndResolveAndDestroy(cx); return true; } if (!HelperThreadState().submitTask(task.get())) { ReportOutOfMemory(cx); return false; } (void)task.release(); return true; } bool js::StartOffThreadPromiseHelperTask(PromiseHelperTask* task) { MOZ_ASSERT(CanUseExtraThreads()); return HelperThreadState().submitTask(task); } bool GlobalHelperThreadState::submitTask(PromiseHelperTask* task) { AutoLockHelperThreadState lock; if (!promiseHelperTasks(lock).append(task)) { return false; } dispatch(DispatchReason::NewTask, lock); return true; } void GlobalHelperThreadState::trace(JSTracer* trc) { { AutoLockHelperThreadState lock; #ifdef DEBUG // Since we hold the helper thread lock here we must disable GCMarker's // checking of the atom marking bitmap since that also relies on taking the // lock. GCMarker* marker = nullptr; if (trc->isMarkingTracer()) { marker = GCMarker::fromTracer(trc); marker->setCheckAtomMarking(false); } auto reenableAtomMarkingCheck = mozilla::MakeScopeExit([marker] { if (marker) { marker->setCheckAtomMarking(true); } }); #endif for (auto task : ionWorklist(lock)) { task->alloc().lifoAlloc()->setReadWrite(); task->trace(trc); task->alloc().lifoAlloc()->setReadOnly(); } for (auto task : ionFinishedList(lock)) { task->trace(trc); } for (auto* helper : HelperThreadState().helperTasks(lock)) { if (helper->is()) { helper->as()->trace(trc); } } } // The lazy link list is only accessed on the main thread, so trace it after // releasing the lock. JSRuntime* rt = trc->runtime(); if (auto* jitRuntime = rt->jitRuntime()) { jit::IonCompileTask* task = jitRuntime->ionLazyLinkList(rt).getFirst(); while (task) { task->trace(trc); task = task->getNext(); } } } // Definition of helper thread tasks. // // Priority is determined by the order they're listed here. const GlobalHelperThreadState::Selector GlobalHelperThreadState::selectors[] = { &GlobalHelperThreadState::maybeGetGCParallelTask, &GlobalHelperThreadState::maybeGetIonCompileTask, &GlobalHelperThreadState::maybeGetWasmTier1CompileTask, &GlobalHelperThreadState::maybeGetPromiseHelperTask, &GlobalHelperThreadState::maybeGetFreeDelazifyTask, &GlobalHelperThreadState::maybeGetDelazifyTask, &GlobalHelperThreadState::maybeGetCompressionTask, &GlobalHelperThreadState::maybeGetLowPrioIonCompileTask, &GlobalHelperThreadState::maybeGetIonFreeTask, &GlobalHelperThreadState::maybeGetWasmTier2CompileTask, &GlobalHelperThreadState::maybeGetWasmTier2GeneratorTask}; bool GlobalHelperThreadState::canStartTasks( const AutoLockHelperThreadState& lock) { return canStartGCParallelTask(lock) || canStartIonCompileTask(lock) || canStartWasmTier1CompileTask(lock) || canStartPromiseHelperTask(lock) || canStartFreeDelazifyTask(lock) || canStartDelazifyTask(lock) || canStartCompressionTask(lock) || canStartIonFreeTask(lock) || canStartWasmTier2CompileTask(lock) || canStartWasmTier2GeneratorTask(lock); } void JS::RunHelperThreadTask() { MOZ_ASSERT(CanUseExtraThreads()); AutoLockHelperThreadState lock; if (!gHelperThreadState || HelperThreadState().isTerminating(lock)) { return; } HelperThreadState().runOneTask(lock); } void GlobalHelperThreadState::runOneTask(AutoLockHelperThreadState& lock) { MOZ_ASSERT(tasksPending_ > 0); tasksPending_--; // The selectors may depend on the HelperThreadState not changing between task // selection and task execution, in particular, on new tasks not being added // (because of the lifo structure of the work lists). Unlocking the // HelperThreadState between task selection and execution is not well-defined. HelperThreadTask* task = findHighestPriorityTask(lock); if (task) { runTaskLocked(task, lock); dispatch(DispatchReason::FinishedTask, lock); } notifyAll(lock); } HelperThreadTask* GlobalHelperThreadState::findHighestPriorityTask( const AutoLockHelperThreadState& locked) { // Return the highest priority task that is ready to start, or nullptr. for (const auto& selector : selectors) { if (auto* task = (this->*(selector))(locked)) { return task; } } return nullptr; } void GlobalHelperThreadState::runTaskLocked(HelperThreadTask* task, AutoLockHelperThreadState& locked) { JS::AutoSuppressGCAnalysis nogc; HelperThreadState().helperTasks(locked).infallibleEmplaceBack(task); ThreadType threadType = task->threadType(); js::oom::SetThreadType(threadType); runningTaskCount[threadType]++; totalCountRunningTasks++; task->runHelperThreadTask(locked); // Delete task from helperTasks. HelperThreadState().helperTasks(locked).eraseIfEqual(task); totalCountRunningTasks--; runningTaskCount[threadType]--; js::oom::SetThreadType(js::THREAD_TYPE_NONE); }