diff --git a/js/src/jit-test/tests/basic/bug1341321.js b/js/src/jit-test/tests/basic/bug1341321.js new file mode 100644 index 000000000000..94ef17a42d8b --- /dev/null +++ b/js/src/jit-test/tests/basic/bug1341321.js @@ -0,0 +1,20 @@ +if (helperThreadCount() == 0) + quit(); + +// The new Debugger here should throw but we don't have a way to verify this +// (exceptions that worker threads throw do not cause the test to fail). +evalInCooperativeThread('cooperativeYield(); var dbg = new Debugger();'); + +var dbg = new Debugger; +assertEq(dbg.addAllGlobalsAsDebuggees(), undefined); + +function assertThrows(f) { + var exception = false; + try { f(); } catch (e) { exception = true; } + assertEq(exception, true); +} + +var dbg = new Debugger; +dbg.onNewGlobalObject = function(global) {}; +assertThrows(() => evalInCooperativeThread("var x = 3")); +assertThrows(cooperativeYield); diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 57dbafe22713..1f5fa6cc4a78 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -565,6 +565,15 @@ JS_GetParentRuntime(JSContext* cx) return cx->runtime()->parentRuntime ? cx->runtime()->parentRuntime : cx->runtime(); } +JS_PUBLIC_API(void) +JS::SetSingleThreadedExecutionCallbacks(JSContext* cx, + BeginSingleThreadedExecutionCallback begin, + EndSingleThreadedExecutionCallback end) +{ + cx->runtime()->beginSingleThreadedExecutionCallback = begin; + cx->runtime()->endSingleThreadedExecutionCallback = end; +} + JS_PUBLIC_API(JSVersion) JS_GetVersion(JSContext* cx) { diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 29fd1bf1bc4e..811aa029a69a 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -1033,6 +1033,31 @@ JS_EndRequest(JSContext* cx); extern JS_PUBLIC_API(void) JS_SetFutexCanWait(JSContext* cx); +namespace JS { + +// Single threaded execution callbacks are used to notify API clients that a +// feature is in use on a context's runtime that is not yet compatible with +// cooperatively multithreaded execution. +// +// Between a call to BeginSingleThreadedExecutionCallback and a corresponding +// call to EndSingleThreadedExecutionCallback, only one thread at a time may +// enter compartments in the runtime. The begin callback may yield as necessary +// to permit other threads to finish up what they're doing, while the end +// callback may not yield or otherwise operate on the runtime (it may be called +// during GC). +// +// These callbacks may be left unspecified for runtimes which only ever have a +// single context. +typedef void (*BeginSingleThreadedExecutionCallback)(JSContext* cx); +typedef void (*EndSingleThreadedExecutionCallback)(JSContext* cx); + +extern JS_PUBLIC_API(void) +SetSingleThreadedExecutionCallbacks(JSContext* cx, + BeginSingleThreadedExecutionCallback begin, + EndSingleThreadedExecutionCallback end); + +} // namespace JS + namespace js { void diff --git a/js/src/jscompartment.cpp b/js/src/jscompartment.cpp index 53462576911d..8feb3155566b 100644 --- a/js/src/jscompartment.cpp +++ b/js/src/jscompartment.cpp @@ -1164,9 +1164,8 @@ JSCompartment::updateDebuggerObservesCoverage() if (debuggerObservesCoverage()) { // Interrupt any running interpreter frame. The scriptCounts are - // allocated on demand when a script resume its execution. + // allocated on demand when a script resumes its execution. JSContext* cx = TlsContext.get(); - MOZ_ASSERT(zone()->group()->ownedByCurrentThread()); for (ActivationIterator iter(cx); !iter.done(); ++iter) { if (iter->isInterpreter()) iter->asInterpreter()->enableInterruptsUnconditionally(); diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 9c977c4b8769..3eb354b4b299 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -3384,6 +3384,7 @@ struct CooperationState , idle(false) , numThreads(0) , yieldCount(0) + , singleThreaded(false) {} Mutex lock; @@ -3391,6 +3392,7 @@ struct CooperationState bool idle; size_t numThreads; uint64_t yieldCount; + bool singleThreaded; }; static CooperationState* cooperationState = nullptr; @@ -3436,8 +3438,8 @@ CooperativeYieldThread(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - if (!cooperationState) { - JS_ReportErrorASCII(cx, "No cooperative threads have been created"); + if (!cx->runtime()->gc.canChangeActiveContext(cx)) { + JS_ReportErrorASCII(cx, "Cooperating multithreading context switches are not currently allowed"); return false; } @@ -3446,6 +3448,11 @@ CooperativeYieldThread(JSContext* cx, unsigned argc, Value* vp) return false; } + if (cooperationState->singleThreaded) { + JS_ReportErrorASCII(cx, "Yielding is not allowed while single threaded"); + return false; + } + CooperativeBeginWait(cx); CooperativeYield(); CooperativeEndWait(cx); @@ -3454,6 +3461,35 @@ CooperativeYieldThread(JSContext* cx, unsigned argc, Value* vp) return true; } +static void +CooperativeBeginSingleThreadedExecution(JSContext* cx) +{ + MOZ_ASSERT(!cooperationState->singleThreaded); + + // Yield until all other threads have exited any zone groups they are in. + while (true) { + bool done = true; + for (ZoneGroupsIter group(cx->runtime()); !group.done(); group.next()) { + if (!group->ownedByCurrentThread() && group->ownerContext().context()) + done = false; + } + if (done) + break; + CooperativeBeginWait(cx); + CooperativeYield(); + CooperativeEndWait(cx); + } + + cooperationState->singleThreaded = true; +} + +static void +CooperativeEndSingleThreadedExecution(JSContext* cx) +{ + if (cooperationState) + cooperationState->singleThreaded = false; +} + struct WorkerInput { JSRuntime* parentRuntime; @@ -3621,6 +3657,11 @@ EvalInThread(JSContext* cx, unsigned argc, Value* vp, bool cooperative) return false; } + if (cooperative && cooperationState->singleThreaded) { + JS_ReportErrorASCII(cx, "Creating cooperative threads is not allowed while single threaded"); + return false; + } + if (!args[0].toString()->ensureLinear(cx)) return false; @@ -3652,8 +3693,6 @@ EvalInThread(JSContext* cx, unsigned argc, Value* vp, bool cooperative) } if (cooperative) { - if (!cooperationState) - cooperationState = js_new(); cooperationState->numThreads++; CooperativeBeginWait(cx); } @@ -3900,7 +3939,7 @@ KillWorkerThreads(JSContext* cx) workerThreadsLock = nullptr; // Yield until all other cooperative threads in the main runtime finish. - while (cooperationState && cooperationState->numThreads) { + while (cooperationState->numThreads) { CooperativeBeginWait(cx); CooperativeYield(); CooperativeEndWait(cx); @@ -8369,6 +8408,11 @@ main(int argc, char** argv, char** envp) js::SetPreserveWrapperCallback(cx, DummyPreserveWrapperCallback); + cooperationState = js_new(); + JS::SetSingleThreadedExecutionCallbacks(cx, + CooperativeBeginSingleThreadedExecution, + CooperativeEndSingleThreadedExecution); + result = Shell(cx, &op, envp); #ifdef DEBUG diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp index d1120b580f2d..7497b17115e8 100644 --- a/js/src/vm/Debugger.cpp +++ b/js/src/vm/Debugger.cpp @@ -713,6 +713,9 @@ Debugger::~Debugger() * background finalized. */ JS_REMOVE_LINK(&onNewGlobalObjectWatchersLink); + + JSContext* cx = TlsContext.get(); + cx->runtime()->endSingleThreadedExecution(cx); } bool @@ -2404,12 +2407,7 @@ class MOZ_RAII ExecutionObservableCompartments : public Debugger::ExecutionObser } bool init() { return compartments_.init() && zones_.init(); } - bool add(JSCompartment* comp) { - // The current cx should have exclusive access to observed content, - // since debuggees must be in the same zone group as ther debugger. - MOZ_ASSERT(comp->zone()->group() == TlsContext.get()->zone()->group()); - return compartments_.put(comp) && zones_.put(comp->zone()); - } + bool add(JSCompartment* comp) { return compartments_.put(comp) && zones_.put(comp->zone()); } typedef HashSet::Range CompartmentRange; const HashSet* compartments() const { return &compartments_; } @@ -2503,9 +2501,6 @@ class MOZ_RAII ExecutionObservableScript : public Debugger::ExecutionObservableS MOZ_GUARD_OBJECT_NOTIFIER_PARAM) : script_(cx, script) { - // The current cx should have exclusive access to observed content, - // since debuggees must be in the same zone group as ther debugger. - MOZ_ASSERT(singleZone()->group() == cx->zone()->group()); MOZ_GUARD_OBJECT_NOTIFIER_INIT; } @@ -3936,11 +3931,24 @@ Debugger::construct(JSContext* cx, unsigned argc, Value* vp) obj->setReservedSlot(slot, proto->getReservedSlot(slot)); obj->setReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE, NullValue()); + // Debuggers currently require single threaded execution. A debugger may be + // used to debug content in other zone groups, and may be used to observe + // all activity in the runtime via hooks like OnNewGlobalObject. + if (!cx->runtime()->beginSingleThreadedExecution(cx)) { + JS_ReportErrorASCII(cx, "Cannot ensure single threaded execution in Debugger"); + return false; + } + Debugger* debugger; { /* Construct the underlying C++ object. */ auto dbg = cx->make_unique(cx, obj.get()); - if (!dbg || !dbg->init(cx)) + if (!dbg) { + JS::AutoSuppressGCAnalysis nogc; // Suppress warning about |dbg|. + cx->runtime()->endSingleThreadedExecution(cx); + return false; + } + if (!dbg->init(cx)) return false; debugger = dbg.release(); @@ -3962,13 +3970,6 @@ Debugger::construct(JSContext* cx, unsigned argc, Value* vp) bool Debugger::addDebuggeeGlobal(JSContext* cx, Handle global) { - // Debuggers are required to be in the same zone group as their debuggees. - // The debugger must be able to observe all activity in the debuggee - // compartment, which requires that its thread have exclusive access to - // that compartment's contents. - MOZ_ASSERT(cx->zone() == object->zone()); - MOZ_RELEASE_ASSERT(global->zone()->group() == cx->zone()->group()); - if (debuggees.has(global)) return true; diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 2c57a50a6cde..5a55bec8aeee 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -97,6 +97,10 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime) #endif activeContext_(nullptr), activeContextChangeProhibited_(0), + singleThreadedExecutionRequired_(0), + startingSingleThreadedExecution_(false), + beginSingleThreadedExecutionCallback(nullptr), + endSingleThreadedExecutionCallback(nullptr), profilerSampleBufferGen_(0), profilerSampleBufferLapCount_(1), telemetryCallback(nullptr), @@ -308,6 +312,7 @@ JSRuntime::destroyRuntime() MOZ_ASSERT(ionLazyLinkListSize_ == 0); MOZ_ASSERT(ionLazyLinkList().isEmpty()); + MOZ_ASSERT(!singleThreadedExecutionRequired_); MOZ_ASSERT(!hasHelperThreadZones()); AutoLockForExclusiveAccess lock(this); @@ -334,12 +339,23 @@ JSRuntime::destroyRuntime() MOZ_ASSERT(oldCount > 0); } +static void +CheckCanChangeActiveContext(JSRuntime* rt) +{ + MOZ_RELEASE_ASSERT(!rt->activeContextChangeProhibited()); + MOZ_RELEASE_ASSERT(!rt->activeContext() || rt->gc.canChangeActiveContext(rt->activeContext())); + + if (rt->singleThreadedExecutionRequired()) { + for (ZoneGroupsIter group(rt); !group.done(); group.next()) + MOZ_RELEASE_ASSERT(group->ownerContext().context() == nullptr); + } +} + void JSRuntime::setActiveContext(JSContext* cx) { + CheckCanChangeActiveContext(this); MOZ_ASSERT_IF(cx, cx->isCooperativelyScheduled()); - MOZ_RELEASE_ASSERT(!activeContextChangeProhibited()); - MOZ_RELEASE_ASSERT(!activeContext() || gc.canChangeActiveContext(activeContext())); activeContext_ = cx; } @@ -347,9 +363,7 @@ JSRuntime::setActiveContext(JSContext* cx) void JSRuntime::setNewbornActiveContext(JSContext* cx) { - MOZ_ASSERT_IF(cx, cx->isCooperativelyScheduled()); - MOZ_RELEASE_ASSERT(!activeContextChangeProhibited()); - MOZ_RELEASE_ASSERT(!activeContext()); + CheckCanChangeActiveContext(this); activeContext_ = cx; @@ -361,14 +375,46 @@ JSRuntime::setNewbornActiveContext(JSContext* cx) void JSRuntime::deleteActiveContext(JSContext* cx) { + CheckCanChangeActiveContext(this); MOZ_ASSERT(cx == activeContext()); - MOZ_RELEASE_ASSERT(!activeContextChangeProhibited()); - MOZ_RELEASE_ASSERT(gc.canChangeActiveContext(cx)); js_delete_poison(cx); activeContext_ = nullptr; } +bool +JSRuntime::beginSingleThreadedExecution(JSContext* cx) +{ + if (singleThreadedExecutionRequired_ == 0) { + if (startingSingleThreadedExecution_) + return false; + startingSingleThreadedExecution_ = true; + if (beginSingleThreadedExecutionCallback) + beginSingleThreadedExecutionCallback(cx); + MOZ_ASSERT(startingSingleThreadedExecution_); + startingSingleThreadedExecution_ = false; + } + + singleThreadedExecutionRequired_++; + + for (ZoneGroupsIter group(this); !group.done(); group.next()) { + MOZ_RELEASE_ASSERT(group->ownedByCurrentThread() || + group->ownerContext().context() == nullptr); + } + + return true; +} + +void +JSRuntime::endSingleThreadedExecution(JSContext* cx) +{ + MOZ_ASSERT(singleThreadedExecutionRequired_); + if (--singleThreadedExecutionRequired_ == 0) { + if (endSingleThreadedExecutionCallback) + endSingleThreadedExecutionCallback(cx); + } +} + void JSRuntime::addTelemetry(int id, uint32_t sample, const char* key) { diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 9f57ef55fcd8..8ce1dfdf0fe5 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -342,6 +342,14 @@ struct JSRuntime : public js::MallocProvider // Count of AutoProhibitActiveContextChange instances on the active context. mozilla::Atomic activeContextChangeProhibited_; + // Count of beginSingleThreadedExecution() calls that have occurred with no + // matching endSingleThreadedExecution(). + mozilla::Atomic singleThreadedExecutionRequired_; + + // Whether some thread has called beginSingleThreadedExecution() and we are + // in the associated callback (which may execute JS on other threads). + js::ActiveThreadData startingSingleThreadedExecution_; + public: JSContext* activeContext() const { return activeContext_; } const void* addressOfActiveContext() { return &activeContext_; } @@ -374,6 +382,17 @@ struct JSRuntime : public js::MallocProvider }; bool activeContextChangeProhibited() { return activeContextChangeProhibited_; } + bool singleThreadedExecutionRequired() { return singleThreadedExecutionRequired_; } + + js::ActiveThreadData beginSingleThreadedExecutionCallback; + js::ActiveThreadData endSingleThreadedExecutionCallback; + + // Ensure there is only a single thread interacting with this runtime. + // beginSingleThreadedExecution() returns false if some context has already + // started forcing this runtime to be single threaded. Calls to these + // functions must be balanced. + bool beginSingleThreadedExecution(JSContext* cx); + void endSingleThreadedExecution(JSContext* cx); /* * The profiler sampler generation after the latest sample.