Bug 1551140 - Add code coverage instrumentation to Baseline Interpreter. r=nbp
authorJan de Mooij <jdemooij@mozilla.com>
Wed, 15 May 2019 06:50:13 +0000
changeset 532736 cc575aa34c366538f85e98e878e8a062e3cfbb33
parent 532735 6d6a45ae267a3cc52a1b6d7db4ec224e1d313f93
child 532737 93299787ec39d62a5832b7494fd3cf5dcdcbca7c
push id11272
push userapavel@mozilla.com
push dateThu, 16 May 2019 15:28:22 +0000
treeherdermozilla-beta@2265bfc5920d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnbp
bugs1551140
milestone68.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 1551140 - Add code coverage instrumentation to Baseline Interpreter. r=nbp Differential Revision: https://phabricator.services.mozilla.com/D30877
js/src/jit/BaselineCompiler.cpp
js/src/jit/BaselineCompiler.h
js/src/jit/BaselineJIT.cpp
js/src/jit/BaselineJIT.h
js/src/jit/VMFunctions.cpp
js/src/jit/VMFunctions.h
js/src/vm/Realm.cpp
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
--- a/js/src/jit/BaselineCompiler.cpp
+++ b/js/src/jit/BaselineCompiler.cpp
@@ -791,16 +791,48 @@ void BaselineInterpreterCodeGen::emitIsD
 
   Label skipCheck;
   CodeOffset toggleOffset = masm.toggledJump(&skipCheck);
   EmitCallFrameIsDebuggeeCheck(masm);
   masm.bind(&skipCheck);
   handler.setDebuggeeCheckOffset(toggleOffset);
 }
 
+static void MaybeIncrementCodeCoverageCounter(MacroAssembler& masm,
+                                              JSScript* script,
+                                              jsbytecode* pc) {
+  if (!script->hasScriptCounts()) {
+    return;
+  }
+  PCCounts* counts = script->maybeGetPCCounts(pc);
+  uint64_t* counterAddr = &counts->numExec();
+  masm.inc64(AbsoluteAddress(counterAddr));
+}
+
+template <>
+bool BaselineCompilerCodeGen::emitHandleCodeCoverageAtPrologue() {
+  // If the main instruction is not a jump target, then we emit the
+  // corresponding code coverage counter.
+  JSScript* script = handler.script();
+  jsbytecode* main = script->main();
+  if (!BytecodeIsJumpTarget(JSOp(*main))) {
+    MaybeIncrementCodeCoverageCounter(masm, script, main);
+  }
+  return true;
+}
+
+template <>
+bool BaselineInterpreterCodeGen::emitHandleCodeCoverageAtPrologue() {
+  Label skipCoverage;
+  CodeOffset toggleOffset = masm.toggledJump(&skipCoverage);
+  masm.call(handler.codeCoverageAtPrologueLabel());
+  masm.bind(&skipCoverage);
+  return handler.codeCoverageOffsets().append(toggleOffset.offset());
+}
+
 template <>
 void BaselineCompilerCodeGen::subtractScriptSlotsSize(Register reg,
                                                       Register scratch) {
   uint32_t slotsSize = handler.script()->nslots() * sizeof(Value);
   masm.subPtr(Imm32(slotsSize), reg);
 }
 
 template <>
@@ -6105,31 +6137,33 @@ bool BaselineCodeGen<Handler>::emit_JSOP
 template <typename Handler>
 bool BaselineCodeGen<Handler>::emit_JSOP_IS_CONSTRUCTING() {
   frame.push(MagicValue(JS_IS_CONSTRUCTING));
   return true;
 }
 
 template <>
 bool BaselineCompilerCodeGen::emit_JSOP_JUMPTARGET() {
-  JSScript* script = handler.script();
-  if (!script->hasScriptCounts()) {
-    return true;
-  }
-  PCCounts* counts = script->maybeGetPCCounts(handler.pc());
-  uint64_t* counterAddr = &counts->numExec();
-  masm.inc64(AbsoluteAddress(counterAddr));
+  MaybeIncrementCodeCoverageCounter(masm, handler.script(), handler.pc());
   return true;
 }
 
 template <>
 bool BaselineInterpreterCodeGen::emit_JSOP_JUMPTARGET() {
   Register scratch1 = R0.scratchReg();
   Register scratch2 = R1.scratchReg();
 
+  Label skipCoverage;
+  CodeOffset toggleOffset = masm.toggledJump(&skipCoverage);
+  masm.call(handler.codeCoverageAtPCLabel());
+  masm.bind(&skipCoverage);
+  if (!handler.codeCoverageOffsets().append(toggleOffset.offset())) {
+    return false;
+  }
+
   // Load icIndex in scratch1.
   LoadInt32Operand(masm, PCRegAtStart, scratch1);
 
   // scratch1 := scratch1 * sizeof(ICEntry)
   static_assert(sizeof(ICEntry) == 8 || sizeof(ICEntry) == 16,
                 "shift below depends on ICEntry size");
   uint32_t shift = (sizeof(ICEntry) == 16) ? 4 : 3;
   masm.lshiftPtr(Imm32(shift), scratch1);
@@ -6461,16 +6495,20 @@ bool BaselineCodeGen<Handler>::emitProlo
   if (!emitStackCheck()) {
     return false;
   }
 
   if (!emitDebugPrologue()) {
     return false;
   }
 
+  if (!emitHandleCodeCoverageAtPrologue()) {
+    return false;
+  }
+
   if (!emitWarmUpCounterIncrement()) {
     return false;
   }
 
   warmUpCheckPrologueOffset_ = CodeOffset(masm.currentOffset());
 
   if (!emitArgumentTypeChecks()) {
     return false;
@@ -6580,24 +6618,16 @@ MethodStatus BaselineCompiler::emitBody(
     if (MOZ_UNLIKELY(!this->emit_##OP())) return Method_Error; \
     break;
         OPCODE_LIST(EMIT_OP)
 #undef EMIT_OP
     }
 
     MOZ_ASSERT(masm.framePushed() == 0);
 
-    // If the main instruction is not a jump target, then we emit the
-    // corresponding code coverage counter.
-    if (handler.pc() == script->main() && !BytecodeIsJumpTarget(op)) {
-      if (!emit_JSOP_JUMPTARGET()) {
-        return Method_Error;
-      }
-    }
-
     // Test if last instructions and stop emitting in that case.
     handler.moveToNextPC();
     if (handler.pc() >= script->codeEnd()) {
       break;
     }
 
     emittedOps++;
     lastOpUnreachable = false;
@@ -6733,16 +6763,50 @@ bool BaselineInterpreterGenerator::emitI
     masm.writeCodePointer(&cl);
     cl.target()->bind(opOffset);
     masm.addCodeLabel(cl);
   }
 
   return true;
 }
 
+void BaselineInterpreterGenerator::emitOutOfLineCodeCoverageInstrumentation() {
+  masm.bind(handler.codeCoverageAtPrologueLabel());
+#ifdef JS_USE_LINK_REGISTER
+  masm.pushReturnAddress();
+#endif
+
+  masm.Push(BaselineFrameReg);
+  masm.setupUnalignedABICall(R0.scratchReg());
+  masm.loadBaselineFramePtr(BaselineFrameReg, R0.scratchReg());
+  masm.passABIArg(R0.scratchReg());
+  masm.callWithABI(
+      JS_FUNC_TO_DATA_PTR(void*, jit::HandleCodeCoverageAtPrologue));
+  masm.Pop(BaselineFrameReg);
+
+  masm.ret();
+
+  masm.bind(handler.codeCoverageAtPCLabel());
+#ifdef JS_USE_LINK_REGISTER
+  masm.pushReturnAddress();
+#endif
+
+  masm.Push(BaselineFrameReg);
+  masm.Push(PCRegAtStart);
+  masm.setupUnalignedABICall(R0.scratchReg());
+  masm.loadBaselineFramePtr(BaselineFrameReg, R0.scratchReg());
+  masm.passABIArg(R0.scratchReg());
+  masm.passABIArg(PCRegAtStart);
+  masm.callWithABI(JS_FUNC_TO_DATA_PTR(void*, jit::HandleCodeCoverageAtPC));
+  masm.Pop(PCRegAtStart);
+  masm.Pop(BaselineFrameReg);
+
+  masm.ret();
+}
+
 bool BaselineInterpreterGenerator::generate(BaselineInterpreter& interpreter) {
   if (!emitPrologue()) {
     return false;
   }
 
   if (!emitInterpreterLoop()) {
     return false;
   }
@@ -6750,46 +6814,55 @@ bool BaselineInterpreterGenerator::gener
   if (!emitEpilogue()) {
     return false;
   }
 
   if (!emitOutOfLinePostBarrierSlot()) {
     return false;
   }
 
-  Linker linker(masm, "BaselineInterpreter");
-  if (masm.oom()) {
-    ReportOutOfMemory(cx);
-    return false;
-  }
-
-  JitCode* code = linker.newCode(cx, CodeKind::Other);
-  if (!code) {
-    return false;
-  }
-
-  // Patch loads now that we know the tableswitch base address.
-  for (CodeOffset off : tableLabels_) {
-    Assembler::PatchDataWithValueCheck(CodeLocationLabel(code, off),
-                                       ImmPtr(code->raw() + tableOffset_),
-                                       ImmPtr((void*)-1));
-  }
+  emitOutOfLineCodeCoverageInstrumentation();
+
+  {
+    Linker linker(masm, "BaselineInterpreter");
+    if (masm.oom()) {
+      ReportOutOfMemory(cx);
+      return false;
+    }
+
+    JitCode* code = linker.newCode(cx, CodeKind::Other);
+    if (!code) {
+      return false;
+    }
+
+    // Patch loads now that we know the tableswitch base address.
+    for (CodeOffset off : tableLabels_) {
+      Assembler::PatchDataWithValueCheck(CodeLocationLabel(code, off),
+                                         ImmPtr(code->raw() + tableOffset_),
+                                         ImmPtr((void*)-1));
+    }
 
 #ifdef JS_ION_PERF
-  writePerfSpewerJitCodeProfile(code, "BaselineInterpreter");
+    writePerfSpewerJitCodeProfile(code, "BaselineInterpreter");
 #endif
 
 #ifdef MOZ_VTUNE
-  vtune::MarkStub(code, "BaselineInterpreter");
+    vtune::MarkStub(code, "BaselineInterpreter");
 #endif
 
-  interpreter.init(
-      code, interpretOpOffset_, profilerEnterFrameToggleOffset_.offset(),
-      profilerExitFrameToggleOffset_.offset(),
-      handler.debuggeeCheckOffset().offset(), std::move(debugTrapOffsets_));
+    interpreter.init(
+        code, interpretOpOffset_, profilerEnterFrameToggleOffset_.offset(),
+        profilerExitFrameToggleOffset_.offset(),
+        handler.debuggeeCheckOffset().offset(), std::move(debugTrapOffsets_),
+        std::move(handler.codeCoverageOffsets()));
+  }
+
+  if (coverage::IsLCovEnabled()) {
+    interpreter.toggleCodeCoverageInstrumentationUnchecked(true);
+  }
 
   return true;
 }
 
 JitCode* JitRuntime::generateDebugTrapHandler(JSContext* cx,
                                               DebugTrapHandlerKind kind) {
   StackMacroAssembler masm;
 
--- a/js/src/jit/BaselineCompiler.h
+++ b/js/src/jit/BaselineCompiler.h
@@ -492,16 +492,18 @@ class BaselineCodeGen {
   MOZ_MUST_USE bool initEnvironmentChainHelper(const F1& initFunctionEnv,
                                                const F2& initGlobalOrEvalEnv,
                                                Register scratch);
   MOZ_MUST_USE bool initEnvironmentChain();
 
   MOZ_MUST_USE bool emitTraceLoggerEnter();
   MOZ_MUST_USE bool emitTraceLoggerExit();
 
+  MOZ_MUST_USE bool emitHandleCodeCoverageAtPrologue();
+
   void emitInitFrameFields();
   void emitIsDebuggeeCheck();
   void emitInitializeLocals();
   void emitPreInitEnvironmentChain(Register nonFunctionEnv);
 
   void emitProfilerEnterFrame();
   void emitProfilerExitFrame();
 };
@@ -651,24 +653,34 @@ class BaselineCompiler final : private B
 };
 
 // Interface used by BaselineCodeGen for BaselineInterpreterGenerator.
 class BaselineInterpreterHandler {
   InterpreterFrameInfo frame_;
   Label interpretOp_;
   CodeOffset debuggeeCheckOffset_;
 
+  // Offsets of toggled jumps for code coverage instrumentation.
+  using CodeOffsetVector = Vector<uint32_t, 0, SystemAllocPolicy>;
+  CodeOffsetVector codeCoverageOffsets_;
+  Label codeCoverageAtPrologueLabel_;
+  Label codeCoverageAtPCLabel_;
+
  public:
   using FrameInfoT = InterpreterFrameInfo;
 
   explicit BaselineInterpreterHandler(JSContext* cx, MacroAssembler& masm);
 
   InterpreterFrameInfo& frame() { return frame_; }
 
   Label* interpretOpLabel() { return &interpretOp_; }
+  Label* codeCoverageAtPrologueLabel() { return &codeCoverageAtPrologueLabel_; }
+  Label* codeCoverageAtPCLabel() { return &codeCoverageAtPCLabel_; }
+
+  CodeOffsetVector& codeCoverageOffsets() { return codeCoverageOffsets_; }
 
   // Interpreter doesn't know the script and pc statically.
   jsbytecode* maybePC() const { return nullptr; }
   bool isDefinitelyLastOp() const { return false; }
   JSScript* maybeScript() const { return nullptr; }
   JSFunction* maybeFunction() const { return nullptr; }
 
   void setDebuggeeCheckOffset(CodeOffset offset) {
@@ -711,14 +723,16 @@ class BaselineInterpreterGenerator final
  public:
   explicit BaselineInterpreterGenerator(JSContext* cx);
 
   MOZ_MUST_USE bool generate(BaselineInterpreter& interpreter);
 
  private:
   MOZ_MUST_USE bool emitInterpreterLoop();
   MOZ_MUST_USE bool emitDebugTrap();
+
+  void emitOutOfLineCodeCoverageInstrumentation();
 };
 
 }  // namespace jit
 }  // namespace js
 
 #endif /* jit_BaselineCompiler_h */
--- a/js/src/jit/BaselineJIT.cpp
+++ b/js/src/jit/BaselineJIT.cpp
@@ -1153,16 +1153,43 @@ void BaselineInterpreter::toggleDebugger
 
   // Toggle DebugTrapHandler calls.
   for (uint32_t offset : debugTrapOffsets_) {
     CodeLocationLabel trapLocation(code_, CodeOffset(offset));
     Assembler::ToggleCall(trapLocation, enable);
   }
 }
 
+void BaselineInterpreter::toggleCodeCoverageInstrumentationUnchecked(
+    bool enable) {
+  if (!JitOptions.baselineInterpreter) {
+    return;
+  }
+
+  AutoWritableJitCode awjc(code_);
+
+  for (uint32_t offset : codeCoverageOffsets_) {
+    CodeLocationLabel label(code_, CodeOffset(offset));
+    if (enable) {
+      Assembler::ToggleToCmp(label);
+    } else {
+      Assembler::ToggleToJmp(label);
+    }
+  }
+}
+
+void BaselineInterpreter::toggleCodeCoverageInstrumentation(bool enable) {
+  if (coverage::IsLCovEnabled()) {
+    // Instrumentation is enabled no matter what.
+    return;
+  }
+
+  toggleCodeCoverageInstrumentationUnchecked(enable);
+}
+
 void ICScript::purgeOptimizedStubs(JSScript* script) {
   MOZ_ASSERT(script->icScript() == this);
 
   Zone* zone = script->zone();
   if (zone->isGCSweeping() && IsAboutToBeFinalizedDuringSweep(*script)) {
     // We're sweeping and the script is dead. Don't purge optimized stubs
     // because (1) accessing CacheIRStubInfo pointers in ICStubs is invalid
     // because we may have swept them already when we started (incremental)
@@ -1421,23 +1448,25 @@ void jit::MarkActiveTypeScripts(Zone* zo
     }
   }
 }
 
 void BaselineInterpreter::init(JitCode* code, uint32_t interpretOpOffset,
                                uint32_t profilerEnterToggleOffset,
                                uint32_t profilerExitToggleOffset,
                                uint32_t debuggeeCheckOffset,
-                               DebugTrapOffsets&& debugTrapOffsets) {
+                               CodeOffsetVector&& debugTrapOffsets,
+                               CodeOffsetVector&& codeCoverageOffsets) {
   code_ = code;
   interpretOpOffset_ = interpretOpOffset;
   profilerEnterToggleOffset_ = profilerEnterToggleOffset;
   profilerExitToggleOffset_ = profilerExitToggleOffset;
   debuggeeCheckOffset_ = debuggeeCheckOffset;
   debugTrapOffsets_ = std::move(debugTrapOffsets);
+  codeCoverageOffsets_ = std::move(codeCoverageOffsets);
 }
 
 bool jit::GenerateBaselineInterpreter(JSContext* cx,
                                       BaselineInterpreter& interpreter) {
   // Temporary JitOptions check to prevent crashes for now.
   if (JitOptions.baselineInterpreter) {
     BaselineInterpreterGenerator generator(cx);
     return generator.generate(interpreter);
--- a/js/src/jit/BaselineJIT.h
+++ b/js/src/jit/BaselineJIT.h
@@ -664,38 +664,45 @@ class BaselineInterpreter {
   uint32_t profilerExitToggleOffset_ = 0;
 
   // The offset for the toggledJump instruction for the debugger's
   // IsDebuggeeCheck code in the prologue.
   uint32_t debuggeeCheckOffset_ = 0;
 
   // Offsets of toggled calls to the DebugTrapHandler trampoline (for
   // breakpoints and stepping).
-  using DebugTrapOffsets = js::Vector<uint32_t, 0, SystemAllocPolicy>;
-  DebugTrapOffsets debugTrapOffsets_;
+  using CodeOffsetVector = Vector<uint32_t, 0, SystemAllocPolicy>;
+  CodeOffsetVector debugTrapOffsets_;
+
+  // Offsets of toggled jumps for code coverage.
+  CodeOffsetVector codeCoverageOffsets_;
 
  public:
   BaselineInterpreter() = default;
 
   BaselineInterpreter(const BaselineInterpreter&) = delete;
   void operator=(const BaselineInterpreter&) = delete;
 
   void init(JitCode* code, uint32_t interpretOpOffset,
             uint32_t profilerEnterToggleOffset,
             uint32_t profilerExitToggleOffset, uint32_t debuggeeCheckOffset,
-            DebugTrapOffsets&& debugTrapOffsets);
+            CodeOffsetVector&& debugTrapOffsets,
+            CodeOffsetVector&& codeCoverageOffsets);
 
   uint8_t* codeRaw() const { return code_->raw(); }
 
   TrampolinePtr interpretOpAddr() const {
     return TrampolinePtr(codeRaw() + interpretOpOffset_);
   }
 
   void toggleProfilerInstrumentation(bool enable);
   void toggleDebuggerInstrumentation(bool enable);
+
+  void toggleCodeCoverageInstrumentationUnchecked(bool enable);
+  void toggleCodeCoverageInstrumentation(bool enable);
 };
 
 MOZ_MUST_USE bool GenerateBaselineInterpreter(JSContext* cx,
                                               BaselineInterpreter& interpreter);
 
 }  // namespace jit
 }  // namespace js
 
--- a/js/src/jit/VMFunctions.cpp
+++ b/js/src/jit/VMFunctions.cpp
@@ -1902,16 +1902,52 @@ bool HasNativeElementPure(JSContext* cx,
     vp[0].setBoolean(uint32_t(index) < obj->as<TypedArrayObject>().length());
     return true;
   }
 
   vp[0].setBoolean(false);
   return true;
 }
 
+void HandleCodeCoverageAtPC(BaselineFrame* frame, jsbytecode* pc) {
+  AutoUnsafeCallWithABI unsafe(UnsafeABIStrictness::AllowPendingExceptions);
+
+  MOZ_ASSERT(frame->runningInInterpreter());
+
+  JSScript* script = frame->script();
+  MOZ_ASSERT(pc == script->main() || BytecodeIsJumpTarget(JSOp(*pc)));
+
+  if (!script->hasScriptCounts()) {
+    if (!script->realm()->collectCoverageForDebug()) {
+      return;
+    }
+    JSContext* cx = script->runtimeFromMainThread()->mainContextFromOwnThread();
+    AutoEnterOOMUnsafeRegion oomUnsafe;
+    if (!script->initScriptCounts(cx)) {
+      oomUnsafe.crash("initScriptCounts");
+    }
+  }
+
+  PCCounts* counts = script->maybeGetPCCounts(pc);
+  MOZ_ASSERT(counts);
+  counts->numExec()++;
+}
+
+void HandleCodeCoverageAtPrologue(BaselineFrame* frame) {
+  AutoUnsafeCallWithABI unsafe;
+
+  MOZ_ASSERT(frame->runningInInterpreter());
+
+  JSScript* script = frame->script();
+  jsbytecode* main = script->main();
+  if (!BytecodeIsJumpTarget(JSOp(*main))) {
+    HandleCodeCoverageAtPC(frame, main);
+  }
+}
+
 JSString* TypeOfObject(JSObject* obj, JSRuntime* rt) {
   AutoUnsafeCallWithABI unsafe;
   JSType type = js::TypeOfObject(obj);
   return TypeName(type, *rt->commonNames);
 }
 
 bool GetPrototypeOf(JSContext* cx, HandleObject target,
                     MutableHandleValue rval) {
--- a/js/src/jit/VMFunctions.h
+++ b/js/src/jit/VMFunctions.h
@@ -1063,16 +1063,19 @@ MOZ_MUST_USE bool CallNativeGetterByValu
 MOZ_MUST_USE bool CallNativeSetter(JSContext* cx, HandleFunction callee,
                                    HandleObject obj, HandleValue rhs);
 
 MOZ_MUST_USE bool EqualStringsHelperPure(JSString* str1, JSString* str2);
 
 MOZ_MUST_USE bool CheckIsCallable(JSContext* cx, HandleValue v,
                                   CheckIsCallableKind kind);
 
+void HandleCodeCoverageAtPC(BaselineFrame* frame, jsbytecode* pc);
+void HandleCodeCoverageAtPrologue(BaselineFrame* frame);
+
 template <bool HandleMissing>
 bool GetNativeDataPropertyPure(JSContext* cx, JSObject* obj, PropertyName* name,
                                Value* vp);
 
 template <bool HandleMissing>
 bool GetNativeDataPropertyByValuePure(JSContext* cx, JSObject* obj, Value* vp);
 
 template <bool HasOwn>
--- a/js/src/vm/Realm.cpp
+++ b/js/src/vm/Realm.cpp
@@ -788,16 +788,19 @@ void Realm::setIsDebuggee() {
   if (!isDebuggee()) {
     debugModeBits_ |= IsDebuggee;
     runtimeFromMainThread()->incrementNumDebuggeeRealms();
   }
 }
 
 void Realm::unsetIsDebuggee() {
   if (isDebuggee()) {
+    if (debuggerObservesCoverage()) {
+      runtime_->decrementNumDebuggeeRealmsObservingCoverage();
+    }
     debugModeBits_ &= ~DebuggerObservesMask;
     DebugEnvironments::onRealmUnsetIsDebuggee(this);
     runtimeFromMainThread()->decrementNumDebuggeeRealms();
   }
 }
 
 void Realm::updateDebuggerObservesCoverage() {
   bool previousState = debuggerObservesCoverage();
@@ -810,19 +813,22 @@ void Realm::updateDebuggerObservesCovera
     // Interrupt any running interpreter frame. The scriptCounts are
     // allocated on demand when a script resumes its execution.
     JSContext* cx = TlsContext.get();
     for (ActivationIterator iter(cx); !iter.done(); ++iter) {
       if (iter->isInterpreter()) {
         iter->asInterpreter()->enableInterruptsUnconditionally();
       }
     }
+    runtime_->incrementNumDebuggeeRealmsObservingCoverage();
     return;
   }
 
+  runtime_->decrementNumDebuggeeRealmsObservingCoverage();
+
   // If code coverage is enabled by any other means, keep it.
   if (collectCoverage()) {
     return;
   }
 
   clearScriptCounts();
   clearScriptNames();
 }
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -117,16 +117,17 @@ JSRuntime::JSRuntime(JSRuntime* parentRu
       scriptDataLock(mutexid::RuntimeScriptData),
 #ifdef DEBUG
       activeThreadHasScriptDataAccess(false),
 #endif
       numActiveHelperThreadZones(0),
       heapState_(JS::HeapState::Idle),
       numRealms(0),
       numDebuggeeRealms_(0),
+      numDebuggeeRealmsObservingCoverage_(0),
       localeCallbacks(nullptr),
       defaultLocale(nullptr),
       profilingScripts(false),
       scriptAndCountsVector(nullptr),
       lcovOutput_(),
       jitRuntime_(nullptr),
       selfHostingGlobal_(nullptr),
       gc(thisFromCtor()),
@@ -185,16 +186,17 @@ JSRuntime::~JSRuntime() {
 
   MOZ_ASSERT(wasmInstances.lock()->empty());
 
   MOZ_ASSERT(offThreadParsesRunning_ == 0);
   MOZ_ASSERT(!offThreadParsingBlocked_);
 
   MOZ_ASSERT(numRealms == 0);
   MOZ_ASSERT(numDebuggeeRealms_ == 0);
+  MOZ_ASSERT(numDebuggeeRealmsObservingCoverage_ == 0);
 }
 
 bool JSRuntime::init(JSContext* cx, uint32_t maxbytes,
                      uint32_t maxNurseryBytes) {
 #ifdef DEBUG
   MOZ_ASSERT(!initialized_);
   initialized_ = true;
 #endif
@@ -782,16 +784,36 @@ void JSRuntime::decrementNumDebuggeeReal
   MOZ_ASSERT(numDebuggeeRealms_ > 0);
   numDebuggeeRealms_--;
 
   if (numDebuggeeRealms_ == 0) {
     jitRuntime()->baselineInterpreter().toggleDebuggerInstrumentation(false);
   }
 }
 
+void JSRuntime::incrementNumDebuggeeRealmsObservingCoverage() {
+  if (numDebuggeeRealmsObservingCoverage_ == 0) {
+    jit::BaselineInterpreter& interp = jitRuntime()->baselineInterpreter();
+    interp.toggleCodeCoverageInstrumentation(true);
+  }
+
+  numDebuggeeRealmsObservingCoverage_++;
+  MOZ_ASSERT(numDebuggeeRealmsObservingCoverage_ <= numRealms);
+}
+
+void JSRuntime::decrementNumDebuggeeRealmsObservingCoverage() {
+  MOZ_ASSERT(numDebuggeeRealmsObservingCoverage_ > 0);
+  numDebuggeeRealmsObservingCoverage_--;
+
+  if (numDebuggeeRealmsObservingCoverage_ == 0) {
+    jit::BaselineInterpreter& interp = jitRuntime()->baselineInterpreter();
+    interp.toggleCodeCoverageInstrumentation(false);
+  }
+}
+
 bool js::CurrentThreadCanAccessRuntime(const JSRuntime* rt) {
   return rt->mainContextFromAnyThread() == TlsContext.get();
 }
 
 bool js::CurrentThreadCanAccessZone(Zone* zone) {
   // Helper thread zones can only be used by their owning thread.
   if (zone->usedByHelperThread()) {
     return zone->ownedByCurrentHelperThread();
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -522,24 +522,30 @@ struct JSRuntime : public js::MallocProv
   // off-thread context realms, so it isn't necessarily equal to the
   // number of realms visited by RealmsIter.
   js::MainThreadData<size_t> numRealms;
 
  private:
   // Number of debuggee realms in the runtime.
   js::MainThreadData<size_t> numDebuggeeRealms_;
 
+  // Number of debuggee realms in the runtime observing code coverage.
+  js::MainThreadData<size_t> numDebuggeeRealmsObservingCoverage_;
+
  public:
   void incrementNumDebuggeeRealms();
   void decrementNumDebuggeeRealms();
 
   size_t numDebuggeeRealms() const {
     return numDebuggeeRealms_;
   }
 
+  void incrementNumDebuggeeRealmsObservingCoverage();
+  void decrementNumDebuggeeRealmsObservingCoverage();
+
   /* Locale-specific callbacks for string conversion. */
   js::MainThreadData<const JSLocaleCallbacks*> localeCallbacks;
 
   /* Default locale for Internationalization API */
   js::MainThreadData<js::UniqueChars> defaultLocale;
 
   /* If true, new scripts must be created with PC counter information. */
   js::MainThreadOrIonCompileData<bool> profilingScripts;