(not to be confused with Basilisk-Dev's implementation) Backport Bugzilla 1379814 and 1431353.

This commit is contained in:
wuggy 2026-04-18 15:42:44 -07:00
commit b0cad39d1a
3 changed files with 283 additions and 128 deletions

View file

@ -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<int32_t> 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<char16_t>& 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<OffThreadJob*, 0, SystemAllocPolicy> 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<OffThreadJob> job(cx->new_<OffThreadJob>(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<ShellContext*>(callbackData);
sc->offThreadState.markDone(token);
auto job = static_cast<OffThreadJob*>(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<char16_t> ownedChars;
JS::UniqueTwoByteChars ownedChars;
if (stableChars.maybeGiveOwnershipToCaller()) {
ownedChars = const_cast<char16_t*>(chars);
ownedChars.reset(const_cast<char16_t*>(chars));
} else {
char16_t* copy = cx->pod_malloc<char16_t>(length);
if (!copy)
ownedChars.reset(cx->pod_malloc<char16_t>(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<char16_t> ownedChars;
JS::UniqueTwoByteChars ownedChars;
if (stableChars.maybeGiveOwnershipToCaller()) {
ownedChars = const_cast<char16_t*>(chars);
ownedChars.reset(const_cast<char16_t*>(chars));
} else {
char16_t* copy = cx->pod_malloc<char16_t>(length);
if (!copy)
ownedChars.reset(cx->pod_malloc<char16_t>(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;

View file

@ -860,19 +860,31 @@ GlobalHelperThreadState::waitForAllThreads()
template <typename T>
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<T>())
count++;
if (thread.currentTask.isSome()) {
if (thread.currentTask->is<T>())
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<ParseTask*>(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<ParseTask*>(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);
}
}

View file

@ -256,7 +256,7 @@ class GlobalHelperThreadState
void waitForAllThreads();
template <typename T>
bool checkTaskThreadLimit(size_t maxThreads) const;
bool checkTaskThreadLimit(size_t maxThreads, bool isMaster = false) const;
private: