From b0cad39d1a1a7e8ea68830be06ad84a6590092f1 Mon Sep 17 00:00:00 2001 From: wuggy Date: Sat, 18 Apr 2026 15:42:44 -0700 Subject: [PATCH] (not to be confused with Basilisk-Dev's implementation) Backport Bugzilla 1379814 and 1431353. --- js/src/shell/js.cpp | 322 +++++++++++++++++++++++++----------- js/src/vm/HelperThreads.cpp | 99 ++++++----- js/src/vm/HelperThreads.h | 2 +- 3 files changed, 289 insertions(+), 134 deletions(-) diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 2c3d7f7a1d..7209dcaa68 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -16,6 +16,7 @@ #include "mozilla/ScopeExit.h" #include "mozilla/SizePrintfMacros.h" #include "mozilla/Sprintf.h" +#include "mozilla/Variant.h" #include "mozilla/TimeStamp.h" #ifdef XP_WIN @@ -173,90 +174,213 @@ enum class ScriptKind Module }; +mozilla::Atomic gOffThreadJobSerial(1); + +class OffThreadJob; + class OffThreadState { enum State { - IDLE, /* ready to work; no token, no source */ - COMPILING, /* working; no token, have source */ - DONE /* compilation done: have token and source */ + RUNNING, + DONE, }; public: OffThreadState() - : monitor(mutexid::ShellOffThreadState), - state(IDLE), - token(), - source(nullptr) - { } + : monitor(mutexid::ShellOffThreadState) + {} - bool startIfIdle(JSContext* cx, ScriptKind kind, - ScopedJSFreePtr& newSource) + ~OffThreadState() { - AutoLockMonitor alm(monitor); - if (state != IDLE) - return false; - - MOZ_ASSERT(!token); - - source = newSource.forget(); - - scriptKind = kind; - state = COMPILING; - return true; + MOZ_ASSERT(jobs.empty()); } - void abandon(JSContext* cx) { - AutoLockMonitor alm(monitor); - MOZ_ASSERT(state == COMPILING); - MOZ_ASSERT(!token); - MOZ_ASSERT(source); + using Source = JS::UniqueTwoByteChars; - js_free(source); - source = nullptr; + OffThreadJob* newJob(JSContext* cx, ScriptKind kind, Source&& source); + OffThreadJob* getSingleJob(JSContext* cx, ScriptKind kind); + OffThreadJob* lookupJobByID(JSContext* cx, ScriptKind kind, int32_t id); + OffThreadJob* lookupJobForArgs(JSContext* cx, ScriptKind kind, const CallArgs& args, + size_t arg); + void deleteJob(JSContext* cx, OffThreadJob* job); + void cancelAll(JSContext* cx); - state = IDLE; + + private: + friend class OffThreadJob; + + Monitor monitor; + Vector jobs; +}; + +class OffThreadJob { + public: + using Source = OffThreadState::Source; + + OffThreadJob(Monitor& monitor, ScriptKind kind, Source&& source) + : id(gOffThreadJobSerial++), + kind(kind), + monitor(monitor), + state(RUNNING), + token(nullptr), + source(Move(source)) + { + MOZ_RELEASE_ASSERT(id > 0, "Off-thread job IDs exhausted"); + } + + ~OffThreadJob() { + MOZ_ASSERT(state != RUNNING); } void markDone(void* newToken) { AutoLockMonitor alm(monitor); - MOZ_ASSERT(state == COMPILING); + MOZ_ASSERT(state == RUNNING); MOZ_ASSERT(!token); - MOZ_ASSERT(source); MOZ_ASSERT(newToken); token = newToken; state = DONE; - alm.notify(); + alm.notifyAll(); } - void* waitUntilDone(JSContext* cx, ScriptKind kind) { + void* waitUntilDone(JSContext* cx) { AutoLockMonitor alm(monitor); - if (state == IDLE || scriptKind != kind) - return nullptr; + MOZ_ASSERT(state != RUNNING || token == nullptr); - if (state == COMPILING) { - while (state != DONE) - alm.wait(); - } - - MOZ_ASSERT(source); - js_free(source); - source = nullptr; + while (state != DONE) + alm.wait(); MOZ_ASSERT(token); - void* holdToken = token; - token = nullptr; - state = IDLE; - return holdToken; + return token; } + const char16_t* sourceChars() const { + return source.get(); + } + + const int32_t id; + const ScriptKind kind; + private: - Monitor monitor; - ScriptKind scriptKind; + Monitor& monitor; State state; void* token; - char16_t* source; + Source source; }; +OffThreadJob* +OffThreadState::newJob(JSContext* cx, ScriptKind kind, Source&& source) +{ + UniquePtr job(cx->new_(monitor, kind, Move(source))); + if (!job) + return nullptr; + + { + AutoLockMonitor alm(monitor); + if (!jobs.append(job.get())) { + JS_ReportErrorASCII(cx, "OOM adding off-thread job"); + return nullptr; + } + } + + return job.release(); +} + +OffThreadJob* +OffThreadState::getSingleJob(JSContext* cx, ScriptKind kind) +{ + AutoLockMonitor alm(monitor); + if (jobs.empty()) { + JS_ReportErrorASCII(cx, "No off-thread jobs are pending"); + return nullptr; + } + + if (jobs.length() > 1) { + JS_ReportErrorASCII(cx, "Multiple off-thread jobs are pending: must specify job ID"); + return nullptr; + } + + OffThreadJob* job = jobs[0]; + if (job->kind != kind) { + JS_ReportErrorASCII(cx, "Off-thread job is the wrong kind"); + return nullptr; + } + + return job; +} + +OffThreadJob* +OffThreadState::lookupJobByID(JSContext* cx, ScriptKind kind, int32_t id) +{ + if (id <= 0) { + JS_ReportErrorASCII(cx, "Bad off-thread job ID"); + return nullptr; + } + + AutoLockMonitor alm(monitor); + if (jobs.empty()) { + JS_ReportErrorASCII(cx, "No off-thread jobs are pending"); + return nullptr; + } + + OffThreadJob* job = nullptr; + for (auto someJob : jobs) { + if (someJob->id == id) { + job = someJob; + break; + } + } + + if (!job) { + JS_ReportErrorASCII(cx, "Off-thread job not found"); + return nullptr; + } + + if (job->kind != kind) { + JS_ReportErrorASCII(cx, "Off-thread job is the wrong kind"); + return nullptr; + } + + return job; +} + +OffThreadJob* +OffThreadState::lookupJobForArgs(JSContext* cx, ScriptKind kind, const CallArgs& args, + size_t arg) +{ + if (args.length() <= arg) + return getSingleJob(cx, kind); + + int32_t id = 0; + RootedValue value(cx, args[arg]); + if (!ToInt32(cx, value, &id)) + return nullptr; + + return lookupJobByID(cx, kind, id); +} + +void +OffThreadState::deleteJob(JSContext* cx, OffThreadJob* job) +{ + AutoLockMonitor alm(monitor); + for (size_t i = 0; i < jobs.length(); i++) { + if (jobs[i] == job) { + jobs.erase(&jobs[i]); + js_delete(job); + return; + } + } + + MOZ_CRASH("Off-thread job not found"); +} + +void +OffThreadState::cancelAll(JSContext* cx) +{ + AutoLockMonitor alm(monitor); + while (!jobs.empty()) + js_delete(jobs.popCopy()); +} + // Per-context shell state. struct ShellContext { @@ -3437,6 +3561,8 @@ WorkerMain(void* arg) JS::SetEnqueuePromiseJobCallback(cx, nullptr); sc->jobQueue.reset(); + sc->offThreadState.cancelAll(cx); + KillWatchdog(cx); JS_DestroyContext(cx); @@ -4424,8 +4550,8 @@ SyntaxParse(JSContext* cx, unsigned argc, Value* vp) static void OffThreadCompileScriptCallback(void* token, void* callbackData) { - ShellContext* sc = static_cast(callbackData); - sc->offThreadState.markDone(token); + auto job = static_cast(callbackData); + job->markDone(token); } static bool @@ -4484,17 +4610,16 @@ OffThreadCompileScript(JSContext* cx, unsigned argc, Value* vp) // Make sure we own the string's chars, so that they are not freed before // the compilation is finished. - ScopedJSFreePtr ownedChars; + JS::UniqueTwoByteChars ownedChars; if (stableChars.maybeGiveOwnershipToCaller()) { - ownedChars = const_cast(chars); + ownedChars.reset(const_cast(chars)); } else { - char16_t* copy = cx->pod_malloc(length); - if (!copy) + ownedChars.reset(cx->pod_malloc(length)); + if (!ownedChars) return false; - mozilla::PodCopy(copy, chars, length); - ownedChars = copy; - chars = copy; + mozilla::PodCopy(ownedChars.get(), chars, length); + chars = ownedChars.get(); } if (!JS::CanCompileOffThread(cx, options, length)) { @@ -4503,20 +4628,18 @@ OffThreadCompileScript(JSContext* cx, unsigned argc, Value* vp) } ShellContext* sc = GetShellContext(cx); - if (!sc->offThreadState.startIfIdle(cx, ScriptKind::Script, ownedChars)) { - JS_ReportErrorASCII(cx, "called offThreadCompileScript without calling runOffThreadScript" - " to receive prior off-thread compilation"); + OffThreadJob* job = sc->offThreadState.newJob(cx, ScriptKind::Script, Move(ownedChars)); + if (!job) return false; - } if (!JS::CompileOffThread(cx, options, chars, length, - OffThreadCompileScriptCallback, sc)) + OffThreadCompileScriptCallback, job)) { - sc->offThreadState.abandon(cx); + sc->offThreadState.deleteJob(cx, job); return false; } - args.rval().setUndefined(); + args.rval().setInt32(job->id); return true; } @@ -4529,11 +4652,14 @@ runOffThreadScript(JSContext* cx, unsigned argc, Value* vp) gc::FinishGC(cx); ShellContext* sc = GetShellContext(cx); - void* token = sc->offThreadState.waitUntilDone(cx, ScriptKind::Script); - if (!token) { - JS_ReportErrorASCII(cx, "called runOffThreadScript when no compilation is pending"); + OffThreadJob* job = sc->offThreadState.lookupJobForArgs(cx, ScriptKind::Script, args, 0); + if (!job) return false; - } + + void* token = job->waitUntilDone(cx); + MOZ_ASSERT(token); + + sc->offThreadState.deleteJob(cx, job); RootedScript script(cx, JS::FinishOffThreadScript(cx, token)); if (!script) @@ -4571,17 +4697,16 @@ OffThreadCompileModule(JSContext* cx, unsigned argc, Value* vp) // Make sure we own the string's chars, so that they are not freed before // the compilation is finished. - ScopedJSFreePtr ownedChars; + JS::UniqueTwoByteChars ownedChars; if (stableChars.maybeGiveOwnershipToCaller()) { - ownedChars = const_cast(chars); + ownedChars.reset(const_cast(chars)); } else { - char16_t* copy = cx->pod_malloc(length); - if (!copy) + ownedChars.reset(cx->pod_malloc(length)); + if (!ownedChars) return false; - mozilla::PodCopy(copy, chars, length); - ownedChars = copy; - chars = copy; + mozilla::PodCopy(ownedChars.get(), chars, length); + chars = ownedChars.get(); } if (!JS::CanCompileOffThread(cx, options, length)) { @@ -4590,20 +4715,18 @@ OffThreadCompileModule(JSContext* cx, unsigned argc, Value* vp) } ShellContext* sc = GetShellContext(cx); - if (!sc->offThreadState.startIfIdle(cx, ScriptKind::Module, ownedChars)) { - JS_ReportErrorASCII(cx, "called offThreadCompileModule without receiving prior off-thread " - "compilation"); + OffThreadJob* job = sc->offThreadState.newJob(cx, ScriptKind::Module, Move(ownedChars)); + if (!job) return false; - } if (!JS::CompileOffThreadModule(cx, options, chars, length, - OffThreadCompileScriptCallback, sc)) + OffThreadCompileScriptCallback, job)) { - sc->offThreadState.abandon(cx); + sc->offThreadState.deleteJob(cx, job); return false; } - args.rval().setUndefined(); + args.rval().setInt32(job->id); return true; } @@ -4616,11 +4739,14 @@ FinishOffThreadModule(JSContext* cx, unsigned argc, Value* vp) gc::FinishGC(cx); ShellContext* sc = GetShellContext(cx); - void* token = sc->offThreadState.waitUntilDone(cx, ScriptKind::Module); - if (!token) { - JS_ReportErrorASCII(cx, "called finishOffThreadModule when no compilation is pending"); + OffThreadJob* job = sc->offThreadState.lookupJobForArgs(cx, ScriptKind::Module, args, 0); + if (!job) return false; - } + + void* token = job->waitUntilDone(cx); + MOZ_ASSERT(token); + + sc->offThreadState.deleteJob(cx, job); RootedObject module(cx, JS::FinishOffThreadModule(cx, token)); if (!module) @@ -6237,7 +6363,8 @@ static const JSFunctionSpecWithHelp shell_functions[] = { JS_FN_HELP("offThreadCompileScript", OffThreadCompileScript, 1, 0, "offThreadCompileScript(code[, options])", " Compile |code| on a helper thread. To wait for the compilation to finish\n" -" and run the code, call |runOffThreadScript|. If present, |options| may\n" +" and run the code, call |runOffThreadScript|. The function returns a job ID.\n" +" If present, |options| may\n" " have properties saying how the code should be compiled:\n" " noScriptRval: use the no-script-rval compiler option (default: false)\n" " fileName: filename for error messages and debug info\n" @@ -6252,19 +6379,22 @@ static const JSFunctionSpecWithHelp shell_functions[] = { " Debugger.Source.prototype.elementAttributeName returns.\n"), JS_FN_HELP("runOffThreadScript", runOffThreadScript, 0, 0, -"runOffThreadScript()", -" Wait for off-thread compilation to complete. If an error occurred,\n" +"runOffThreadScript([jobID])", +" Wait for an off-thread compilation job to complete. The job ID can be\n" +" omitted if there is only one job pending. If an error occurred,\n" " throw the appropriate exception; otherwise, run the script and return\n" " its value."), JS_FN_HELP("offThreadCompileModule", OffThreadCompileModule, 1, 0, "offThreadCompileModule(code)", -" Compile |code| on a helper thread. To wait for the compilation to finish\n" -" and get the module object, call |finishOffThreadModule|."), +" Compile |code| on a helper thread, returning a job ID. To wait for the\n" +" compilation to finish and get the module object, call |finishOffThreadModule|\n" +" passing the job ID."), JS_FN_HELP("finishOffThreadModule", FinishOffThreadModule, 0, 0, -"finishOffThreadModule()", -" Wait for off-thread compilation to complete. If an error occurred,\n" +"finishOffThreadModule([jobID])", +" Wait for an off-thread compilation job to complete. The job ID can be\n" +" omitted if there is only one job pending. If an error occurred,\n" " throw the appropriate exception; otherwise, return the module object"), JS_FN_HELP("timeout", Timeout, 1, 0, @@ -8308,6 +8438,8 @@ main(int argc, char** argv, char** envp) DestructSharedArrayBufferMailbox(); + sc->offThreadState.cancelAll(cx); + JS_DestroyContext(cx); JS_ShutDown(); return result; diff --git a/js/src/vm/HelperThreads.cpp b/js/src/vm/HelperThreads.cpp index b264b3bb9c..f0996d404e 100644 --- a/js/src/vm/HelperThreads.cpp +++ b/js/src/vm/HelperThreads.cpp @@ -860,19 +860,31 @@ GlobalHelperThreadState::waitForAllThreads() template bool -GlobalHelperThreadState::checkTaskThreadLimit(size_t maxThreads) const +GlobalHelperThreadState::checkTaskThreadLimit(size_t maxThreads, bool isMaster) const { + MOZ_ASSERT(maxThreads > 0); + if (maxThreads >= threadCount) return true; size_t count = 0; + size_t idle = 0; for (auto& thread : *threads) { - if (thread.currentTask.isSome() && thread.currentTask->is()) - count++; + if (thread.currentTask.isSome()) { + if (thread.currentTask->is()) + count++; + } else { + idle++; + } if (count >= maxThreads) return false; } + MOZ_ASSERT(idle > 0); + + if (isMaster && idle <= 1) + return false; + return true; } @@ -1091,7 +1103,11 @@ GlobalHelperThreadState::pendingIonCompileHasSufficientPriority( bool GlobalHelperThreadState::canStartParseTask(const AutoLockHelperThreadState& lock) { - return !parseWorklist(lock).empty() && checkTaskThreadLimit(maxParseThreads()); + // Parse tasks that end up compiling asm.js may use Wasm compilation threads + // to generate machine code. We do not know ahead of time which parse tasks + // will need those resources, so conservatively treat them as master tasks. + return !parseWorklist(lock).empty() && + checkTaskThreadLimit(maxParseThreads(), /*isMaster=*/true); } bool @@ -1892,49 +1908,56 @@ HelperThread::threadLoop() while (true) { MOZ_ASSERT(idle()); - // Block until a task is available. Save the value of whether we are - // going to do an Ion compile, in case the value returned by the method - // changes. - bool ionCompile = false; + js::oom::ThreadType task = js::oom::THREAD_TYPE_NONE; while (true) { if (terminate) return; - if ((ionCompile = HelperThreadState().pendingIonCompileHasSufficientPriority(lock)) || - HelperThreadState().canStartWasmCompile(lock) || - HelperThreadState().canStartPromiseTask(lock) || - HelperThreadState().canStartParseTask(lock) || - HelperThreadState().canStartCompressionTask(lock) || - HelperThreadState().canStartGCHelperTask(lock) || - HelperThreadState().canStartGCParallelTask(lock)) - { + if (HelperThreadState().canStartGCParallelTask(lock)) + task = js::oom::THREAD_TYPE_GCPARALLEL; + else if (HelperThreadState().canStartGCHelperTask(lock)) + task = js::oom::THREAD_TYPE_GCHELPER; + else if (HelperThreadState().pendingIonCompileHasSufficientPriority(lock)) + task = js::oom::THREAD_TYPE_ION; + else if (HelperThreadState().canStartWasmCompile(lock)) + task = js::oom::THREAD_TYPE_ASMJS; + else if (HelperThreadState().canStartPromiseTask(lock)) + task = js::oom::THREAD_TYPE_PROMISE_TASK; + else if (HelperThreadState().canStartParseTask(lock)) + task = js::oom::THREAD_TYPE_PARSE; + else if (HelperThreadState().canStartCompressionTask(lock)) + task = js::oom::THREAD_TYPE_COMPRESS; + + if (task != js::oom::THREAD_TYPE_NONE) break; - } HelperThreadState().wait(lock, GlobalHelperThreadState::PRODUCER); } - if (ionCompile) { - js::oom::SetThreadType(js::oom::THREAD_TYPE_ION); - handleIonWorkload(lock); - } else if (HelperThreadState().canStartWasmCompile(lock)) { - js::oom::SetThreadType(js::oom::THREAD_TYPE_ASMJS); - handleWasmWorkload(lock); - } else if (HelperThreadState().canStartPromiseTask(lock)) { - js::oom::SetThreadType(js::oom::THREAD_TYPE_PROMISE_TASK); - handlePromiseTaskWorkload(lock); - } else if (HelperThreadState().canStartParseTask(lock)) { - js::oom::SetThreadType(js::oom::THREAD_TYPE_PARSE); - handleParseWorkload(lock, stackLimit); - } else if (HelperThreadState().canStartCompressionTask(lock)) { - js::oom::SetThreadType(js::oom::THREAD_TYPE_COMPRESS); - handleCompressionWorkload(lock); - } else if (HelperThreadState().canStartGCHelperTask(lock)) { - js::oom::SetThreadType(js::oom::THREAD_TYPE_GCHELPER); - handleGCHelperWorkload(lock); - } else if (HelperThreadState().canStartGCParallelTask(lock)) { - js::oom::SetThreadType(js::oom::THREAD_TYPE_GCPARALLEL); + js::oom::SetThreadType(task); + switch (task) { + case js::oom::THREAD_TYPE_GCPARALLEL: handleGCParallelWorkload(lock); - } else { + break; + case js::oom::THREAD_TYPE_GCHELPER: + handleGCHelperWorkload(lock); + break; + case js::oom::THREAD_TYPE_ION: + handleIonWorkload(lock); + break; + case js::oom::THREAD_TYPE_ASMJS: + handleWasmWorkload(lock); + break; + case js::oom::THREAD_TYPE_PROMISE_TASK: + handlePromiseTaskWorkload(lock); + break; + case js::oom::THREAD_TYPE_PARSE: + handleParseWorkload(lock, stackLimit); + break; + case js::oom::THREAD_TYPE_COMPRESS: + handleCompressionWorkload(lock); + break; + default: MOZ_CRASH("No task to perform"); } + js::oom::SetThreadType(js::oom::THREAD_TYPE_NONE); } } diff --git a/js/src/vm/HelperThreads.h b/js/src/vm/HelperThreads.h index ebd4c9b9cb..5a1cc02e9a 100644 --- a/js/src/vm/HelperThreads.h +++ b/js/src/vm/HelperThreads.h @@ -256,7 +256,7 @@ class GlobalHelperThreadState void waitForAllThreads(); template - bool checkTaskThreadLimit(size_t maxThreads) const; + bool checkTaskThreadLimit(size_t maxThreads, bool isMaster = false) const; private: