#include "vm/HelperThreads.h"
#include "mozilla/Maybe.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Unused.h"
#include "builtin/Promise.h"
#include "frontend/BytecodeCompilation.h"
#include "gc/GCInternals.h"
#include "jit/IonBuilder.h"
#include "js/ContextOptions.h"
#include "js/SourceText.h"
#include "js/UniquePtr.h"
#include "js/Utility.h"
#include "threading/CpuCount.h"
#include "util/NativeStack.h"
#include "vm/Debugger.h"
#include "vm/ErrorReporting.h"
#include "vm/SharedImmutableStringsCache.h"
#include "vm/Time.h"
#include "vm/TraceLogging.h"
#include "vm/Xdr.h"
#include "wasm/WasmGenerator.h"
#include "gc/PrivateIterators-inl.h"
#include "vm/JSContext-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/JSScript-inl.h"
#include "vm/NativeObject-inl.h"
#include "vm/Realm-inl.h"
using namespace js;
using mozilla::Maybe;
using mozilla::TimeDuration;
using mozilla::TimeStamp;
using mozilla::Unused;
using JS::CompileOptions;
using JS::ReadOnlyCompileOptions;
namespace js {
GlobalHelperThreadState* gHelperThreadState = nullptr;
}
#define PROFILER_RAII_PASTE(id, line) id##line
#define PROFILER_RAII_EXPAND(id, line) PROFILER_RAII_PASTE(id, line)
#define PROFILER_RAII PROFILER_RAII_EXPAND(raiiObject, __LINE__)
#define AUTO_PROFILER_LABEL(label, categoryPair) \
HelperThread::AutoProfilerLabel PROFILER_RAII( \
this, label, JS::ProfilingCategoryPair::categoryPair)
bool js::CreateHelperThreadsState() {
MOZ_ASSERT(!gHelperThreadState);
gHelperThreadState = js_new<GlobalHelperThreadState>();
return gHelperThreadState != nullptr;
}
void js::DestroyHelperThreadsState() {
MOZ_ASSERT(gHelperThreadState);
gHelperThreadState->finish();
js_delete(gHelperThreadState);
gHelperThreadState = nullptr;
}
bool js::EnsureHelperThreadsInitialized() {
MOZ_ASSERT(gHelperThreadState);
return gHelperThreadState->ensureInitialized();
}
static size_t ClampDefaultCPUCount(size_t cpuCount) {
return Min<size_t>(cpuCount, 8);
}
static size_t ThreadCountForCPUCount(size_t cpuCount) {
return Max<size_t>(cpuCount, 2);
}
void js::SetFakeCPUCount(size_t count) {
MOZ_ASSERT(!HelperThreadState().threads);
HelperThreadState().cpuCount = count;
HelperThreadState().threadCount = ThreadCountForCPUCount(count);
}
void JS::SetProfilingThreadCallbacks(
JS::RegisterThreadCallback registerThread,
JS::UnregisterThreadCallback unregisterThread) {
HelperThreadState().registerThread = registerThread;
HelperThreadState().unregisterThread = unregisterThread;
}
bool js::StartOffThreadWasmCompile(wasm::CompileTask* task,
wasm::CompileMode mode) {
AutoLockHelperThreadState lock;
if (!HelperThreadState().wasmWorklist(lock, mode).pushBack(task)) {
return false;
}
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
return true;
}
void js::StartOffThreadWasmTier2Generator(wasm::UniqueTier2GeneratorTask task) {
MOZ_ASSERT(CanUseExtraThreads());
AutoLockHelperThreadState lock;
if (!HelperThreadState().wasmTier2GeneratorWorklist(lock).append(
task.get())) {
return;
}
Unused << task.release();
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
}
static void CancelOffThreadWasmTier2GeneratorLocked(
AutoLockHelperThreadState& lock) {
if (!HelperThreadState().threads) {
return;
}
{
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);
}
}
static_assert(GlobalHelperThreadState::MaxTier2GeneratorTasks == 1,
"code must be generalized");
for (auto& helper : *HelperThreadState().threads) {
if (helper.wasmTier2GeneratorTask()) {
helper.wasmTier2GeneratorTask()->cancel();
uint32_t oldFinishedCount =
HelperThreadState().wasmTier2GeneratorsFinished(lock);
while (HelperThreadState().wasmTier2GeneratorsFinished(lock) ==
oldFinishedCount) {
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
break;
}
}
}
void js::CancelOffThreadWasmTier2Generator() {
AutoLockHelperThreadState lock;
CancelOffThreadWasmTier2GeneratorLocked(lock);
}
bool js::StartOffThreadIonCompile(jit::IonBuilder* builder,
const AutoLockHelperThreadState& lock) {
if (!HelperThreadState().ionWorklist(lock).append(builder)) {
return false;
}
builder->alloc().lifoAlloc()->setReadOnly();
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
return true;
}
bool js::StartOffThreadIonFree(jit::IonBuilder* builder,
const AutoLockHelperThreadState& lock) {
MOZ_ASSERT(CanUseExtraThreads());
if (!HelperThreadState().ionFreeList(lock).append(builder)) {
return false;
}
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
return true;
}
static void FinishOffThreadIonCompile(jit::IonBuilder* builder,
const AutoLockHelperThreadState& lock) {
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!HelperThreadState().ionFinishedList(lock).append(builder)) {
oomUnsafe.crash("FinishOffThreadIonCompile");
}
builder->script()
->runtimeFromAnyThread()
->jitRuntime()
->numFinishedBuildersRef(lock)++;
}
static JSRuntime* GetSelectorRuntime(const CompilationSelector& selector) {
struct Matcher {
JSRuntime* match(JSScript* script) {
return script->runtimeFromMainThread();
}
JSRuntime* match(Realm* realm) { return realm->runtimeFromMainThread(); }
JSRuntime* match(Zone* zone) { return zone->runtimeFromMainThread(); }
JSRuntime* match(ZonesInState zbs) { return zbs.runtime; }
JSRuntime* match(JSRuntime* runtime) { return runtime; }
JSRuntime* match(AllCompilations all) { return nullptr; }
JSRuntime* match(CompilationsUsingNursery cun) { return cun.runtime; }
};
return selector.match(Matcher());
}
static bool JitDataStructuresExist(const CompilationSelector& selector) {
struct Matcher {
bool match(JSScript* script) { return !!script->realm()->jitRealm(); }
bool match(Realm* realm) { return !!realm->jitRealm(); }
bool match(Zone* zone) { return !!zone->jitZone(); }
bool match(ZonesInState zbs) { return zbs.runtime->hasJitRuntime(); }
bool match(JSRuntime* runtime) { return runtime->hasJitRuntime(); }
bool match(AllCompilations all) { return true; }
bool match(CompilationsUsingNursery cun) {
return cun.runtime->hasJitRuntime();
}
};
return selector.match(Matcher());
}
static bool IonBuilderMatches(const CompilationSelector& selector,
jit::IonBuilder* builder) {
struct BuilderMatches {
jit::IonBuilder* builder_;
bool match(JSScript* script) { return script == builder_->script(); }
bool match(Realm* realm) { return realm == builder_->script()->realm(); }
bool match(Zone* zone) {
return zone == builder_->script()->zoneFromAnyThread();
}
bool match(JSRuntime* runtime) {
return runtime == builder_->script()->runtimeFromAnyThread();
}
bool match(AllCompilations all) { return true; }
bool match(ZonesInState zbs) {
return zbs.runtime == builder_->script()->runtimeFromAnyThread() &&
zbs.state == builder_->script()->zoneFromAnyThread()->gcState();
}
bool match(CompilationsUsingNursery cun) {
return cun.runtime == builder_->script()->runtimeFromAnyThread() &&
!builder_->safeForMinorGC();
}
};
return selector.match(BuilderMatches{builder});
}
static void CancelOffThreadIonCompileLocked(const CompilationSelector& selector,
bool discardLazyLinkList,
AutoLockHelperThreadState& lock) {
if (!HelperThreadState().threads) {
return;
}
GlobalHelperThreadState::IonBuilderVector& worklist =
HelperThreadState().ionWorklist(lock);
for (size_t i = 0; i < worklist.length(); i++) {
jit::IonBuilder* builder = worklist[i];
if (IonBuilderMatches(selector, builder)) {
worklist[i]->alloc().lifoAlloc()->setReadWrite();
FinishOffThreadIonCompile(builder, lock);
HelperThreadState().remove(worklist, &i);
}
}
bool cancelled;
do {
cancelled = false;
for (auto& helper : *HelperThreadState().threads) {
if (helper.ionBuilder() &&
IonBuilderMatches(selector, helper.ionBuilder())) {
helper.ionBuilder()->cancel();
cancelled = true;
}
}
if (cancelled) {
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
} while (cancelled);
GlobalHelperThreadState::IonBuilderVector& finished =
HelperThreadState().ionFinishedList(lock);
for (size_t i = 0; i < finished.length(); i++) {
jit::IonBuilder* builder = finished[i];
if (IonBuilderMatches(selector, builder)) {
JSRuntime* rt = builder->script()->runtimeFromAnyThread();
rt->jitRuntime()->numFinishedBuildersRef(lock)--;
jit::FinishOffThreadBuilder(rt, builder, lock);
HelperThreadState().remove(finished, &i);
}
}
if (discardLazyLinkList) {
MOZ_ASSERT(!selector.is<AllCompilations>());
JSRuntime* runtime = GetSelectorRuntime(selector);
jit::IonBuilder* builder =
runtime->jitRuntime()->ionLazyLinkList(runtime).getFirst();
while (builder) {
jit::IonBuilder* next = builder->getNext();
if (IonBuilderMatches(selector, builder)) {
jit::FinishOffThreadBuilder(runtime, builder, lock);
}
builder = next;
}
}
}
void js::CancelOffThreadIonCompile(const CompilationSelector& selector,
bool discardLazyLinkList) {
if (!JitDataStructuresExist(selector)) {
return;
}
AutoLockHelperThreadState lock;
CancelOffThreadIonCompileLocked(selector, discardLazyLinkList, lock);
}
#ifdef DEBUG
bool js::HasOffThreadIonCompile(Realm* realm) {
AutoLockHelperThreadState lock;
if (!HelperThreadState().threads) {
return false;
}
GlobalHelperThreadState::IonBuilderVector& worklist =
HelperThreadState().ionWorklist(lock);
for (size_t i = 0; i < worklist.length(); i++) {
jit::IonBuilder* builder = worklist[i];
if (builder->script()->realm() == realm) {
return true;
}
}
for (auto& helper : *HelperThreadState().threads) {
if (helper.ionBuilder() &&
helper.ionBuilder()->script()->realm() == realm) {
return true;
}
}
GlobalHelperThreadState::IonBuilderVector& finished =
HelperThreadState().ionFinishedList(lock);
for (size_t i = 0; i < finished.length(); i++) {
jit::IonBuilder* builder = finished[i];
if (builder->script()->realm() == realm) {
return true;
}
}
JSRuntime* rt = realm->runtimeFromMainThread();
jit::IonBuilder* builder = rt->jitRuntime()->ionLazyLinkList(rt).getFirst();
while (builder) {
if (builder->script()->realm() == realm) {
return true;
}
builder = builder->getNext();
}
return false;
}
#endif
static const JSClassOps parseTaskGlobalClassOps = {nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
JS_GlobalObjectTraceHook};
static const JSClass parseTaskGlobalClass = {"internal-parse-task-global",
JSCLASS_GLOBAL_FLAGS,
&parseTaskGlobalClassOps};
ParseTask::ParseTask(ParseTaskKind kind, JSContext* cx,
JS::OffThreadCompileCallback callback, void* callbackData)
: kind(kind),
options(cx),
parseGlobal(nullptr),
callback(callback),
callbackData(callbackData),
overRecursed(false),
outOfMemory(false) {
MOZ_ASSERT(!cx->helperThread());
MOZ_ALWAYS_TRUE(scripts.reserve(scripts.capacity()));
MOZ_ALWAYS_TRUE(sourceObjects.reserve(sourceObjects.capacity()));
}
bool ParseTask::init(JSContext* cx, const ReadOnlyCompileOptions& options,
JSObject* global) {
MOZ_ASSERT(!cx->helperThread());
if (!this->options.copy(cx, options)) {
return false;
}
parseGlobal = global;
return true;
}
void ParseTask::activate(JSRuntime* rt) {
rt->setUsedByHelperThread(parseGlobal->zone());
}
ParseTask::~ParseTask() = default;
void ParseTask::trace(JSTracer* trc) {
if (parseGlobal->runtimeFromAnyThread() != trc->runtime()) {
return;
}
Zone* zone = MaybeForwarded(parseGlobal)->zoneFromAnyThread();
if (zone->usedByHelperThread()) {
MOZ_ASSERT(!zone->isCollecting());
return;
}
TraceManuallyBarrieredEdge(trc, &parseGlobal, "ParseTask::parseGlobal");
scripts.trace(trc);
sourceObjects.trace(trc);
}
size_t ParseTask::sizeOfExcludingThis(
mozilla::MallocSizeOf mallocSizeOf) const {
return options.sizeOfExcludingThis(mallocSizeOf) +
errors.sizeOfExcludingThis(mallocSizeOf);
}
ScriptParseTask::ScriptParseTask(JSContext* cx,
JS::SourceText<char16_t>& srcBuf,
JS::OffThreadCompileCallback callback,
void* callbackData)
: ParseTask(ParseTaskKind::Script, cx, callback, callbackData),
data(std::move(srcBuf)) {}
void ScriptParseTask::parse(JSContext* cx) {
MOZ_ASSERT(cx->helperThread());
JSScript* script;
Rooted<ScriptSourceObject*> sourceObject(cx);
{
ScopeKind scopeKind =
options.nonSyntacticScope ? ScopeKind::NonSyntactic : ScopeKind::Global;
frontend::GlobalScriptInfo info(cx, options, scopeKind);
script = frontend::CompileGlobalScript(
info, data,
&sourceObject.get());
}
if (script) {
scripts.infallibleAppend(script);
}
if (sourceObject) {
sourceObjects.infallibleAppend(sourceObject);
}
}
ModuleParseTask::ModuleParseTask(JSContext* cx,
JS::SourceText<char16_t>& srcBuf,
JS::OffThreadCompileCallback callback,
void* callbackData)
: ParseTask(ParseTaskKind::Module, cx, callback, callbackData),
data(std::move(srcBuf)) {}
void ModuleParseTask::parse(JSContext* cx) {
MOZ_ASSERT(cx->helperThread());
Rooted<ScriptSourceObject*> sourceObject(cx);
ModuleObject* module =
frontend::CompileModule(cx, options, data, &sourceObject.get());
if (module) {
scripts.infallibleAppend(module->script());
if (sourceObject) {
sourceObjects.infallibleAppend(sourceObject);
}
}
}
ScriptDecodeTask::ScriptDecodeTask(JSContext* cx,
const JS::TranscodeRange& range,
JS::OffThreadCompileCallback callback,
void* callbackData)
: ParseTask(ParseTaskKind::ScriptDecode, cx, callback, callbackData),
range(range) {}
void ScriptDecodeTask::parse(JSContext* cx) {
MOZ_ASSERT(cx->helperThread());
RootedScript resultScript(cx);
Rooted<ScriptSourceObject*> sourceObject(cx);
XDROffThreadDecoder decoder(
cx, &options, &sourceObject.get(), range);
XDRResult res = decoder.codeScript(&resultScript);
MOZ_ASSERT(bool(resultScript) == res.isOk());
if (res.isOk()) {
scripts.infallibleAppend(resultScript);
if (sourceObject) {
sourceObjects.infallibleAppend(sourceObject);
}
}
}
#if defined(JS_BUILD_BINAST)
BinASTDecodeTask::BinASTDecodeTask(JSContext* cx, const uint8_t* buf,
size_t length,
JS::OffThreadCompileCallback callback,
void* callbackData)
: ParseTask(ParseTaskKind::BinAST, cx, callback, callbackData),
data(buf, length) {}
void BinASTDecodeTask::parse(JSContext* cx) {
MOZ_ASSERT(cx->helperThread());
RootedScriptSourceObject sourceObject(cx);
JSScript* script = frontend::CompileGlobalBinASTScript(
cx, cx->tempLifoAlloc(), options, data.begin().get(), data.length(),
&sourceObject.get());
if (script) {
scripts.infallibleAppend(script);
if (sourceObject) {
sourceObjects.infallibleAppend(sourceObject);
}
}
}
#endif
MultiScriptsDecodeTask::MultiScriptsDecodeTask(
JSContext* cx, JS::TranscodeSources& sources,
JS::OffThreadCompileCallback callback, void* callbackData)
: ParseTask(ParseTaskKind::MultiScriptsDecode, cx, callback, callbackData),
sources(&sources) {}
void MultiScriptsDecodeTask::parse(JSContext* cx) {
MOZ_ASSERT(cx->helperThread());
if (!scripts.reserve(sources->length()) ||
!sourceObjects.reserve(sources->length())) {
ReportOutOfMemory(cx); return;
}
for (auto& source : *sources) {
CompileOptions opts(cx, options);
opts.setFileAndLine(source.filename, source.lineno);
RootedScript resultScript(cx);
Rooted<ScriptSourceObject*> sourceObject(cx);
XDROffThreadDecoder decoder(cx, &opts, &sourceObject.get(), source.range);
XDRResult res = decoder.codeScript(&resultScript);
MOZ_ASSERT(bool(resultScript) == res.isOk());
if (res.isErr()) {
break;
}
MOZ_ASSERT(resultScript);
scripts.infallibleAppend(resultScript);
sourceObjects.infallibleAppend(sourceObject);
}
}
void js::CancelOffThreadParses(JSRuntime* rt) {
AutoLockHelperThreadState lock;
if (!HelperThreadState().threads) {
return;
}
#ifdef DEBUG
GlobalHelperThreadState::ParseTaskVector& waitingOnGC =
HelperThreadState().parseWaitingOnGC(lock);
for (size_t i = 0; i < waitingOnGC.length(); i++) {
MOZ_ASSERT(!waitingOnGC[i]->runtimeMatches(rt));
}
#endif
while (true) {
bool pending = false;
GlobalHelperThreadState::ParseTaskVector& worklist =
HelperThreadState().parseWorklist(lock);
for (size_t i = 0; i < worklist.length(); i++) {
ParseTask* task = worklist[i];
if (task->runtimeMatches(rt)) {
pending = true;
}
}
if (!pending) {
bool inProgress = false;
for (auto& thread : *HelperThreadState().threads) {
ParseTask* task = thread.parseTask();
if (task && task->runtimeMatches(rt)) {
inProgress = true;
}
}
if (!inProgress) {
break;
}
}
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
auto& finished = HelperThreadState().parseFinishedList(lock);
while (true) {
bool found = false;
ParseTask* next;
ParseTask* task = finished.getFirst();
while (task) {
next = task->getNext();
if (task->runtimeMatches(rt)) {
found = true;
task->remove();
HelperThreadState().destroyParseTask(rt, task);
}
task = next;
}
if (!found) {
break;
}
}
#ifdef DEBUG
GlobalHelperThreadState::ParseTaskVector& worklist =
HelperThreadState().parseWorklist(lock);
for (size_t i = 0; i < worklist.length(); i++) {
ParseTask* task = worklist[i];
MOZ_ASSERT(!task->runtimeMatches(rt));
}
#endif
}
bool js::OffThreadParsingMustWaitForGC(JSRuntime* rt) {
return rt->activeGCInAtomsZone();
}
static bool EnsureConstructor(JSContext* cx, Handle<GlobalObject*> global,
JSProtoKey key) {
if (!GlobalObject::ensureConstructor(cx, global, key)) {
return false;
}
MOZ_ASSERT(global->getPrototype(key).toObject().isDelegate(),
"standard class prototype wasn't a delegate from birth");
return true;
}
static bool EnsureParserCreatedClasses(JSContext* cx, ParseTaskKind kind) {
Handle<GlobalObject*> global = cx->global();
if (!EnsureConstructor(cx, global, JSProto_Function)) {
return false; }
if (!EnsureConstructor(cx, global, JSProto_Array)) {
return false; }
if (!EnsureConstructor(cx, global, JSProto_RegExp)) {
return false; }
if (!GlobalObject::initGenerators(cx, global)) {
return false; }
if (!GlobalObject::initAsyncFunction(cx, global)) {
return false; }
if (!GlobalObject::initAsyncGenerators(cx, global)) {
return false; }
if (kind == ParseTaskKind::Module &&
!GlobalObject::ensureModulePrototypesCreated(cx, global)) {
return false;
}
return true;
}
class MOZ_RAII AutoSetCreatedForHelperThread {
Zone* zone;
public:
explicit AutoSetCreatedForHelperThread(JSObject* global)
: zone(global->zone()) {
zone->setCreatedForHelperThread();
}
void forget() { zone = nullptr; }
~AutoSetCreatedForHelperThread() {
if (zone) {
zone->clearUsedByHelperThread();
}
}
};
static JSObject* CreateGlobalForOffThreadParse(JSContext* cx,
const gc::AutoSuppressGC& nogc) {
JS::Realm* currentRealm = cx->realm();
JS::RealmOptions realmOptions(currentRealm->creationOptions(),
currentRealm->behaviors());
auto& creationOptions = realmOptions.creationOptions();
creationOptions.setInvisibleToDebugger(true)
.setMergeable(true)
.setNewCompartmentAndZone();
creationOptions.setTrace(nullptr);
return JS_NewGlobalObject(cx, &parseTaskGlobalClass,
currentRealm->principals(),
JS::DontFireOnNewGlobalHook, realmOptions);
}
static bool QueueOffThreadParseTask(JSContext* cx, ParseTask* task) {
AutoLockHelperThreadState lock;
bool mustWait = OffThreadParsingMustWaitForGC(cx->runtime());
auto& queue = mustWait ? HelperThreadState().parseWaitingOnGC(lock)
: HelperThreadState().parseWorklist(lock);
if (!queue.append(task)) {
ReportOutOfMemory(cx);
return false;
}
if (!mustWait) {
task->activate(cx->runtime());
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
}
return true;
}
bool StartOffThreadParseTask(JSContext* cx, ParseTask* task,
const ReadOnlyCompileOptions& options) {
gc::AutoSuppressGC nogc(cx);
gc::AutoSuppressNurseryCellAlloc noNurseryAlloc(cx);
AutoSuppressAllocationMetadataBuilder suppressMetadata(cx);
JSObject* global = CreateGlobalForOffThreadParse(cx, nogc);
if (!global) {
return false;
}
AutoSetCreatedForHelperThread createdForHelper(global);
if (!task->init(cx, options, global)) {
return false;
}
if (!QueueOffThreadParseTask(cx, task)) {
return false;
}
createdForHelper.forget();
return true;
}
bool js::StartOffThreadParseScript(JSContext* cx,
const ReadOnlyCompileOptions& options,
JS::SourceText<char16_t>& srcBuf,
JS::OffThreadCompileCallback callback,
void* callbackData) {
auto task =
cx->make_unique<ScriptParseTask>(cx, srcBuf, callback, callbackData);
if (!task || !StartOffThreadParseTask(cx, task.get(), options)) {
return false;
}
Unused << task.release();
return true;
}
bool js::StartOffThreadParseModule(JSContext* cx,
const ReadOnlyCompileOptions& options,
JS::SourceText<char16_t>& srcBuf,
JS::OffThreadCompileCallback callback,
void* callbackData) {
auto task =
cx->make_unique<ModuleParseTask>(cx, srcBuf, callback, callbackData);
if (!task || !StartOffThreadParseTask(cx, task.get(), options)) {
return false;
}
Unused << task.release();
return true;
}
bool js::StartOffThreadDecodeScript(JSContext* cx,
const ReadOnlyCompileOptions& options,
const JS::TranscodeRange& range,
JS::OffThreadCompileCallback callback,
void* callbackData) {
auto task =
cx->make_unique<ScriptDecodeTask>(cx, range, callback, callbackData);
if (!task || !StartOffThreadParseTask(cx, task.get(), options)) {
return false;
}
Unused << task.release();
return true;
}
bool js::StartOffThreadDecodeMultiScripts(JSContext* cx,
const ReadOnlyCompileOptions& options,
JS::TranscodeSources& sources,
JS::OffThreadCompileCallback callback,
void* callbackData) {
auto task = cx->make_unique<MultiScriptsDecodeTask>(cx, sources, callback,
callbackData);
if (!task || !StartOffThreadParseTask(cx, task.get(), options)) {
return false;
}
Unused << task.release();
return true;
}
#if defined(JS_BUILD_BINAST)
bool js::StartOffThreadDecodeBinAST(JSContext* cx,
const ReadOnlyCompileOptions& options,
const uint8_t* buf, size_t length,
JS::OffThreadCompileCallback callback,
void* callbackData) {
if (!cx->runtime()->binast().ensureBinTablesInitialized(cx)) {
return false;
}
auto task = cx->make_unique<BinASTDecodeTask>(cx, buf, length, callback,
callbackData);
if (!task || !StartOffThreadParseTask(cx, task.get(), options)) {
return false;
}
Unused << task.release();
return true;
}
#endif
void js::EnqueuePendingParseTasksAfterGC(JSRuntime* rt) {
MOZ_ASSERT(!OffThreadParsingMustWaitForGC(rt));
GlobalHelperThreadState::ParseTaskVector newTasks;
{
AutoLockHelperThreadState lock;
GlobalHelperThreadState::ParseTaskVector& waiting =
HelperThreadState().parseWaitingOnGC(lock);
for (size_t i = 0; i < waiting.length(); i++) {
ParseTask* task = waiting[i];
if (task->runtimeMatches(rt)) {
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!newTasks.append(task)) {
oomUnsafe.crash("EnqueuePendingParseTasksAfterGC");
}
HelperThreadState().remove(waiting, &i);
}
}
}
if (newTasks.empty()) {
return;
}
for (size_t i = 0; i < newTasks.length(); i++) {
newTasks[i]->activate(rt);
}
AutoLockHelperThreadState lock;
{
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!HelperThreadState().parseWorklist(lock).appendAll(newTasks)) {
oomUnsafe.crash("EnqueuePendingParseTasksAfterGC");
}
}
HelperThreadState().notifyAll(GlobalHelperThreadState::PRODUCER, lock);
}
#ifdef DEBUG
bool js::CurrentThreadIsParseThread() {
JSContext* cx = TlsContext.get();
return cx->helperThread() && cx->helperThread()->parseTask();
}
#endif
static const uint32_t kDefaultHelperStackSize = 2048 * 1024 - 2 * 4096;
static const uint32_t kDefaultHelperStackQuota = 1800 * 1024;
#if defined(MOZ_TSAN)
static const uint32_t HELPER_STACK_SIZE = 2 * kDefaultHelperStackSize;
static const uint32_t HELPER_STACK_QUOTA = 2 * kDefaultHelperStackQuota;
#else
static const uint32_t HELPER_STACK_SIZE = kDefaultHelperStackSize;
static const uint32_t HELPER_STACK_QUOTA = kDefaultHelperStackQuota;
#endif
bool GlobalHelperThreadState::ensureInitialized() {
MOZ_ASSERT(CanUseExtraThreads());
MOZ_ASSERT(this == &HelperThreadState());
AutoLockHelperThreadState lock;
if (threads) {
return true;
}
threads = js::MakeUnique<HelperThreadVector>();
if (!threads || !threads->initCapacity(threadCount)) {
return false;
}
for (size_t i = 0; i < threadCount; i++) {
threads->infallibleEmplaceBack();
HelperThread& helper = (*threads)[i];
helper.thread = mozilla::Some(
Thread(Thread::Options().setStackSize(HELPER_STACK_SIZE)));
if (!helper.thread->init(HelperThread::ThreadMain, &helper)) {
goto error;
}
continue;
error:
threads->popBack();
finishThreads();
return false;
}
return true;
}
GlobalHelperThreadState::GlobalHelperThreadState()
: cpuCount(0),
threadCount(0),
threads(nullptr),
registerThread(nullptr),
unregisterThread(nullptr),
wasmTier2GeneratorsFinished_(0),
helperLock(mutexid::GlobalHelperThreadState) {
cpuCount = ClampDefaultCPUCount(GetCPUCount());
threadCount = ThreadCountForCPUCount(cpuCount);
MOZ_ASSERT(cpuCount > 0, "GetCPUCount() seems broken");
}
void GlobalHelperThreadState::finish() {
CancelOffThreadWasmTier2Generator();
finishThreads();
AutoLockHelperThreadState lock;
auto& freeList = ionFreeList(lock);
while (!freeList.empty()) {
jit::FreeIonBuilder(freeList.popCopy());
}
}
void GlobalHelperThreadState::finishThreads() {
if (!threads) {
return;
}
MOZ_ASSERT(CanUseExtraThreads());
for (auto& thread : *threads) {
thread.destroy();
}
threads.reset(nullptr);
}
void GlobalHelperThreadState::lock() { helperLock.lock(); }
void GlobalHelperThreadState::unlock() { helperLock.unlock(); }
#ifdef DEBUG
bool GlobalHelperThreadState::isLockedByCurrentThread() const {
return helperLock.ownedByCurrentThread();
}
#endif
void GlobalHelperThreadState::wait(
AutoLockHelperThreadState& locked, CondVar which,
TimeDuration timeout ) {
whichWakeup(which).wait_for(locked, timeout);
}
void GlobalHelperThreadState::notifyAll(CondVar which,
const AutoLockHelperThreadState&) {
whichWakeup(which).notify_all();
}
void GlobalHelperThreadState::notifyOne(CondVar which,
const AutoLockHelperThreadState&) {
whichWakeup(which).notify_one();
}
bool GlobalHelperThreadState::hasActiveThreads(
const AutoLockHelperThreadState&) {
if (!threads) {
return false;
}
for (auto& thread : *threads) {
if (!thread.idle()) {
return true;
}
}
return false;
}
void GlobalHelperThreadState::waitForAllThreads() {
AutoLockHelperThreadState lock;
waitForAllThreadsLocked(lock);
}
void GlobalHelperThreadState::waitForAllThreadsLocked(
AutoLockHelperThreadState& lock) {
CancelOffThreadIonCompileLocked(CompilationSelector(AllCompilations()), false,
lock);
CancelOffThreadWasmTier2GeneratorLocked(lock);
while (hasActiveThreads(lock)) {
wait(lock, CONSUMER);
}
}
template <typename T>
bool GlobalHelperThreadState::checkTaskThreadLimit(size_t maxThreads,
bool isMaster) const {
MOZ_ASSERT(maxThreads > 0);
if (!isMaster && maxThreads >= threadCount) {
return true;
}
size_t count = 0;
size_t idle = 0;
for (auto& thread : *threads) {
if (thread.currentTask.isSome()) {
if (thread.currentTask->is<T>()) {
count++;
}
} else {
idle++;
}
if (count >= maxThreads) {
return false;
}
}
if (idle == 0) {
return false;
}
if (isMaster && idle == 1) {
return false;
}
return true;
}
void GlobalHelperThreadState::triggerFreeUnusedMemory() {
if (!CanUseExtraThreads()) {
return;
}
AutoLockHelperThreadState lock;
for (auto& thread : *threads) {
thread.shouldFreeUnusedMemory = true;
}
notifyAll(PRODUCER, lock);
}
struct MOZ_RAII AutoSetContextRuntime {
explicit AutoSetContextRuntime(JSRuntime* rt) {
TlsContext.get()->setRuntime(rt);
}
~AutoSetContextRuntime() { TlsContext.get()->setRuntime(nullptr); }
};
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, AutoLockHelperThreadState& lock) const {
MOZ_ASSERT(isLockedByCurrentThread());
mozilla::MallocSizeOf mallocSizeOf = stats->mallocSizeOf_;
JS::HelperThreadStats& htStats = stats->helperThread;
htStats.stateData += mallocSizeOf(this);
if (threads) {
htStats.stateData += threads->sizeOfIncludingThis(mallocSizeOf);
}
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) +
parseWorklist_.sizeOfExcludingThis(mallocSizeOf) +
parseFinishedList_.sizeOfExcludingThis(mallocSizeOf) +
parseWaitingOnGC_.sizeOfExcludingThis(mallocSizeOf) +
compressionPendingList_.sizeOfExcludingThis(mallocSizeOf) +
compressionWorklist_.sizeOfExcludingThis(mallocSizeOf) +
compressionFinishedList_.sizeOfExcludingThis(mallocSizeOf) +
gcParallelWorklist_.sizeOfExcludingThis(mallocSizeOf);
for (auto task : parseWorklist_) {
htStats.parseTask += task->sizeOfIncludingThis(mallocSizeOf);
}
for (auto task : parseFinishedList_) {
htStats.parseTask += task->sizeOfIncludingThis(mallocSizeOf);
}
for (auto task : parseWaitingOnGC_) {
htStats.parseTask += task->sizeOfIncludingThis(mallocSizeOf);
}
for (auto builder : ionWorklist_) {
htStats.ionBuilder += builder->sizeOfExcludingThis(mallocSizeOf);
}
for (auto builder : ionFinishedList_) {
htStats.ionBuilder += builder->sizeOfExcludingThis(mallocSizeOf);
}
for (auto builder : ionFreeList_) {
htStats.ionBuilder += builder->sizeOfExcludingThis(mallocSizeOf);
}
for (auto task : wasmWorklist_tier1_) {
htStats.wasmCompile += task->sizeOfExcludingThis(mallocSizeOf);
}
for (auto task : wasmWorklist_tier2_) {
htStats.wasmCompile += task->sizeOfExcludingThis(mallocSizeOf);
}
MOZ_ASSERT(htStats.idleThreadCount == 0);
if (threads) {
for (auto& thread : *threads) {
if (thread.idle()) {
htStats.idleThreadCount++;
} else {
htStats.activeThreadCount++;
}
}
}
}
size_t GlobalHelperThreadState::maxIonCompilationThreads() const {
if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_ION)) {
return 1;
}
return threadCount;
}
size_t GlobalHelperThreadState::maxWasmCompilationThreads() const {
if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_WASM)) {
return 1;
}
return cpuCount;
}
size_t GlobalHelperThreadState::maxWasmTier2GeneratorThreads() const {
return MaxTier2GeneratorTasks;
}
size_t GlobalHelperThreadState::maxPromiseHelperThreads() const {
if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_WASM)) {
return 1;
}
return cpuCount;
}
size_t GlobalHelperThreadState::maxParseThreads() const {
if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_PARSE)) {
return 1;
}
return cpuCount;
}
size_t GlobalHelperThreadState::maxCompressionThreads() const {
if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_COMPRESS)) {
return 1;
}
return 1;
}
size_t GlobalHelperThreadState::maxGCParallelThreads() const {
if (IsHelperThreadSimulatingOOM(js::THREAD_TYPE_GCPARALLEL)) {
return 1;
}
return threadCount;
}
bool GlobalHelperThreadState::canStartWasmTier1Compile(
const AutoLockHelperThreadState& lock) {
return canStartWasmCompile(lock, wasm::CompileMode::Tier1);
}
bool GlobalHelperThreadState::canStartWasmTier2Compile(
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;
}
MOZ_RELEASE_ASSERT(cpuCount > 1);
bool tier2oversubscribed = wasmTier2GeneratorWorklist(lock).length() > 20;
size_t physCoresAvailable = size_t(ceil(cpuCount / 3.0));
size_t threads;
if (mode == wasm::CompileMode::Tier2) {
if (tier2oversubscribed) {
threads = maxWasmCompilationThreads();
} else {
threads = physCoresAvailable;
}
} else {
if (tier2oversubscribed) {
threads = 0;
} else {
threads = maxWasmCompilationThreads();
}
}
if (!threads || !checkTaskThreadLimit<wasm::CompileTask*>(threads)) {
return false;
}
return true;
}
bool GlobalHelperThreadState::canStartWasmTier2Generator(
const AutoLockHelperThreadState& lock) {
return !wasmTier2GeneratorWorklist(lock).empty() &&
checkTaskThreadLimit<wasm::Tier2GeneratorTask*>(
maxWasmTier2GeneratorThreads(),
true);
}
bool GlobalHelperThreadState::canStartPromiseHelperTask(
const AutoLockHelperThreadState& lock) {
return !promiseHelperTasks(lock).empty() &&
checkTaskThreadLimit<PromiseHelperTask*>(maxPromiseHelperThreads(),
true);
}
static bool IonBuilderHasHigherPriority(jit::IonBuilder* first,
jit::IonBuilder* second) {
if (first->optimizationInfo().level() != second->optimizationInfo().level()) {
return first->optimizationInfo().level() <
second->optimizationInfo().level();
}
if (first->scriptHasIonScript() != second->scriptHasIonScript()) {
return !first->scriptHasIonScript();
}
return first->script()->getWarmUpCount() / first->script()->length() >
second->script()->getWarmUpCount() / second->script()->length();
}
bool GlobalHelperThreadState::canStartIonCompile(
const AutoLockHelperThreadState& lock) {
return !ionWorklist(lock).empty() &&
checkTaskThreadLimit<jit::IonBuilder*>(maxIonCompilationThreads());
}
bool GlobalHelperThreadState::canStartIonFreeTask(
const AutoLockHelperThreadState& lock) {
return !ionFreeList(lock).empty();
}
jit::IonBuilder* GlobalHelperThreadState::highestPriorityPendingIonCompile(
const AutoLockHelperThreadState& lock) {
auto& worklist = ionWorklist(lock);
MOZ_ASSERT(!worklist.empty());
size_t index = 0;
for (size_t i = 1; i < worklist.length(); i++) {
if (IonBuilderHasHigherPriority(worklist[i], worklist[index])) {
index = i;
}
}
jit::IonBuilder* builder = worklist[index];
worklist.erase(&worklist[index]);
return builder;
}
bool GlobalHelperThreadState::canStartParseTask(
const AutoLockHelperThreadState& lock) {
return !parseWorklist(lock).empty() &&
checkTaskThreadLimit<ParseTask*>(maxParseThreads(), true);
}
bool GlobalHelperThreadState::canStartCompressionTask(
const AutoLockHelperThreadState& lock) {
return !compressionWorklist(lock).empty() &&
checkTaskThreadLimit<SourceCompressionTask*>(maxCompressionThreads());
}
void GlobalHelperThreadState::startHandlingCompressionTasks(
const AutoLockHelperThreadState& lock) {
scheduleCompressionTasks(lock);
if (canStartCompressionTask(lock)) {
notifyOne(PRODUCER, lock);
}
}
void GlobalHelperThreadState::scheduleCompressionTasks(
const AutoLockHelperThreadState& lock) {
auto& pending = compressionPendingList(lock);
auto& worklist = compressionWorklist(lock);
for (size_t i = 0; i < pending.length(); i++) {
if (pending[i]->shouldStart()) {
Unused << worklist.append(std::move(pending[i]));
remove(pending, &i);
}
}
}
bool GlobalHelperThreadState::canStartGCParallelTask(
const AutoLockHelperThreadState& lock) {
return !gcParallelWorklist(lock).empty() &&
checkTaskThreadLimit<GCParallelTask*>(maxGCParallelThreads());
}
js::GCParallelTask::~GCParallelTask() {
assertNotStarted();
}
bool js::GCParallelTask::startWithLockHeld(AutoLockHelperThreadState& lock) {
MOZ_ASSERT(CanUseExtraThreads());
assertNotStarted();
if (!HelperThreadState().threads) {
return false;
}
if (!HelperThreadState().gcParallelWorklist(lock).append(this)) {
return false;
}
setDispatched(lock);
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
return true;
}
bool js::GCParallelTask::start() {
AutoLockHelperThreadState helperLock;
return startWithLockHeld(helperLock);
}
void js::GCParallelTask::startOrRunIfIdle(AutoLockHelperThreadState& lock) {
if (isRunningWithLockHeld(lock)) {
return;
}
joinWithLockHeld(lock);
if (!startWithLockHeld(lock)) {
AutoUnlockHelperThreadState unlock(lock);
runFromMainThread(runtime());
}
}
void js::GCParallelTask::joinWithLockHeld(AutoLockHelperThreadState& lock) {
if (isNotStarted(lock)) {
return;
}
while (!isFinished(lock)) {
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
setNotStarted(lock);
cancel_ = false;
}
void js::GCParallelTask::join() {
AutoLockHelperThreadState helperLock;
joinWithLockHeld(helperLock);
}
static inline TimeDuration TimeSince(TimeStamp prev) {
TimeStamp now = ReallyNow();
MOZ_ASSERT(now >= prev);
if (now < prev) {
now = prev;
}
return now - prev;
}
void GCParallelTask::joinAndRunFromMainThread(JSRuntime* rt) {
{
AutoLockHelperThreadState lock;
MOZ_ASSERT(!isRunningWithLockHeld(lock));
joinWithLockHeld(lock);
}
runFromMainThread(rt);
}
void js::GCParallelTask::runFromMainThread(JSRuntime* rt) {
assertNotStarted();
MOZ_ASSERT(js::CurrentThreadCanAccessRuntime(rt));
TimeStamp timeStart = ReallyNow();
runTask();
duration_ = TimeSince(timeStart);
}
void js::GCParallelTask::runFromHelperThread(AutoLockHelperThreadState& lock) {
MOZ_ASSERT(isDispatched(lock));
AutoSetContextRuntime ascr(runtime());
gc::AutoSetThreadIsPerformingGC performingGC;
{
AutoUnlockHelperThreadState parallelSection(lock);
TimeStamp timeStart = ReallyNow();
runTask();
duration_ = TimeSince(timeStart);
}
setFinished(lock);
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, lock);
}
bool js::GCParallelTask::isRunning() const {
AutoLockHelperThreadState lock;
return isRunningWithLockHeld(lock);
}
void HelperThread::handleGCParallelWorkload(AutoLockHelperThreadState& lock) {
MOZ_ASSERT(HelperThreadState().canStartGCParallelTask(lock));
MOZ_ASSERT(idle());
TraceLoggerThread* logger = TraceLoggerForCurrentThread();
AutoTraceLog logCompile(logger, TraceLogger_GC);
currentTask.emplace(HelperThreadState().gcParallelWorklist(lock).popCopy());
gcParallelTask()->runFromHelperThread(lock);
currentTask.reset();
}
static void LeaveParseTaskZone(JSRuntime* rt, ParseTask* task) {
rt->clearUsedByHelperThread(task->parseGlobal->zoneFromAnyThread());
}
ParseTask* GlobalHelperThreadState::removeFinishedParseTask(
ParseTaskKind kind, JS::OffThreadToken* token) {
auto task = static_cast<ParseTask*>(token);
MOZ_ASSERT(task->kind == kind);
AutoLockHelperThreadState lock;
#ifdef DEBUG
auto& finished = parseFinishedList(lock);
bool found = false;
for (auto t : finished) {
if (t == task) {
found = true;
break;
}
}
MOZ_ASSERT(found);
#endif
task->remove();
return task;
}
UniquePtr<ParseTask> GlobalHelperThreadState::finishParseTaskCommon(
JSContext* cx, ParseTaskKind kind, JS::OffThreadToken* token) {
MOZ_ASSERT(!cx->helperThread());
MOZ_ASSERT(cx->realm());
Rooted<UniquePtr<ParseTask>> parseTask(cx,
removeFinishedParseTask(kind, token));
if (!EnsureParserCreatedClasses(cx, kind)) {
LeaveParseTaskZone(cx->runtime(), parseTask.get().get());
return nullptr;
}
mergeParseTaskRealm(cx, parseTask.get().get(), cx->realm());
for (auto& script : parseTask->scripts) {
cx->releaseCheck(script);
}
for (auto& sourceObject : parseTask->sourceObjects) {
RootedScriptSourceObject sso(cx, sourceObject);
if (!ScriptSourceObject::initFromOptions(cx, sso, parseTask->options)) {
return nullptr;
}
if (!sso->source()->tryCompressOffThread(cx)) {
return nullptr;
}
}
if (parseTask->outOfMemory) {
ReportOutOfMemory(cx);
return nullptr;
}
for (size_t i = 0; i < parseTask->errors.length(); i++) {
parseTask->errors[i]->throwError(cx);
}
if (parseTask->overRecursed) {
ReportOverRecursed(cx);
}
if (cx->isExceptionPending()) {
return nullptr;
}
return std::move(parseTask.get());
}
JSScript* GlobalHelperThreadState::finishSingleParseTask(
JSContext* cx, ParseTaskKind kind, JS::OffThreadToken* token) {
JS::RootedScript script(cx);
Rooted<UniquePtr<ParseTask>> parseTask(
cx, finishParseTaskCommon(cx, kind, token));
if (!parseTask) {
return nullptr;
}
MOZ_RELEASE_ASSERT(parseTask->scripts.length() <= 1);
if (parseTask->scripts.length() > 0) {
script = parseTask->scripts[0];
}
if (!script) {
MOZ_ASSERT(false, "Expected script");
ReportOutOfMemory(cx);
return nullptr;
}
Debugger::onNewScript(cx, script);
return script;
}
bool GlobalHelperThreadState::finishMultiParseTask(
JSContext* cx, ParseTaskKind kind, JS::OffThreadToken* token,
MutableHandle<ScriptVector> scripts) {
Rooted<UniquePtr<ParseTask>> parseTask(
cx, finishParseTaskCommon(cx, kind, token));
if (!parseTask) {
return false;
}
MOZ_ASSERT(parseTask->kind == ParseTaskKind::MultiScriptsDecode);
auto task = static_cast<MultiScriptsDecodeTask*>(parseTask.get().get());
size_t expectedLength = task->sources->length();
if (!scripts.reserve(parseTask->scripts.length())) {
ReportOutOfMemory(cx);
return false;
}
for (auto& script : parseTask->scripts) {
scripts.infallibleAppend(script);
}
if (scripts.length() != expectedLength) {
MOZ_ASSERT(false, "Expected more scripts");
ReportOutOfMemory(cx);
return false;
}
JS::RootedScript rooted(cx);
for (auto& script : scripts) {
MOZ_ASSERT(script->isGlobalCode());
rooted = script;
Debugger::onNewScript(cx, rooted);
}
return true;
}
JSScript* GlobalHelperThreadState::finishScriptParseTask(
JSContext* cx, JS::OffThreadToken* token) {
JSScript* script = finishSingleParseTask(cx, ParseTaskKind::Script, token);
MOZ_ASSERT_IF(script, script->isGlobalCode());
return script;
}
JSScript* GlobalHelperThreadState::finishScriptDecodeTask(
JSContext* cx, JS::OffThreadToken* token) {
JSScript* script =
finishSingleParseTask(cx, ParseTaskKind::ScriptDecode, token);
MOZ_ASSERT_IF(script, script->isGlobalCode());
return script;
}
#if defined(JS_BUILD_BINAST)
JSScript* GlobalHelperThreadState::finishBinASTDecodeTask(
JSContext* cx, JS::OffThreadToken* token) {
JSScript* script = finishSingleParseTask(cx, ParseTaskKind::BinAST, token);
MOZ_ASSERT_IF(script, script->isGlobalCode());
return script;
}
#endif
bool GlobalHelperThreadState::finishMultiScriptsDecodeTask(
JSContext* cx, JS::OffThreadToken* token,
MutableHandle<ScriptVector> scripts) {
return finishMultiParseTask(cx, ParseTaskKind::MultiScriptsDecode, token,
scripts);
}
JSObject* GlobalHelperThreadState::finishModuleParseTask(
JSContext* cx, JS::OffThreadToken* token) {
JSScript* script = finishSingleParseTask(cx, ParseTaskKind::Module, token);
if (!script) {
return nullptr;
}
MOZ_ASSERT(script->module());
RootedModuleObject module(cx, script->module());
module->fixEnvironmentsAfterCompartmentMerge();
if (!ModuleObject::Freeze(cx, module)) {
return nullptr;
}
return module;
}
void GlobalHelperThreadState::cancelParseTask(JSRuntime* rt, ParseTaskKind kind,
JS::OffThreadToken* token) {
destroyParseTask(rt, removeFinishedParseTask(kind, token));
}
void GlobalHelperThreadState::destroyParseTask(JSRuntime* rt,
ParseTask* parseTask) {
MOZ_ASSERT(!parseTask->isInList());
LeaveParseTaskZone(rt, parseTask);
js_delete(parseTask);
}
void GlobalHelperThreadState::mergeParseTaskRealm(JSContext* cx,
ParseTask* parseTask,
Realm* dest) {
JS::AutoAssertNoGC nogc(cx);
LeaveParseTaskZone(cx->runtime(), parseTask);
gc::MergeRealms(parseTask->parseGlobal->as<GlobalObject>().realm(), dest);
}
void HelperThread::destroy() {
if (thread.isSome()) {
{
AutoLockHelperThreadState lock;
terminate = true;
HelperThreadState().notifyAll(GlobalHelperThreadState::PRODUCER, lock);
}
thread->join();
thread.reset();
}
}
void HelperThread::ensureRegisteredWithProfiler() {
if (profilingStack || mozilla::recordreplay::IsRecordingOrReplaying()) {
return;
}
JS::RegisterThreadCallback callback = HelperThreadState().registerThread;
if (callback) {
profilingStack =
callback("JS Helper", reinterpret_cast<void*>(GetNativeStackBase()));
}
}
void HelperThread::unregisterWithProfilerIfNeeded() {
if (!profilingStack) {
return;
}
JS::UnregisterThreadCallback callback = HelperThreadState().unregisterThread;
if (callback) {
callback();
profilingStack = nullptr;
}
}
void HelperThread::ThreadMain(void* arg) {
ThisThread::SetName("JS Helper");
mozilla::recordreplay::AutoDisallowThreadEvents d;
static_cast<HelperThread*>(arg)->threadLoop();
Mutex::ShutDown();
}
void HelperThread::handleWasmTier1Workload(AutoLockHelperThreadState& locked) {
handleWasmWorkload(locked, wasm::CompileMode::Tier1);
}
void HelperThread::handleWasmTier2Workload(AutoLockHelperThreadState& locked) {
handleWasmWorkload(locked, wasm::CompileMode::Tier2);
}
void HelperThread::handleWasmWorkload(AutoLockHelperThreadState& locked,
wasm::CompileMode mode) {
MOZ_ASSERT(HelperThreadState().canStartWasmCompile(locked, mode));
MOZ_ASSERT(idle());
currentTask.emplace(
HelperThreadState().wasmWorklist(locked, mode).popCopyFront());
wasm::CompileTask* task = wasmTask();
{
AutoUnlockHelperThreadState unlock(locked);
wasm::ExecuteCompileTaskFromHelperThread(task);
}
currentTask.reset();
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked);
}
void HelperThread::handleWasmTier2GeneratorWorkload(
AutoLockHelperThreadState& locked) {
MOZ_ASSERT(HelperThreadState().canStartWasmTier2Generator(locked));
MOZ_ASSERT(idle());
currentTask.emplace(
HelperThreadState().wasmTier2GeneratorWorklist(locked).popCopy());
wasm::Tier2GeneratorTask* task = wasmTier2GeneratorTask();
{
AutoUnlockHelperThreadState unlock(locked);
task->execute();
}
currentTask.reset();
js_delete(task);
HelperThreadState().incWasmTier2GeneratorsFinished(locked);
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked);
}
void HelperThread::handlePromiseHelperTaskWorkload(
AutoLockHelperThreadState& locked) {
MOZ_ASSERT(HelperThreadState().canStartPromiseHelperTask(locked));
MOZ_ASSERT(idle());
PromiseHelperTask* task =
HelperThreadState().promiseHelperTasks(locked).popCopy();
currentTask.emplace(task);
{
AutoUnlockHelperThreadState unlock(locked);
task->execute();
task->dispatchResolveAndDestroy();
}
currentTask.reset();
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked);
}
void HelperThread::handleIonWorkload(AutoLockHelperThreadState& locked) {
MOZ_ASSERT(HelperThreadState().canStartIonCompile(locked));
MOZ_ASSERT(idle());
jit::IonBuilder* builder =
HelperThreadState().highestPriorityPendingIonCompile(locked);
builder->alloc().lifoAlloc()->setReadWrite();
currentTask.emplace(builder);
JSRuntime* rt = builder->script()->runtimeFromAnyThread();
{
AutoUnlockHelperThreadState unlock(locked);
TraceLoggerThread* logger = TraceLoggerForCurrentThread();
TraceLoggerEvent event(TraceLogger_AnnotateScripts, builder->script());
AutoTraceLog logScript(logger, event);
AutoTraceLog logCompile(logger, TraceLogger_IonCompilation);
AutoSetContextRuntime ascr(rt);
jit::JitContext jctx(jit::CompileRuntime::get(rt),
jit::CompileRealm::get(builder->script()->realm()),
&builder->alloc());
builder->setBackgroundCodegen(jit::CompileBackEnd(builder));
}
FinishOffThreadIonCompile(builder, locked);
rt->mainContextFromAnyThread()->requestInterrupt(
InterruptReason::AttachIonCompilations);
currentTask.reset();
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked);
}
void HelperThread::handleIonFreeWorkload(AutoLockHelperThreadState& locked) {
MOZ_ASSERT(idle());
MOZ_ASSERT(HelperThreadState().canStartIonFreeTask(locked));
auto& freeList = HelperThreadState().ionFreeList(locked);
jit::IonBuilder* builder = freeList.popCopy();
{
AutoUnlockHelperThreadState unlock(locked);
FreeIonBuilder(builder);
}
}
HelperThread* js::CurrentHelperThread() {
if (!HelperThreadState().threads) {
return nullptr;
}
auto threadId = ThisThread::GetId();
for (auto& thisThread : *HelperThreadState().threads) {
if (thisThread.thread.isSome() && threadId == thisThread.thread->get_id()) {
return &thisThread;
}
}
return nullptr;
}
bool JSContext::addPendingCompileError(js::CompileError** error) {
auto errorPtr = make_unique<js::CompileError>();
if (!errorPtr) {
return false;
}
ParseTask* parseTask = helperThread()->parseTask();
if (!parseTask->errors.append(std::move(errorPtr))) {
ReportOutOfMemory(this);
return false;
}
*error = parseTask->errors.back().get();
return true;
}
bool JSContext::isCompileErrorPending() const {
ParseTask* parseTask = helperThread()->parseTask();
return parseTask->errors.length() > 0;
}
void JSContext::addPendingOverRecursed() {
if (helperThread()->parseTask()) {
helperThread()->parseTask()->overRecursed = true;
}
}
void JSContext::addPendingOutOfMemory() {
if (helperThread()->parseTask()) {
helperThread()->parseTask()->outOfMemory = true;
}
}
void HelperThread::handleParseWorkload(AutoLockHelperThreadState& locked) {
MOZ_ASSERT(HelperThreadState().canStartParseTask(locked));
MOZ_ASSERT(idle());
currentTask.emplace(HelperThreadState().parseWorklist(locked).popCopy());
ParseTask* task = parseTask();
JSRuntime* runtime = task->parseGlobal->runtimeFromAnyThread();
#ifdef DEBUG
runtime->incOffThreadParsesRunning();
#endif
{
AutoUnlockHelperThreadState unlock(locked);
AutoSetContextRuntime ascr(runtime);
JSContext* cx = TlsContext.get();
Zone* zone = task->parseGlobal->zoneFromAnyThread();
zone->setHelperThreadOwnerContext(cx);
auto resetOwnerContext = mozilla::MakeScopeExit(
[&] { zone->setHelperThreadOwnerContext(nullptr); });
AutoRealm ar(cx, task->parseGlobal);
task->parse(cx);
MOZ_ASSERT(cx->tempLifoAlloc().isEmpty());
cx->tempLifoAlloc().freeAll();
cx->frontendCollectionPool().purge();
cx->atomsZoneFreeLists().clear();
}
task->callback(task, task->callbackData);
HelperThreadState().parseFinishedList(locked).insertBack(task);
#ifdef DEBUG
runtime->decOffThreadParsesRunning();
#endif
currentTask.reset();
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked);
}
void HelperThread::handleCompressionWorkload(
AutoLockHelperThreadState& locked) {
MOZ_ASSERT(HelperThreadState().canStartCompressionTask(locked));
MOZ_ASSERT(idle());
UniquePtr<SourceCompressionTask> task;
{
auto& worklist = HelperThreadState().compressionWorklist(locked);
task = std::move(worklist.back());
worklist.popBack();
currentTask.emplace(task.get());
}
{
AutoUnlockHelperThreadState unlock(locked);
TraceLoggerThread* logger = TraceLoggerForCurrentThread();
AutoTraceLog logCompile(logger, TraceLogger_CompressSource);
task->work();
}
{
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!HelperThreadState().compressionFinishedList(locked).append(
std::move(task))) {
oomUnsafe.crash("handleCompressionWorkload");
}
}
currentTask.reset();
HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked);
}
bool js::EnqueueOffThreadCompression(JSContext* cx,
UniquePtr<SourceCompressionTask> task) {
AutoLockHelperThreadState lock;
auto& pending = HelperThreadState().compressionPendingList(lock);
if (!pending.append(std::move(task))) {
if (!cx->helperThread()) {
ReportOutOfMemory(cx);
}
return false;
}
return true;
}
template <typename T>
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) {
AutoLockHelperThreadState lock;
if (!HelperThreadState().threads) {
return;
}
ClearCompressionTaskList(HelperThreadState().compressionPendingList(lock),
runtime);
ClearCompressionTaskList(HelperThreadState().compressionWorklist(lock),
runtime);
while (true) {
bool inProgress = false;
for (auto& thread : *HelperThreadState().threads) {
SourceCompressionTask* task = thread.compressionTask();
if (task && task->runtimeMatches(runtime)) {
inProgress = true;
}
}
if (!inProgress) {
break;
}
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
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<SourceCompressionTask> compressionTask(std::move(finished[i]));
HelperThreadState().remove(finished, &i);
compressionTask->complete();
}
}
}
void js::RunPendingSourceCompressions(JSRuntime* runtime) {
AutoLockHelperThreadState lock;
if (!HelperThreadState().threads) {
return;
}
HelperThreadState().startHandlingCompressionTasks(lock);
while (!HelperThreadState().compressionWorklist(lock).empty()) {
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
AttachFinishedCompressions(runtime, lock);
}
void PromiseHelperTask::executeAndResolveAndDestroy(JSContext* cx) {
execute();
run(cx, JS::Dispatchable::NotShuttingDown);
}
bool js::StartOffThreadPromiseHelperTask(JSContext* cx,
UniquePtr<PromiseHelperTask> task) {
if (!CanUseExtraThreads()) {
task.release()->executeAndResolveAndDestroy(cx);
return true;
}
AutoLockHelperThreadState lock;
if (!HelperThreadState().promiseHelperTasks(lock).append(task.get())) {
ReportOutOfMemory(cx);
return false;
}
Unused << task.release();
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
return true;
}
bool js::StartOffThreadPromiseHelperTask(PromiseHelperTask* task) {
MOZ_ASSERT(CanUseExtraThreads());
AutoLockHelperThreadState lock;
if (!HelperThreadState().promiseHelperTasks(lock).append(task)) {
return false;
}
HelperThreadState().notifyOne(GlobalHelperThreadState::PRODUCER, lock);
return true;
}
void GlobalHelperThreadState::trace(JSTracer* trc) {
AutoLockHelperThreadState lock;
for (auto builder : ionWorklist(lock)) {
builder->alloc().lifoAlloc()->setReadWrite();
builder->trace(trc);
builder->alloc().lifoAlloc()->setReadOnly();
}
for (auto builder : ionFinishedList(lock)) {
builder->trace(trc);
}
if (HelperThreadState().threads) {
for (auto& helper : *HelperThreadState().threads) {
if (auto builder = helper.ionBuilder()) {
builder->trace(trc);
}
}
}
JSRuntime* rt = trc->runtime();
if (auto* jitRuntime = rt->jitRuntime()) {
jit::IonBuilder* builder = jitRuntime->ionLazyLinkList(rt).getFirst();
while (builder) {
builder->trace(trc);
builder = builder->getNext();
}
}
for (auto parseTask : parseWorklist_) {
parseTask->trace(trc);
}
for (auto parseTask : parseFinishedList_) {
parseTask->trace(trc);
}
for (auto parseTask : parseWaitingOnGC_) {
parseTask->trace(trc);
}
}
void JSContext::setHelperThread(HelperThread* thread) {
if (helperThread_) {
nurserySuppressions_--;
}
helperThread_ = thread;
if (helperThread_) {
nurserySuppressions_++;
}
}
const HelperThread::TaskSpec HelperThread::taskSpecs[] = {
{THREAD_TYPE_GCPARALLEL, &GlobalHelperThreadState::canStartGCParallelTask,
&HelperThread::handleGCParallelWorkload},
{THREAD_TYPE_ION, &GlobalHelperThreadState::canStartIonCompile,
&HelperThread::handleIonWorkload},
{THREAD_TYPE_WASM, &GlobalHelperThreadState::canStartWasmTier1Compile,
&HelperThread::handleWasmTier1Workload},
{THREAD_TYPE_PROMISE_TASK,
&GlobalHelperThreadState::canStartPromiseHelperTask,
&HelperThread::handlePromiseHelperTaskWorkload},
{THREAD_TYPE_PARSE, &GlobalHelperThreadState::canStartParseTask,
&HelperThread::handleParseWorkload},
{THREAD_TYPE_COMPRESS, &GlobalHelperThreadState::canStartCompressionTask,
&HelperThread::handleCompressionWorkload},
{THREAD_TYPE_ION_FREE, &GlobalHelperThreadState::canStartIonFreeTask,
&HelperThread::handleIonFreeWorkload},
{THREAD_TYPE_WASM, &GlobalHelperThreadState::canStartWasmTier2Compile,
&HelperThread::handleWasmTier2Workload},
{THREAD_TYPE_WASM_TIER2,
&GlobalHelperThreadState::canStartWasmTier2Generator,
&HelperThread::handleWasmTier2GeneratorWorkload}};
HelperThread::AutoProfilerLabel::AutoProfilerLabel(
HelperThread* helperThread, const char* label,
JS::ProfilingCategoryPair categoryPair)
: profilingStack(helperThread->profilingStack) {
if (profilingStack) {
profilingStack->pushLabelFrame(label, nullptr, this, categoryPair);
}
}
HelperThread::AutoProfilerLabel::~AutoProfilerLabel() {
if (profilingStack) {
profilingStack->pop();
}
}
void HelperThread::threadLoop() {
MOZ_ASSERT(CanUseExtraThreads());
JS::AutoSuppressGCAnalysis nogc;
AutoLockHelperThreadState lock;
ensureRegisteredWithProfiler();
JSContext cx(nullptr, JS::ContextOptions());
{
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!cx.init(ContextKind::HelperThread)) {
oomUnsafe.crash("HelperThread cx.init()");
}
}
cx.setHelperThread(this);
JS_SetNativeStackQuota(&cx, HELPER_STACK_QUOTA);
while (!terminate) {
MOZ_ASSERT(idle());
maybeFreeUnusedMemory(&cx);
const TaskSpec* task = findHighestPriorityTask(lock);
if (!task) {
AUTO_PROFILER_LABEL("HelperThread::threadLoop::wait", IDLE);
HelperThreadState().wait(lock, GlobalHelperThreadState::PRODUCER);
continue;
}
js::oom::SetThreadType(task->type);
(this->*(task->handleWorkload))(lock);
js::oom::SetThreadType(js::THREAD_TYPE_NONE);
}
unregisterWithProfilerIfNeeded();
}
const HelperThread::TaskSpec* HelperThread::findHighestPriorityTask(
const AutoLockHelperThreadState& locked) {
for (const auto& task : taskSpecs) {
if ((HelperThreadState().*(task.canStart))(locked)) {
return &task;
}
}
return nullptr;
}
void HelperThread::maybeFreeUnusedMemory(JSContext* cx) {
MOZ_ASSERT(idle());
cx->tempLifoAlloc().releaseAll();
if (shouldFreeUnusedMemory) {
cx->tempLifoAlloc().freeAll();
shouldFreeUnusedMemory = false;
}
}