diff --git a/js/src/gc/RootMarking.cpp b/js/src/gc/RootMarking.cpp index 9d1e82f31be9..ea326b4ab45c 100644 --- a/js/src/gc/RootMarking.cpp +++ b/js/src/gc/RootMarking.cpp @@ -308,22 +308,6 @@ js::gc::GCRuntime::markRuntime(JSTracer* trc, TraceOrMarkRuntime traceOrMark) for (ContextIter acx(rt); !acx.done(); acx.next()) acx->mark(trc); - for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) { - if (traceOrMark == MarkRuntime && !zone->isCollecting()) - continue; - - /* Do not discard scripts with counts while profiling. */ - if (rt->profilingScripts && !rt->isHeapMinorCollecting()) { - for (ZoneCellIterUnderGC i(zone, AllocKind::SCRIPT); !i.done(); i.next()) { - JSScript* script = i.get(); - if (script->hasScriptCounts()) { - TraceRoot(trc, &script, "profilingScripts"); - MOZ_ASSERT(script == i.get()); - } - } - } - } - for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) c->traceRoots(trc, traceOrMark); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-02.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-02.js new file mode 100644 index 000000000000..a8e6128f361b --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-02.js @@ -0,0 +1,40 @@ + +// This script check that when we enable / disable the code coverage collection, +// then we have different results for the getOffsetsCoverage methods. + +var g = newGlobal(); +var dbg = Debugger(g); +var coverageInfo = []; +var num = 20; +function loop(i) { + var n = 0; + for (n = 0; n < i; n++) + debugger; +} +g.eval(loop.toSource()); + +dbg.onDebuggerStatement = function (f) { + // Collect coverage info each time we hit a debugger statement. + coverageInfo.push(f.callee.script.getOffsetsCoverage()); +}; + +coverageInfo = []; +dbg.collectCoverageInfo = false; +g.eval("loop(" + num + ");"); +assertEq(coverageInfo.length, num); +assertEq(coverageInfo[0], null); +assertEq(coverageInfo[num - 1], null); + +coverageInfo = []; +dbg.collectCoverageInfo = true; +g.eval("loop(" + num + ");"); +assertEq(coverageInfo.length, num); +assertEq(!coverageInfo[0], false); +assertEq(!coverageInfo[num - 1], false); + +coverageInfo = []; +dbg.collectCoverageInfo = false; +g.eval("loop(" + num + ");"); +assertEq(coverageInfo.length, num); +assertEq(coverageInfo[0], null); +assertEq(coverageInfo[num - 1], null); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-03.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-03.js new file mode 100644 index 000000000000..094f60447bf8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-03.js @@ -0,0 +1,21 @@ +// |jit-test| error: Error: can't start debugging: a debuggee script is on the stack + +var g = newGlobal(); +var dbg = Debugger(g); +function loop(i) { + var n = 0; + for (n = 0; n < i; n++) + debugger; +} +g.eval(loop.toSource()); + +var countDown = 20; +dbg.onDebuggerStatement = function (f) { + // Should throw an error. + if (countDown > 0 && --countDown == 0) { + dbg.collectCoverageInfo = !dbg.collectCoverageInfo; + } +}; + +dbg.collectCoverageInfo = false; +g.eval("loop("+ (2 * countDown) +");"); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-04.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-04.js new file mode 100644 index 000000000000..e9888ac16b57 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-04.js @@ -0,0 +1,22 @@ +// |jit-test| error: Error: can't start debugging: a debuggee script is on the stack + +var g = newGlobal(); +var dbg = Debugger(g); + +function loop(i) { + var n = 0; + for (n = 0; n < i; n++) + debugger; +} +g.eval(loop.toSource()); + +var countDown = 20; +dbg.onDebuggerStatement = function (f) { + // Should throw an error. + if (countDown > 0 && --countDown == 0) { + dbg.collectCoverageInfo = !dbg.collectCoverageInfo; + } +}; + +dbg.collectCoverageInfo = true; +g.eval("loop("+ (2 * countDown) +");"); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-05.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-05.js new file mode 100644 index 000000000000..de2c74351b62 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-05.js @@ -0,0 +1,24 @@ +var g = newGlobal(); +var dbg = Debugger(g); +function f(x) { + while (x) { + interruptIf(true); + x -= 1; + } +} +g.eval(f.toSource()); + +// Toogle the debugger while the function f is running. +setInterruptCallback(toogleDebugger); +function toogleDebugger() { + dbg.enabled = !dbg.enabled; + return true; +} + +dbg.collectCoverageInfo = false; +dbg.enabled = false; +g.eval("f(10);"); + +dbg.collectCoverageInfo = true; +dbg.enabled = false; +g.eval("f(10);"); diff --git a/js/src/jit/BaselineCompiler.cpp b/js/src/jit/BaselineCompiler.cpp index 855efe100198..99c66614860e 100644 --- a/js/src/jit/BaselineCompiler.cpp +++ b/js/src/jit/BaselineCompiler.cpp @@ -91,6 +91,12 @@ BaselineCompiler::compile() if (!script->ensureHasTypes(cx) || !script->ensureHasAnalyzedArgsUsage(cx)) return Method_Error; + // When a Debugger set the collectCoverageInfo flag, we recompile baseline + // scripts without entering the interpreter again. We have to create the + // ScriptCounts if they do not exist. + if (!script->hasScriptCounts() && cx->compartment()->collectCoverage()) + script->initScriptCounts(cx); + // Pin analysis info during compilation. AutoEnterAnalysis autoEnterAnalysis(cx); diff --git a/js/src/jscompartment.cpp b/js/src/jscompartment.cpp index fec74a64cddf..fe485dc8f0d7 100644 --- a/js/src/jscompartment.cpp +++ b/js/src/jscompartment.cpp @@ -568,6 +568,16 @@ JSCompartment::traceRoots(JSTracer* trc, js::gc::GCRuntime::TraceOrMarkRuntime t if (objectMetadataTable) objectMetadataTable->trace(trc); + + if (scriptCountsMap && !trc->runtime()->isHeapMinorCollecting()) { + MOZ_ASSERT_IF(!trc->runtime()->isBeingDestroyed(), collectCoverage()); + for (ScriptCountsMap::Range r = scriptCountsMap->all(); !r.empty(); r.popFront()) { + JSScript* script = const_cast(r.front().key()); + MOZ_ASSERT(script->hasScriptCounts()); + TraceRoot(trc, &script, "profilingScripts"); + MOZ_ASSERT(script == r.front().key(), "const_cast is only a work-around"); + } + } } void @@ -925,14 +935,15 @@ JSCompartment::updateDebuggerObservesFlag(unsigned flag) { MOZ_ASSERT(isDebuggee()); MOZ_ASSERT(flag == DebuggerObservesAllExecution || + flag == DebuggerObservesCoverage || flag == DebuggerObservesAsmJS); const GlobalObject::DebuggerVector* v = maybeGlobal()->getDebuggers(); for (Debugger * const* p = v->begin(); p != v->end(); p++) { Debugger* dbg = *p; - if (flag == DebuggerObservesAllExecution - ? dbg->observesAllExecution() - : dbg->observesAsmJS()) + if (flag == DebuggerObservesAllExecution ? dbg->observesAllExecution() : + flag == DebuggerObservesCoverage ? dbg->observesCoverage() : + dbg->observesAsmJS()) { debugModeBits |= flag; return; @@ -951,6 +962,49 @@ JSCompartment::unsetIsDebuggee() } } +void +JSCompartment::updateDebuggerObservesCoverage() +{ + bool previousState = debuggerObservesCoverage(); + updateDebuggerObservesFlag(DebuggerObservesCoverage); + if (previousState == debuggerObservesCoverage()) + return; + + if (debuggerObservesCoverage()) { + // Interrupt any running interpreter frame. The scriptCounts are + // allocated on demand when a script resume its execution. + for (ActivationIterator iter(runtimeFromMainThread()); !iter.done(); ++iter) { + if (iter->isInterpreter()) + iter->asInterpreter()->enableInterruptsUnconditionally(); + } + return; + } + + // If the runtime flag is enabled, then keep the data until + // StopPCCountProfiling is called. + if (runtimeFromMainThread()->profilingScripts) + return; + + clearScriptCounts(); +} + +void +JSCompartment::clearScriptCounts() +{ + if (!scriptCountsMap) + return; + + // Clear all hasScriptCounts_ flags of JSScript, in order to release all + // ScriptCounts entry of the current compartment. + for (ScriptCountsMap::Range r = scriptCountsMap->all(); !r.empty(); r.popFront()) { + ScriptCounts* value = &r.front().value(); + r.front().key()->takeOverScriptCountsMapEntry(value); + } + + js_delete(scriptCountsMap); + scriptCountsMap = nullptr; +} + void JSCompartment::clearBreakpointsIn(FreeOp* fop, js::Debugger* dbg, HandleObject handler) { diff --git a/js/src/jscompartment.h b/js/src/jscompartment.h index c1feed7b1700..2803be3ded14 100644 --- a/js/src/jscompartment.h +++ b/js/src/jscompartment.h @@ -465,13 +465,15 @@ struct JSCompartment IsDebuggee = 1 << 0, DebuggerObservesAllExecution = 1 << 1, DebuggerObservesAsmJS = 1 << 2, - DebuggerNeedsDelazification = 1 << 3 + DebuggerObservesCoverage = 1 << 3, + DebuggerNeedsDelazification = 1 << 4 }; unsigned debugModeBits; static const unsigned DebuggerObservesMask = IsDebuggee | DebuggerObservesAllExecution | + DebuggerObservesCoverage | DebuggerObservesAsmJS; void updateDebuggerObservesFlag(unsigned flag); @@ -652,6 +654,22 @@ struct JSCompartment updateDebuggerObservesFlag(DebuggerObservesAsmJS); } + // True if this compartment's global is a debuggee of some Debugger object + // whose collectCoverageInfo flag is true. + bool debuggerObservesCoverage() const { + static const unsigned Mask = DebuggerObservesCoverage; + return (debugModeBits & Mask) == Mask; + } + void updateDebuggerObservesCoverage(); + + // The code coverage can be enabled either for each compartment, with the + // Debugger API, or for the entire runtime. + bool collectCoverage() const { + return debuggerObservesCoverage() || + runtimeFromAnyThread()->profilingScripts; + } + void clearScriptCounts(); + bool needsDelazificationForDebugger() const { return debugModeBits & DebuggerNeedsDelazification; } diff --git a/js/src/jsscript.cpp b/js/src/jsscript.cpp index f7a9441817c4..692c569c4854 100644 --- a/js/src/jsscript.cpp +++ b/js/src/jsscript.cpp @@ -1455,6 +1455,16 @@ JSScript::getIonCounts() return getScriptCounts().ionCounts_; } +void +JSScript::takeOverScriptCountsMapEntry(ScriptCounts* entryValue) +{ +#ifdef DEBUG + ScriptCountsMap::Ptr p = GetScriptCountsMapEntry(this); + MOZ_ASSERT(entryValue == &p->value()); +#endif + hasScriptCounts_ = false; +} + void JSScript::releaseScriptCounts(ScriptCounts* counts) { diff --git a/js/src/jsscript.h b/js/src/jsscript.h index e058ef8c8322..453bfc716f84 100644 --- a/js/src/jsscript.h +++ b/js/src/jsscript.h @@ -1639,6 +1639,8 @@ class JSScript : public js::gc::TenuredCell js::jit::IonScriptCounts* getIonCounts(); void releaseScriptCounts(js::ScriptCounts* counts); void destroyScriptCounts(js::FreeOp* fop); + // The entry should be removed after using this function. + void takeOverScriptCountsMapEntry(js::ScriptCounts* entryValue); jsbytecode* main() { return code() + mainOffset(); diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp index 716decde200e..9e2119252369 100644 --- a/js/src/vm/Debugger.cpp +++ b/js/src/vm/Debugger.cpp @@ -358,6 +358,7 @@ Debugger::Debugger(JSContext* cx, NativeObject* dbg) uncaughtExceptionHook(nullptr), enabled(true), allowUnobservedAsmJS(false), + collectCoverageInfo(false), observedGCs(cx), tenurePromotionsLog(cx), trackingTenurePromotions(false), @@ -1841,6 +1842,9 @@ class MOZ_RAII ExecutionObservableCompartments : public Debugger::ExecutionObser bool init() { return compartments_.init() && zones_.init(); } bool add(JSCompartment* comp) { return compartments_.put(comp) && zones_.put(comp->zone()); } + typedef HashSet::Range CompartmentRange; + const HashSet* compartments() const { return &compartments_; } + const HashSet* zones() const { return &zones_; } bool shouldRecompileOrInvalidate(JSScript* script) const { return script->hasBaselineScript() && compartments_.has(script->compartment()); @@ -2185,6 +2189,14 @@ Debugger::observesAsmJS() const return NotObserving; } +Debugger::IsObserving +Debugger::observesCoverage() const +{ + if (enabled && collectCoverageInfo) + return Observing; + return NotObserving; +} + // Toggle whether this Debugger's debuggees observe all execution. This is // called when a hook that observes all execution is set or unset. See // hookObservesAllExecution. @@ -2213,6 +2225,53 @@ Debugger::updateObservesAllExecutionOnDebuggees(JSContext* cx, IsObserving obser return updateExecutionObservability(cx, obs, observing); } +bool +Debugger::updateObservesCoverageOnDebuggees(JSContext* cx, IsObserving observing) +{ + ExecutionObservableCompartments obs(cx); + if (!obs.init()) + return false; + + for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { + GlobalObject* global = r.front(); + JSCompartment* comp = global->compartment(); + + if (comp->debuggerObservesCoverage() == observing) + continue; + + // Invalidate and recompile a compartment to add or remove PCCounts + // increments. We have to eagerly invalidate, as otherwise we might have + // dangling pointers to freed PCCounts. + if (!obs.add(comp)) + return false; + } + + // If any frame on the stack belongs to the debuggee, then we cannot update + // the ScriptCounts, because this would imply to invalidate a Debugger.Frame + // to recompile it with/without ScriptCount support. + for (ScriptFrameIter iter(cx, ScriptFrameIter::ALL_CONTEXTS, + ScriptFrameIter::GO_THROUGH_SAVED); + !iter.done(); + ++iter) + { + if (obs.shouldMarkAsDebuggee(iter)) { + JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_IDLE); + return false; + } + } + + if (!updateExecutionObservability(cx, obs, observing)) + return false; + + // All compartments can safely be toggled, and all scripts will be + // recompiled. Thus we can update each compartment accordingly. + typedef ExecutionObservableCompartments::CompartmentRange CompartmentRange; + for (CompartmentRange r = obs.compartments()->all(); !r.empty(); r.popFront()) + r.front()->updateDebuggerObservesCoverage(); + + return true; +} + void Debugger::updateObservesAsmJSOnDebuggees(IsObserving observing) { @@ -2694,6 +2753,9 @@ Debugger::setEnabled(JSContext* cx, unsigned argc, Value* vp) if (!dbg->updateObservesAllExecutionOnDebuggees(cx, dbg->observesAllExecution())) return false; + // Note: To toogle code coverage, we currently need to have no live + // stack frame, thus the coverage does not depend on the enabled flag. + dbg->updateObservesAsmJSOnDebuggees(dbg->observesAsmJS()); } @@ -2902,6 +2964,30 @@ Debugger::setAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp) return true; } +/* static */ bool +Debugger::getCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "get collectCoverageInfo", args, dbg); + args.rval().setBoolean(dbg->collectCoverageInfo); + return true; +} + +/* static */ bool +Debugger::setCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp) +{ + THIS_DEBUGGER(cx, argc, vp, "set collectCoverageInfo", args, dbg); + if (!args.requireAtLeast(cx, "Debugger.set collectCoverageInfo", 1)) + return false; + dbg->collectCoverageInfo = ToBoolean(args[0]); + + IsObserving observing = dbg->collectCoverageInfo ? Observing : NotObserving; + if (!dbg->updateObservesCoverageOnDebuggees(cx, observing)) + return false; + + args.rval().setUndefined(); + return true; +} + /* static */ bool Debugger::getMemory(JSContext* cx, unsigned argc, Value* vp) { @@ -3339,6 +3425,7 @@ Debugger::addDebuggeeGlobal(JSContext* cx, Handle global) // (6) debuggeeCompartment->setIsDebuggee(); debuggeeCompartment->updateDebuggerObservesAsmJS(); + debuggeeCompartment->updateDebuggerObservesCoverage(); if (observesAllExecution() && !ensureExecutionObservabilityOfCompartment(cx, debuggeeCompartment)) return false; @@ -3453,6 +3540,7 @@ Debugger::removeDebuggeeGlobal(FreeOp* fop, GlobalObject* global, } else { global->compartment()->updateDebuggerObservesAllExecution(); global->compartment()->updateDebuggerObservesAsmJS(); + global->compartment()->updateDebuggerObservesCoverage(); } } @@ -4433,6 +4521,8 @@ const JSPropertySpec Debugger::properties[] = { Debugger::setUncaughtExceptionHook, 0), JS_PSGS("allowUnobservedAsmJS", Debugger::getAllowUnobservedAsmJS, Debugger::setAllowUnobservedAsmJS, 0), + JS_PSGS("collectCoverageInfo", Debugger::getCollectCoverageInfo, + Debugger::setCollectCoverageInfo, 0), JS_PSG("memory", Debugger::getMemory, 0), JS_PSGS("onIonCompilation", Debugger::getOnIonCompilation, Debugger::setOnIonCompilation, 0), JS_PS_END diff --git a/js/src/vm/Debugger.h b/js/src/vm/Debugger.h index eeb1bce5dc05..b291805bbb34 100644 --- a/js/src/vm/Debugger.h +++ b/js/src/vm/Debugger.h @@ -340,6 +340,10 @@ class Debugger : private mozilla::LinkedListElement js::HeapPtrObject uncaughtExceptionHook; /* Strong reference. */ bool enabled; bool allowUnobservedAsmJS; + + // Wether to enable code coverage on the Debuggee. + bool collectCoverageInfo; + JSCList breakpoints; /* Circular list of all js::Breakpoints in this debugger */ // The set of GC numbers for which one or more of this Debugger's observed @@ -545,6 +549,8 @@ class Debugger : private mozilla::LinkedListElement static bool setUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp); static bool getAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp); static bool setAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp); + static bool getCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp); + static bool setCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp); static bool getMemory(JSContext* cx, unsigned argc, Value* vp); static bool getOnIonCompilation(JSContext* cx, unsigned argc, Value* vp); static bool setOnIonCompilation(JSContext* cx, unsigned argc, Value* vp); @@ -594,6 +600,10 @@ class Debugger : private mozilla::LinkedListElement // execution of its debuggees. IsObserving observesAsmJS() const; + // Whether the Debugger instance needs to observe coverage of any JavaScript + // execution. + IsObserving observesCoverage() const; + private: static bool ensureExecutionObservabilityOfFrame(JSContext* cx, AbstractFramePtr frame); static bool ensureExecutionObservabilityOfCompartment(JSContext* cx, JSCompartment* comp); @@ -601,6 +611,7 @@ class Debugger : private mozilla::LinkedListElement static bool hookObservesAllExecution(Hook which); bool updateObservesAllExecutionOnDebuggees(JSContext* cx, IsObserving observing); + bool updateObservesCoverageOnDebuggees(JSContext* cx, IsObserving observing); void updateObservesAsmJSOnDebuggees(IsObserving observing); JSObject* getHook(Hook hook) const; diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 724218e084fa..e6fc435f87d8 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1960,7 +1960,7 @@ Interpret(JSContext* cx, RunState& state) MOZ_CRASH("bad Debugger::onEnterFrame status"); } - if (cx->runtime()->profilingScripts) + if (cx->compartment()->collectCoverage()) activation.enableInterruptsUnconditionally(); // Enter the interpreter loop starting at the current pc. @@ -1973,9 +1973,8 @@ CASE(EnableInterruptsPseudoOpcode) bool moreInterrupts = false; jsbytecode op = *REGS.pc; - if (cx->runtime()->profilingScripts) { - if (!script->hasScriptCounts()) - script->initScriptCounts(cx); + if (!script->hasScriptCounts() && cx->compartment()->collectCoverage()) { + script->initScriptCounts(cx); moreInterrupts = true; } diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 54e6d09121c1..84c572da05b9 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -379,6 +379,13 @@ JSRuntime::~JSRuntime() wpmap->clear(); } + /* + * Clear script counts map, to remove the strong reference on the + * JSScript key. + */ + for (CompartmentsIter comp(this, SkipAtoms); !comp.done(); comp.next()) + comp->clearScriptCounts(); + /* Clear atoms to remove GC roots and heap allocations. */ finishAtoms();