Bug 1357958 - Move the JS shell's Promise job handling into the engine to be used as a default implementation. r=jandem

The shell has a very basic implementation of Promise job queue handling. This patch moves it into the engine, exposed through friendapi functions. The motivation is that I want to write JSAPI tests for streams, which requires Promise handling. The test harness would need essentially a copy of the shell's Promise handling, which isn't nice.

To be clear, the default implementation isn't used automatically: the embedding has to explicitly request it using js::UseInternalJobQueues.

MozReview-Commit-ID: 6bZ5VG5mJKV
This commit is contained in:
Till Schneidereit 2017-05-10 16:16:27 +02:00
parent cdc033857b
commit be699bc7bf
10 changed files with 355 additions and 148 deletions

View File

@ -0,0 +1,4 @@
Promise.resolve()
.then(()=>quit(0));
Promise.resolve()
.then(()=>crash("Must not run any more promise jobs after quitting"));

View File

@ -89,3 +89,81 @@ BEGIN_TEST(testPromise_RejectPromise)
return true;
}
END_TEST(testPromise_RejectPromise)
static bool thenHandler_called = false;
static bool
PromiseThenHandler(JSContext* cx, unsigned argc, Value* vp)
{
#ifdef DEBUG
CallArgs args = CallArgsFromVp(argc, vp);
#endif // DEBUG
MOZ_ASSERT(args.length() == 1);
thenHandler_called = true;
return true;
}
static bool catchHandler_called = false;
static bool
PromiseCatchHandler(JSContext* cx, unsigned argc, Value* vp)
{
#ifdef DEBUG
CallArgs args = CallArgsFromVp(argc, vp);
#endif // DEBUG
MOZ_ASSERT(args.length() == 1);
catchHandler_called = true;
return true;
}
BEGIN_TEST(testPromise_PromiseThen)
{
RootedObject promise(cx, CreatePromise(cx));
if (!promise)
return false;
RootedFunction thenHandler(cx, JS_NewFunction(cx, PromiseThenHandler, 1, 0, "thenHandler"));
if (!thenHandler)
return false;
RootedFunction catchHandler(cx, JS_NewFunction(cx, PromiseCatchHandler, 1, 0, "catchHandler"));
if (!catchHandler)
return false;
JS::AddPromiseReactions(cx, promise, thenHandler, catchHandler);
RootedValue result(cx);
result.setInt32(42);
JS::ResolvePromise(cx, promise, result);
js::RunJobs(cx);
CHECK(thenHandler_called);
return true;
}
END_TEST(testPromise_PromiseThen)
BEGIN_TEST(testPromise_PromiseCatch)
{
RootedObject promise(cx, CreatePromise(cx));
if (!promise)
return false;
RootedFunction thenHandler(cx, JS_NewFunction(cx, PromiseThenHandler, 1, 0, "thenHandler"));
if (!thenHandler)
return false;
RootedFunction catchHandler(cx, JS_NewFunction(cx, PromiseCatchHandler, 1, 0, "catchHandler"));
if (!catchHandler)
return false;
JS::AddPromiseReactions(cx, promise, thenHandler, catchHandler);
RootedValue result(cx);
result.setInt32(42);
JS::RejectPromise(cx, promise, result);
js::RunJobs(cx);
CHECK(catchHandler_called);
return true;
}
END_TEST(testPromise_PromiseCatch)

View File

@ -18,6 +18,7 @@ bool JSAPITest::init()
cx = createContext();
if (!cx)
return false;
js::UseInternalJobQueues(cx);
if (!JS::InitSelfHostedCode(cx))
return false;
JS_BeginRequest(cx);

View File

@ -208,6 +208,20 @@ js::ResumeCooperativeContext(JSContext* cx)
cx->runtime()->setActiveContext(cx);
}
static void
FreeJobQueueHandling(JSContext* cx)
{
if (!cx->jobQueue)
return;
cx->jobQueue->reset();
FreeOp* fop = cx->defaultFreeOp();
fop->delete_(cx->jobQueue.ref());
cx->getIncumbentGlobalCallback = nullptr;
cx->enqueuePromiseJobCallback = nullptr;
cx->enqueuePromiseJobCallbackData = nullptr;
}
void
js::DestroyContext(JSContext* cx)
{
@ -224,6 +238,8 @@ js::DestroyContext(JSContext* cx)
// zone group. See HelperThread::handleIonWorkload.
CancelOffThreadIonCompile(cx->runtime());
FreeJobQueueHandling(cx);
if (cx->runtime()->cooperatingContexts().length() == 1) {
// Destroy the runtime along with its last context.
cx->runtime()->destroyRuntime();
@ -1094,6 +1110,184 @@ JSContext::recoverFromOutOfMemory()
}
}
static bool
InternalEnqueuePromiseJobCallback(JSContext* cx, JS::HandleObject job,
JS::HandleObject allocationSite,
JS::HandleObject incumbentGlobal, void* data)
{
MOZ_ASSERT(job);
return cx->jobQueue->append(job);
}
static bool
InternalStartAsyncTaskCallback(JSContext* cx, JS::AsyncTask* task)
{
task->user = cx;
ExclusiveData<InternalAsyncTasks>::Guard asyncTasks = cx->asyncTasks.lock();
asyncTasks->outstanding++;
return true;
}
static bool
InternalFinishAsyncTaskCallback(JS::AsyncTask* task)
{
JSContext* cx = (JSContext*)task->user;
ExclusiveData<InternalAsyncTasks>::Guard asyncTasks = cx->asyncTasks.lock();
MOZ_ASSERT(asyncTasks->outstanding > 0);
asyncTasks->outstanding--;
return asyncTasks->finished.append(task);
}
namespace {
class MOZ_STACK_CLASS ReportExceptionClosure : public ScriptEnvironmentPreparer::Closure
{
public:
explicit ReportExceptionClosure(HandleValue exn)
: exn_(exn)
{
}
bool operator()(JSContext* cx) override
{
cx->setPendingException(exn_);
return false;
}
private:
HandleValue exn_;
};
} // anonymous namespace
JS_FRIEND_API(bool)
js::UseInternalJobQueues(JSContext* cx)
{
// Internal job queue handling must be set up very early. Self-hosting
// initialization is as good a marker for that as any.
MOZ_RELEASE_ASSERT(!cx->runtime()->hasInitializedSelfHosting(),
"js::UseInternalJobQueues must be called early during runtime startup.");
MOZ_ASSERT(!cx->jobQueue);
auto* queue = cx->new_<PersistentRooted<JobQueue>>(cx, JobQueue(SystemAllocPolicy()));
if (!queue)
return false;
cx->jobQueue = queue;
JS::SetEnqueuePromiseJobCallback(cx, InternalEnqueuePromiseJobCallback);
JS::SetAsyncTaskCallbacks(cx, InternalStartAsyncTaskCallback, InternalFinishAsyncTaskCallback);
return true;
}
JS_FRIEND_API(void)
js::StopDrainingJobQueue(JSContext* cx)
{
MOZ_ASSERT(cx->jobQueue);
cx->stopDrainingJobQueue = true;
}
JS_FRIEND_API(void)
js::RunJobs(JSContext* cx)
{
MOZ_ASSERT(cx->jobQueue);
if (cx->drainingJobQueue || cx->stopDrainingJobQueue)
return;
while (true) {
// Wait for any outstanding async tasks to finish so that the
// finishedAsyncTasks list is fixed.
while (true) {
AutoLockHelperThreadState lock;
if (!cx->asyncTasks.lock()->outstanding)
break;
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
// Lock the whole time while copying back the asyncTasks finished queue
// so that any new tasks created during finish() cannot racily join the
// job queue. Call finish() only thereafter, to avoid a circular mutex
// dependency (see also bug 1297901).
Vector<JS::AsyncTask*, 0, SystemAllocPolicy> finished;
{
ExclusiveData<InternalAsyncTasks>::Guard asyncTasks = cx->asyncTasks.lock();
finished = Move(asyncTasks->finished);
asyncTasks->finished.clear();
}
for (JS::AsyncTask* task : finished)
task->finish(cx);
// It doesn't make sense for job queue draining to be reentrant. At the
// same time we don't want to assert against it, because that'd make
// drainJobQueue unsafe for fuzzers. We do want fuzzers to test this,
// so we simply ignore nested calls of drainJobQueue.
cx->drainingJobQueue = true;
RootedObject job(cx);
JS::HandleValueArray args(JS::HandleValueArray::empty());
RootedValue rval(cx);
// Execute jobs in a loop until we've reached the end of the queue.
// Since executing a job can trigger enqueuing of additional jobs,
// it's crucial to re-check the queue length during each iteration.
for (size_t i = 0; i < cx->jobQueue->length(); i++) {
// A previous job might have set this flag. E.g., the js shell
// sets it if the `quit` builtin function is called.
if (cx->stopDrainingJobQueue)
break;
job = cx->jobQueue->get()[i];
// It's possible that queue draining was interrupted prematurely,
// leaving the queue partly processed. In that case, slots for
// already-executed entries will contain nullptrs, which we should
// just skip.
if (!job)
continue;
cx->jobQueue->get()[i] = nullptr;
AutoCompartment ac(cx, job);
{
if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval)) {
// Nothing we can do about uncatchable exceptions.
if (!cx->isExceptionPending())
continue;
RootedValue exn(cx);
if (cx->getPendingException(&exn)) {
/*
* Clear the exception, because
* PrepareScriptEnvironmentAndInvoke will assert that we don't
* have one.
*/
cx->clearPendingException();
ReportExceptionClosure reportExn(exn);
PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
}
}
}
}
cx->drainingJobQueue = false;
if (cx->stopDrainingJobQueue) {
cx->stopDrainingJobQueue = false;
break;
}
cx->jobQueue->clear();
// It's possible a job added an async task, and it's also possible
// that task has already finished.
{
ExclusiveData<InternalAsyncTasks>::Guard asyncTasks = cx->asyncTasks.lock();
if (asyncTasks->outstanding == 0 && asyncTasks->finished.length() == 0)
break;
}
}
}
JS::Error JSContext::reportedError;
JS::OOM JSContext::reportedOOM;
@ -1204,6 +1398,10 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options)
getIncumbentGlobalCallback(nullptr),
enqueuePromiseJobCallback(nullptr),
enqueuePromiseJobCallbackData(nullptr),
jobQueue(nullptr),
drainingJobQueue(false),
stopDrainingJobQueue(false),
asyncTasks(mutexid::InternalAsyncTasks),
promiseRejectionTrackerCallback(nullptr),
promiseRejectionTrackerCallbackData(nullptr)
{

View File

@ -68,6 +68,25 @@ struct AutoResolving;
struct HelperThread;
using JobQueue = GCVector<JSObject*, 0, SystemAllocPolicy>;
class AutoLockForExclusiveAccess;
/*
* Used for engine-internal handling of async tasks, as currently
* enabled in the js shell and jsapi tests.
*/
struct InternalAsyncTasks
{
explicit InternalAsyncTasks()
: outstanding(0),
finished()
{}
size_t outstanding;
Vector<JS::AsyncTask*, 0, SystemAllocPolicy> finished;
};
void ReportOverRecursed(JSContext* cx, unsigned errorNumber);
/* Thread Local Storage slot for storing the context for a thread. */
@ -907,6 +926,14 @@ struct JSContext : public JS::RootingContext,
js::ThreadLocalData<JSEnqueuePromiseJobCallback> enqueuePromiseJobCallback;
js::ThreadLocalData<void*> enqueuePromiseJobCallbackData;
// Queue of pending jobs as described in ES2016 section 8.4.
// Only used if internal job queue handling was activated using
// `js::UseInternalJobQueues`.
js::ThreadLocalData<JS::PersistentRooted<js::JobQueue>*> jobQueue;
js::ThreadLocalData<bool> drainingJobQueue;
js::ThreadLocalData<bool> stopDrainingJobQueue;
js::ExclusiveData<js::InternalAsyncTasks> asyncTasks;
js::ThreadLocalData<JSPromiseRejectionTrackerCallback> promiseRejectionTrackerCallback;
js::ThreadLocalData<void*> promiseRejectionTrackerCallbackData;

View File

@ -382,6 +382,33 @@ SetSourceHook(JSContext* cx, mozilla::UniquePtr<SourceHook> hook);
extern JS_FRIEND_API(mozilla::UniquePtr<SourceHook>)
ForgetSourceHook(JSContext* cx);
/**
* Use the runtime's internal handling of job queues for Promise jobs.
*
* Most embeddings, notably web browsers, will have their own task scheduling
* systems and need to integrate handling of Promise jobs into that, so they
* will want to manage job queues themselves. For basic embeddings such as the
* JS shell that don't have an event loop of their own, it's easier to have
* SpiderMonkey handle job queues internally.
*
* Note that the embedding still has to trigger processing of job queues at
* right time(s), such as after evaluation of a script has run to completion.
*/
extern JS_FRIEND_API(bool)
UseInternalJobQueues(JSContext* cx);
/**
* Instruct the runtime to stop draining the internal job queue.
*
* Useful if the embedding is in the process of quitting in reaction to a
* builtin being called, or if it wants to resume executing jobs later on.
*/
extern JS_FRIEND_API(void)
StopDrainingJobQueue(JSContext* cx);
extern JS_FRIEND_API(void)
RunJobs(JSContext* cx);
extern JS_FRIEND_API(JS::Zone*)
GetCompartmentZone(JSCompartment* comp);

View File

@ -157,19 +157,6 @@ static const TimeDuration MAX_TIMEOUT_INTERVAL = TimeDuration::FromSeconds(1800.
# define SINGLESTEP_PROFILING
#endif
using JobQueue = GCVector<JSObject*, 0, SystemAllocPolicy>;
struct ShellAsyncTasks
{
explicit ShellAsyncTasks(JSContext* cx)
: outstanding(0),
finished(cx)
{}
size_t outstanding;
Vector<JS::AsyncTask*> finished;
};
enum class ScriptKind
{
Script,
@ -318,9 +305,6 @@ struct ShellContext
bool lastWarningEnabled;
JS::PersistentRootedValue lastWarning;
JS::PersistentRootedValue promiseRejectionTrackerCallback;
JS::PersistentRooted<JobQueue> jobQueue;
ExclusiveData<ShellAsyncTasks> asyncTasks;
bool drainingJobQueue;
#ifdef SINGLESTEP_PROFILING
Vector<StackChars, 0, SystemAllocPolicy> stacks;
#endif
@ -488,8 +472,6 @@ ShellContext::ShellContext(JSContext* cx)
lastWarningEnabled(false),
lastWarning(cx, NullValue()),
promiseRejectionTrackerCallback(cx, NullValue()),
asyncTasks(mutexid::ShellAsyncTasks, cx),
drainingJobQueue(false),
watchdogLock(mutexid::ShellContextWatchdog),
exitCode(0),
quitting(false),
@ -816,115 +798,13 @@ RunModule(JSContext* cx, const char* filename, FILE* file, bool compileOnly)
return JS_CallFunction(cx, loaderObj, importFun, args, &value);
}
static JSObject*
ShellGetIncumbentGlobalCallback(JSContext* cx)
{
return JS::CurrentGlobalOrNull(cx);
}
static bool
ShellEnqueuePromiseJobCallback(JSContext* cx, JS::HandleObject job, JS::HandleObject allocationSite,
JS::HandleObject incumbentGlobal, void* data)
{
ShellContext* sc = GetShellContext(cx);
MOZ_ASSERT(job);
return sc->jobQueue.append(job);
}
static bool
ShellStartAsyncTaskCallback(JSContext* cx, JS::AsyncTask* task)
{
ShellContext* sc = GetShellContext(cx);
task->user = sc;
ExclusiveData<ShellAsyncTasks>::Guard asyncTasks = sc->asyncTasks.lock();
asyncTasks->outstanding++;
return true;
}
static bool
ShellFinishAsyncTaskCallback(JS::AsyncTask* task)
{
ShellContext* sc = (ShellContext*)task->user;
ExclusiveData<ShellAsyncTasks>::Guard asyncTasks = sc->asyncTasks.lock();
MOZ_ASSERT(asyncTasks->outstanding > 0);
asyncTasks->outstanding--;
return asyncTasks->finished.append(task);
}
static void
DrainJobQueue(JSContext* cx)
{
ShellContext* sc = GetShellContext(cx);
if (sc->quitting || sc->drainingJobQueue)
return;
while (true) {
// Wait for any outstanding async tasks to finish so that the
// finishedAsyncTasks list is fixed.
while (true) {
AutoLockHelperThreadState lock;
if (!sc->asyncTasks.lock()->outstanding)
break;
HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER);
}
// Lock the whole time while copying back the asyncTasks finished queue
// so that any new tasks created during finish() cannot racily join the
// job queue. Call finish() only thereafter, to avoid a circular mutex
// dependency (see also bug 1297901).
Vector<JS::AsyncTask*> finished(cx);
{
ExclusiveData<ShellAsyncTasks>::Guard asyncTasks = sc->asyncTasks.lock();
finished = Move(asyncTasks->finished);
asyncTasks->finished.clear();
}
for (JS::AsyncTask* task : finished)
task->finish(cx);
// It doesn't make sense for job queue draining to be reentrant. At the
// same time we don't want to assert against it, because that'd make
// drainJobQueue unsafe for fuzzers. We do want fuzzers to test this,
// so we simply ignore nested calls of drainJobQueue.
sc->drainingJobQueue = true;
RootedObject job(cx);
JS::HandleValueArray args(JS::HandleValueArray::empty());
RootedValue rval(cx);
// Execute jobs in a loop until we've reached the end of the queue.
// Since executing a job can trigger enqueuing of additional jobs,
// it's crucial to re-check the queue length during each iteration.
for (size_t i = 0; i < sc->jobQueue.length(); i++) {
job = sc->jobQueue[i];
AutoCompartment ac(cx, job);
{
AutoReportException are(cx);
JS::Call(cx, UndefinedHandleValue, job, args, &rval);
}
sc->jobQueue[i].set(nullptr);
}
sc->jobQueue.clear();
sc->drainingJobQueue = false;
// It's possible a job added an async task, and it's also possible
// that task has already finished.
{
ExclusiveData<ShellAsyncTasks>::Guard asyncTasks = sc->asyncTasks.lock();
if (asyncTasks->outstanding == 0 && asyncTasks->finished.length() == 0)
break;
}
}
}
static bool
DrainJobQueue(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
DrainJobQueue(cx);
MOZ_ASSERT(!GetShellContext(cx)->quitting);
js::RunJobs(cx);
args.rval().setUndefined();
return true;
@ -1107,7 +987,8 @@ ReadEvalPrintLoop(JSContext* cx, FILE* in, bool compileOnly)
stderr);
}
DrainJobQueue(cx);
if (!GetShellContext(cx)->quitting)
js::RunJobs(cx);
} while (!hitEOF && !sc->quitting);
@ -2287,6 +2168,7 @@ Quit(JSContext* cx, unsigned argc, Value* vp)
return false;
}
js::StopDrainingJobQueue(cx);
sc->exitCode = code;
sc->quitting = true;
return false;
@ -3586,9 +3468,7 @@ WorkerMain(void* arg)
if (input->parentRuntime)
sc->isWorker = true;
JS_SetContextPrivate(cx, sc.get());
JS_SetGrayGCRootsTracer(cx, TraceGrayRoots, nullptr);
SetWorkerContextOptions(cx);
sc->jobQueue.init(cx, JobQueue(SystemAllocPolicy()));
Maybe<EnvironmentPreparer> environmentPreparer;
if (input->parentRuntime) {
@ -3597,13 +3477,11 @@ WorkerMain(void* arg)
js::SetPreserveWrapperCallback(cx, DummyPreserveWrapperCallback);
JS_InitDestroyPrincipalsCallback(cx, ShellPrincipals::destroy);
js::UseInternalJobQueues(cx);
if (!JS::InitSelfHostedCode(cx))
return;
JS::SetEnqueuePromiseJobCallback(cx, ShellEnqueuePromiseJobCallback);
JS::SetGetIncumbentGlobalCallback(cx, ShellGetIncumbentGlobalCallback);
JS::SetAsyncTaskCallbacks(cx, ShellStartAsyncTaskCallback, ShellFinishAsyncTaskCallback);
environmentPreparer.emplace(cx);
} else {
JS_AddInterruptCallback(cx, ShellInterruptCallback);
@ -3635,13 +3513,6 @@ WorkerMain(void* arg)
JS_ExecuteScript(cx, script, &result);
} while (0);
if (input->parentRuntime) {
JS::SetGetIncumbentGlobalCallback(cx, nullptr);
JS::SetEnqueuePromiseJobCallback(cx, nullptr);
}
sc->jobQueue.reset();
KillWatchdog(cx);
JS_SetGrayGCRootsTracer(cx, nullptr, nullptr);
}
@ -8290,7 +8161,8 @@ Shell(JSContext* cx, OptionParser* op, char** envp)
* tasks before the main thread JSRuntime is torn down. Drain after
* uncaught exceptions have been reported since draining runs callbacks.
*/
DrainJobQueue(cx);
if (!GetShellContext(cx)->quitting)
js::RunJobs(cx);
if (sc->exitCode)
result = sc->exitCode;
@ -8647,14 +8519,11 @@ main(int argc, char** argv, char** envp)
JS::dbg::SetDebuggerMallocSizeOf(cx, moz_malloc_size_of);
js::UseInternalJobQueues(cx);
if (!JS::InitSelfHostedCode(cx))
return 1;
sc->jobQueue.init(cx, JobQueue(SystemAllocPolicy()));
JS::SetEnqueuePromiseJobCallback(cx, ShellEnqueuePromiseJobCallback);
JS::SetGetIncumbentGlobalCallback(cx, ShellGetIncumbentGlobalCallback);
JS::SetAsyncTaskCallbacks(cx, ShellStartAsyncTaskCallback, ShellFinishAsyncTaskCallback);
EnvironmentPreparer environmentPreparer(cx);
JS_SetGCParameter(cx, JSGC_MODE, JSGC_MODE_INCREMENTAL);
@ -8686,13 +8555,10 @@ main(int argc, char** argv, char** envp)
printf("OOM max count: %" PRIu64 "\n", js::oom::counter);
#endif
JS::SetGetIncumbentGlobalCallback(cx, nullptr);
JS::SetEnqueuePromiseJobCallback(cx, nullptr);
JS_SetGrayGCRootsTracer(cx, nullptr, nullptr);
// Must clear out some of sc's pointer containers before JS_DestroyContext.
sc->markObservers.reset();
sc->jobQueue.reset();
KillWatchdog(cx);

View File

@ -22,7 +22,7 @@
\
_(GlobalHelperThreadState, 300) \
\
_(ShellAsyncTasks, 350) \
_(InternalAsyncTasks, 350) \
\
_(GCLock, 400) \
\

View File

@ -694,8 +694,13 @@ FreeOp::isDefaultFreeOp() const
JSObject*
JSRuntime::getIncumbentGlobal(JSContext* cx)
{
MOZ_ASSERT(cx->getIncumbentGlobalCallback,
"Must set a callback using SetGetIncumbentGlobalCallback before using Promises");
// If the embedding didn't set a callback for getting the incumbent
// global, the currently active global is used.
if (!cx->getIncumbentGlobalCallback) {
if (!cx->compartment())
return nullptr;
return cx->global();
}
return cx->getIncumbentGlobalCallback(cx);
}

View File

@ -286,6 +286,7 @@ void DisableExtraThreads();
using ScriptAndCountsVector = GCVector<ScriptAndCounts, 0, SystemAllocPolicy>;
class AutoLockForExclusiveAccess;
} // namespace js
struct JSRuntime : public js::MallocProvider<JSRuntime>