Bug 1176880 part 1 - Add a flag on the Debugger & Compartment to record code-coverage information. r=shu
authorNicolas B. Pierron <nicolas.b.pierron@mozilla.com>
Wed, 16 Sep 2015 21:11:34 +0200
changeset 295472 1ac20ebb3bd71fbdc7cf901c637d1af16d549cf8
parent 295471 51994c27948c7255ae448d126a53a6308fa14ab0
child 295473 b617f0a6a77c649d1f4c721b0548501a6f54c2a0
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersshu
bugs1176880
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1176880 part 1 - Add a flag on the Debugger & Compartment to record code-coverage information. r=shu
js/src/gc/RootMarking.cpp
js/src/jit-test/tests/debug/Script-getOffsetsCoverage-02.js
js/src/jit-test/tests/debug/Script-getOffsetsCoverage-03.js
js/src/jit-test/tests/debug/Script-getOffsetsCoverage-04.js
js/src/jit-test/tests/debug/Script-getOffsetsCoverage-05.js
js/src/jit/BaselineCompiler.cpp
js/src/jscompartment.cpp
js/src/jscompartment.h
js/src/jsscript.cpp
js/src/jsscript.h
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
js/src/vm/Interpreter.cpp
js/src/vm/Runtime.cpp
--- a/js/src/gc/RootMarking.cpp
+++ b/js/src/gc/RootMarking.cpp
@@ -303,32 +303,16 @@ js::gc::GCRuntime::markRuntime(JSTracer*
     }
 
     if (rt->isHeapMinorCollecting())
         jit::JitRuntime::MarkJitcodeGlobalTableUnconditionally(trc);
 
     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<JSScript>();
-                if (script->hasScriptCounts()) {
-                    TraceRoot(trc, &script, "profilingScripts");
-                    MOZ_ASSERT(script == i.get<JSScript>());
-                }
-            }
-        }
-    }
-
     for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next())
         c->traceRoots(trc, traceOrMark);
 
     MarkInterpreterActivations(rt, trc);
 
     jit::MarkJitActivations(rt, trc);
 
     if (!rt->isHeapMinorCollecting()) {
new file mode 100644
--- /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);
new file mode 100644
--- /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) +");");
new file mode 100644
--- /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) +");");
new file mode 100644
--- /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);");
--- a/js/src/jit/BaselineCompiler.cpp
+++ b/js/src/jit/BaselineCompiler.cpp
@@ -86,16 +86,22 @@ BaselineCompiler::compile()
     TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime());
     TraceLoggerEvent scriptEvent(logger, TraceLogger_AnnotateScripts, script);
     AutoTraceLog logScript(logger, scriptEvent);
     AutoTraceLog logCompile(logger, TraceLogger_BaselineCompilation);
 
     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);
 
     MOZ_ASSERT(!script->hasBaselineScript());
 
     if (!emitPrologue())
         return Method_Error;
 
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -563,16 +563,26 @@ JSCompartment::traceRoots(JSTracer* trc,
     if (debugScopes)
         debugScopes->mark(trc);
 
     if (lazyArrayBuffers)
         lazyArrayBuffers->trace(trc);
 
     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<JSScript*>(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
 JSCompartment::sweepAfterMinorGC()
 {
     globalWriteBarriered = false;
 
     if (innerViews.needsSweepAfterMinorGC())
@@ -920,24 +930,25 @@ JSCompartment::ensureDelazifyScriptsForD
     return true;
 }
 
 void
 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;
         }
     }
 
     debugModeBits &= ~flag;
 }
@@ -947,16 +958,59 @@ JSCompartment::unsetIsDebuggee()
 {
     if (isDebuggee()) {
         debugModeBits &= ~DebuggerObservesMask;
         DebugScopes::onCompartmentUnsetIsDebuggee(this);
     }
 }
 
 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)
 {
     for (gc::ZoneCellIter i(zone(), gc::AllocKind::SCRIPT); !i.done(); i.next()) {
         JSScript* script = i.get<JSScript>();
         if (script->compartment() == this && script->hasAnyBreakpointsOrStepMode())
             script->clearBreakpointsIn(fop, dbg, handler);
     }
 }
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -460,23 +460,25 @@ struct JSCompartment
   private:
     /* Whether to preserve JIT code on non-shrinking GCs. */
     bool                         gcPreserveJitCode;
 
     enum {
         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);
 
   public:
     JSCompartment(JS::Zone* zone, const JS::CompartmentOptions& options);
     ~JSCompartment();
 
@@ -647,16 +649,32 @@ struct JSCompartment
     bool debuggerObservesAsmJS() const {
         static const unsigned Mask = IsDebuggee | DebuggerObservesAsmJS;
         return (debugModeBits & Mask) == Mask;
     }
     void updateDebuggerObservesAsmJS() {
         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;
     }
 
     /*
      * Schedule the compartment to be delazified. Called from
      * LazyScript::Create.
      */
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -1451,16 +1451,26 @@ JSScript::addIonCounts(jit::IonScriptCou
 
 jit::IonScriptCounts*
 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)
 {
     ScriptCountsMap::Ptr p = GetScriptCountsMapEntry(this);
     *counts = Move(p->value());
     compartment()->scriptCountsMap->remove(p);
     hasScriptCounts_ = false;
 }
 
--- a/js/src/jsscript.h
+++ b/js/src/jsscript.h
@@ -1634,16 +1634,18 @@ class JSScript : public js::gc::TenuredC
     js::ScriptCounts& getScriptCounts();
     js::PCCounts* maybeGetPCCounts(jsbytecode* pc);
     const js::PCCounts* maybeGetThrowCounts(jsbytecode* pc);
     js::PCCounts* getThrowCounts(jsbytecode* pc);
     void addIonCounts(js::jit::IonScriptCounts* ionCounts);
     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();
     }
 
     /*
      * computedSizeOfData() is the in-use size of all the data sections.
      * sizeOfData() is the size of the block allocated to hold all the data
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -353,16 +353,17 @@ Breakpoint::nextInSite()
 
 /*** Debugger hook dispatch **********************************************************************/
 
 Debugger::Debugger(JSContext* cx, NativeObject* dbg)
   : object(dbg),
     uncaughtExceptionHook(nullptr),
     enabled(true),
     allowUnobservedAsmJS(false),
+    collectCoverageInfo(false),
     observedGCs(cx),
     tenurePromotionsLog(cx),
     trackingTenurePromotions(false),
     maxTenurePromotionsLogLength(DEFAULT_MAX_LOG_LENGTH),
     tenurePromotionsLogOverflowed(false),
     allocationsLog(cx),
     trackingAllocationSites(false),
     allocationSamplingProbability(1.0),
@@ -1836,16 +1837,19 @@ class MOZ_RAII ExecutionObservableCompar
         zones_(cx)
     {
         MOZ_GUARD_OBJECT_NOTIFIER_INIT;
     }
 
     bool init() { return compartments_.init() && zones_.init(); }
     bool add(JSCompartment* comp) { return compartments_.put(comp) && zones_.put(comp->zone()); }
 
+    typedef HashSet<JSCompartment*>::Range CompartmentRange;
+    const HashSet<JSCompartment*>* compartments() const { return &compartments_; }
+
     const HashSet<Zone*>* zones() const { return &zones_; }
     bool shouldRecompileOrInvalidate(JSScript* script) const {
         return script->hasBaselineScript() && compartments_.has(script->compartment());
     }
     bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const {
         // AbstractFramePtr can't refer to non-remateralized Ion frames, so if
         // iter refers to one such, we know we don't match.
         return iter.hasUsableAbstractFramePtr() && compartments_.has(iter.compartment());
@@ -2180,16 +2184,24 @@ Debugger::observesAllExecution() const
 Debugger::IsObserving
 Debugger::observesAsmJS() const
 {
     if (enabled && !allowUnobservedAsmJS)
         return Observing;
     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.
 bool
 Debugger::updateObservesAllExecutionOnDebuggees(JSContext* cx, IsObserving observing)
 {
     ExecutionObservableCompartments obs(cx);
     if (!obs.init())
@@ -2208,16 +2220,63 @@ Debugger::updateObservesAllExecutionOnDe
             return false;
 
         comp->updateDebuggerObservesAllExecution();
     }
 
     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)
 {
     for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
         GlobalObject* global = r.front();
         JSCompartment* comp = global->compartment();
 
         if (comp->debuggerObservesAsmJS() == observing)
@@ -2689,16 +2748,19 @@ Debugger::setEnabled(JSContext* cx, unsi
             }
         }
 
         // Ensure the compartment is observable if we are re-enabling a
         // Debugger with hooks that observe all execution.
         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());
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 /* static */ bool
@@ -2898,16 +2960,40 @@ Debugger::setAllowUnobservedAsmJS(JSCont
         comp->updateDebuggerObservesAsmJS();
     }
 
     args.rval().setUndefined();
     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)
 {
     THIS_DEBUGGER(cx, argc, vp, "get memory", args, dbg);
     Value memoryValue = dbg->object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE);
 
     if (!memoryValue.isObject()) {
         RootedObject memory(cx, DebuggerMemory::create(cx, dbg));
         if (!memory)
@@ -3334,16 +3420,17 @@ Debugger::addDebuggeeGlobal(JSContext* c
     auto allocationsTrackingGuard = MakeScopeExit([&] {
         if (trackingAllocationSites)
             Debugger::removeAllocationsTracking(*global);
     });
 
     // (6)
     debuggeeCompartment->setIsDebuggee();
     debuggeeCompartment->updateDebuggerObservesAsmJS();
+    debuggeeCompartment->updateDebuggerObservesCoverage();
     if (observesAllExecution() && !ensureExecutionObservabilityOfCompartment(cx, debuggeeCompartment))
         return false;
 
     globalDebuggersGuard.release();
     debuggeesGuard.release();
     zoneDebuggersGuard.release();
     debuggeeZonesGuard.release();
     allocationsTrackingGuard.release();
@@ -3448,16 +3535,17 @@ Debugger::removeDebuggeeGlobal(FreeOp* f
     if (trackingAllocationSites)
         Debugger::removeAllocationsTracking(*global);
 
     if (global->getDebuggers()->empty()) {
         global->compartment()->unsetIsDebuggee();
     } else {
         global->compartment()->updateDebuggerObservesAllExecution();
         global->compartment()->updateDebuggerObservesAsmJS();
+        global->compartment()->updateDebuggerObservesCoverage();
     }
 }
 
 
 static inline ScriptSourceObject* GetSourceReferent(JSObject* obj);
 
 /*
  * A class for parsing 'findScripts' query arguments and searching for
@@ -4428,16 +4516,18 @@ const JSPropertySpec Debugger::propertie
     JS_PSGS("onNewPromise", Debugger::getOnNewPromise, Debugger::setOnNewPromise, 0),
     JS_PSGS("onPromiseSettled", Debugger::getOnPromiseSettled, Debugger::setOnPromiseSettled, 0),
     JS_PSGS("onEnterFrame", Debugger::getOnEnterFrame, Debugger::setOnEnterFrame, 0),
     JS_PSGS("onNewGlobalObject", Debugger::getOnNewGlobalObject, Debugger::setOnNewGlobalObject, 0),
     JS_PSGS("uncaughtExceptionHook", Debugger::getUncaughtExceptionHook,
             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
 };
 const JSFunctionSpec Debugger::methods[] = {
     JS_FN("addDebuggee", Debugger::addDebuggee, 1, 0),
     JS_FN("addAllGlobalsAsDebuggees", Debugger::addAllGlobalsAsDebuggees, 0, 0),
     JS_FN("removeDebuggee", Debugger::removeDebuggee, 1, 0),
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -335,16 +335,20 @@ class Debugger : private mozilla::Linked
 
   private:
     HeapPtrNativeObject object;         /* The Debugger object. Strong reference. */
     WeakGlobalObjectSet debuggees;      /* Debuggee globals. Cross-compartment weak references. */
     JS::ZoneSet debuggeeZones; /* Set of zones that we have debuggees in. */
     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
     // debuggees participated in.
     js::HashSet<uint64_t> observedGCs;
 
     using TenurePromotionsLog = js::TraceableFifo<TenurePromotionsLogEntry>;
     TenurePromotionsLog tenurePromotionsLog;
@@ -540,16 +544,18 @@ class Debugger : private mozilla::Linked
     static bool getOnNewPromise(JSContext* cx, unsigned argc, Value* vp);
     static bool setOnNewPromise(JSContext* cx, unsigned argc, Value* vp);
     static bool getOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp);
     static bool setOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp);
     static bool getUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp);
     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);
     static bool addDebuggee(JSContext* cx, unsigned argc, Value* vp);
     static bool addAllGlobalsAsDebuggees(JSContext* cx, unsigned argc, Value* vp);
     static bool removeDebuggee(JSContext* cx, unsigned argc, Value* vp);
     static bool removeAllDebuggees(JSContext* cx, unsigned argc, Value* vp);
     static bool hasDebuggee(JSContext* cx, unsigned argc, Value* vp);
@@ -589,23 +595,28 @@ class Debugger : private mozilla::Linked
     // Whether the Debugger instance needs to observe all non-AOT JS
     // execution of its debugees.
     IsObserving observesAllExecution() const;
 
     // Whether the Debugger instance needs to observe AOT-compiled asm.js
     // 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);
 
     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;
     bool hasAnyLiveHooks() const;
 
     static JSTrapStatus slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame);
     static bool slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool ok);
     static JSTrapStatus slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame);
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -1955,32 +1955,31 @@ Interpret(JSContext* cx, RunState& state
         goto successful_return_continuation;
       case JSTRAP_THROW:
       case JSTRAP_ERROR:
         goto error;
       default:
         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.
     ADVANCE_AND_DISPATCH(0);
 
 INTERPRETER_LOOP() {
 
 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;
     }
 
     if (script->hasScriptCounts()) {
         PCCounts* counts = script->maybeGetPCCounts(REGS.pc);
         if (counts)
             counts->numExec()++;
         moreInterrupts = true;
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -374,16 +374,23 @@ JSRuntime::~JSRuntime()
         CancelOffThreadParses(this);
 
         /* Clear debugging state to remove GC roots. */
         for (CompartmentsIter comp(this, SkipAtoms); !comp.done(); comp.next()) {
             if (WatchpointMap* wpmap = comp->watchpointMap)
                 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();
 
         /* Remove persistent GC roots. */
         gc.finishRoots();
 
         /*
          * Flag us as being destroyed. This allows the GC to free things like