Bug 1191289 part 1 - Add a JSFriendApi function to produce LCOV information about the current compartment. r=bhackett
authorNicolas B. Pierron <nicolas.b.pierron@mozilla.com>
Sat, 29 Aug 2015 01:32:37 +0200
changeset 259915 8c305052960d89707587d12e37431019be00ed64
parent 259914 bc8f81a31df45d2ed607e46741657008acb27c35
child 259916 70a4840a6680e14e9bbbaa8d84e06bf9c6da4650
push id29296
push userryanvm@gmail.com
push dateSun, 30 Aug 2015 19:45:10 +0000
treeherdermozilla-central@2ad5077d86ba [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbhackett
bugs1191289
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 1191289 part 1 - Add a JSFriendApi function to produce LCOV information about the current compartment. r=bhackett
js/src/builtin/TestingFunctions.cpp
js/src/jit-test/tests/coverage/simple.js
js/src/jsfriendapi.h
js/src/jsopcode.cpp
js/src/jsscript.cpp
js/src/shell/js.cpp
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -2706,16 +2706,66 @@ SetARMHwCapFlags(JSContext* cx, unsigned
 
     jit::ParseARMHwCapFlags(flagsList.ptr());
 #endif
 
     args.rval().setUndefined();
     return true;
 }
 
+static bool
+GetLcovInfo(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (args.length() > 1) {
+        JS_ReportError(cx, "Wrong number of arguments");
+        return false;
+    }
+
+    RootedObject global(cx);
+    if (args.hasDefined(0)) {
+        global = ToObject(cx, args[0]);
+        if (!global) {
+            JS_ReportError(cx, "First argument should be an object");
+            return false;
+        }
+        global = CheckedUnwrap(global);
+        if (!global) {
+            JS_ReportError(cx, "Permission denied to access global");
+            return false;
+        }
+        if (!global->is<GlobalObject>()) {
+            JS_ReportError(cx, "Argument must be a global object");
+            return false;
+        }
+    } else {
+        global = JS::CurrentGlobalOrNull(cx);
+    }
+
+    size_t length = 0;
+    char* content = nullptr;
+    {
+        AutoCompartment ac(cx, global);
+        content = js::GetCodeCoverageSummary(cx, &length);
+    }
+
+    if (!content)
+        return false;
+
+    JSString* str = JS_NewStringCopyN(cx, content, length);
+    free(content);
+
+    if (!str)
+        return false;
+
+    args.rval().setString(str);
+    return true;
+}
+
 static const JSFunctionSpecWithHelp TestingFunctions[] = {
     JS_FN_HELP("gc", ::GC, 0, 0,
 "gc([obj] | 'compartment' [, 'shrinking'])",
 "  Run the garbage collector. When obj is given, GC only its compartment.\n"
 "  If 'compartment' is given, GC any compartments that were scheduled for\n"
 "  GC via schedulegc.\n"
 "  If 'shrinking' is passed as the optional second argument, perform a\n"
 "  shrinking GC rather than a normal GC."),
@@ -3148,16 +3198,21 @@ gc::ZealModeHelpText),
 "    'minorGC' - run a nursery collection\n"
 "    'majorGC' - run a major collection, nesting up to a given 'depth'\n"),
 
     JS_FN_HELP("setARMHwCapFlags", SetARMHwCapFlags, 1, 0,
 "setARMHwCapFlags(\"flag1,flag2 flag3\")",
 "  On non-ARM, no-op. On ARM, set the hardware capabilities. The list of \n"
 "  flags is available by calling this function with \"help\" as the flag's name"),
 
+    JS_FN_HELP("getLcovInfo", GetLcovInfo, 1, 0,
+"getLcovInfo(global)",
+"  Generate LCOV tracefile for the given compartment.  If no global are provided then\n"
+"  the current global is used as the default one.\n"),
+
     JS_FS_HELP_END
 };
 
 static const JSPropertySpec TestingProperties[] = {
     JS_PSG("timesAccessed", TimesAccessed, 0),
     JS_PS_END
 };
 
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/coverage/simple.js
@@ -0,0 +1,153 @@
+// |jit-test| --code-coverage;
+
+// Currently the Jit integration has a few issues, let's keep this test
+// case deterministic.
+//
+//  - Baseline OSR increments the loop header twice.
+//  - Ion is not updating any counter yet.
+//
+if (getJitCompilerOptions()["ion.warmup.trigger"] != 30)
+  setJitCompilerOption("ion.warmup.trigger", 30);
+if (getJitCompilerOptions()["baseline.warmup.trigger"] != 10)
+  setJitCompilerOption("baseline.warmup.trigger", 10);
+
+function checkLcov(fun) {
+  var keys = [ "TN", "SF", "FN", "FNDA", "FNF", "FNH", "BRDA", "BRF", "BRH", "DA", "LF", "LH" ];
+  function startsWithKey(s) {
+    for (k of keys) {
+      if (s.startsWith(k))
+        return true;
+    }
+    return false;
+  };
+
+  // Extract the body of the function, as the code to be executed.
+  var source = fun.toSource();
+  source = source.slice(source.indexOf('{') + 1, source.lastIndexOf('}'));
+
+  // Extract comment starting with the previous keys, as a reference of the
+  // output expected from getLcovInfo.
+  var lcovRef = [];
+  var currLine = 0;
+  var currFun = "<badfunction>";
+  for (var line of source.split('\n')) {
+    currLine++;
+    for (var comment of line.split("//").slice(1)) {
+      if (!startsWithKey(comment))
+        continue;
+      comment = comment.trim();
+      if (comment.startsWith("FN:"))
+        currFun = comment.split(',')[1];
+      comment = comment.replace('$', currLine);
+      comment = comment.replace('%', currFun);
+      lcovRef.push(comment);
+    }
+  }
+
+  // Evaluate the code, and generate the Lcov result from the execution.
+  var g = newGlobal();
+  g.eval(source);
+  var lcovResRaw = getLcovInfo(g);
+
+  // Check that all the lines are present the result.
+  var lcovRes = lcovResRaw.split('\n');
+  for (ref of lcovRef) {
+    if (lcovRes.indexOf(ref) == -1) {
+      print("Cannot find `" + ref + "` in the following Lcov result:\n", lcovResRaw);
+      assertEq(true, false);
+    }
+  }
+}
+
+checkLcov(function () { //FN:$,top-level //FNDA:1,%
+  ",".split(','); //DA:$,1
+  //FNF:1
+  //FNH:1
+  //LF:1
+  //LH:1
+});
+
+checkLcov(function () { //FN:$,top-level //FNDA:1,%
+  function f() {    //FN:$,f
+    ",".split(','); //DA:$,0
+  }
+  ",".split(',');   //DA:$,1
+  //FNF:2
+  //FNH:1
+  //LF:2
+  //LH:1
+});
+
+checkLcov(function () { //FN:$,top-level //FNDA:1,%
+  function f() {    //FN:$,f //FNDA:1,%
+    ",".split(','); //DA:$,1
+  }
+  f();              //DA:$,1
+  //FNF:2
+  //FNH:2
+  //LF:2
+  //LH:2
+});
+
+checkLcov(function () { //FN:$,top-level //FNDA:1,%
+  var l = ",".split(','); //DA:$,1
+  if (l.length == 3)      //DA:$,1
+    l.push('');           //DA:$,0
+  else
+    l.pop();              //DA:$,1
+  //FNF:1
+  //FNH:1
+  //LF:4
+  //LH:3
+  //BRF:1
+  //BRH:1
+});
+
+checkLcov(function () { //FN:$,top-level //FNDA:1,%
+  var l = ",".split(','); //DA:$,1
+  if (l.length == 2)      //DA:$,1
+    l.push('');           //DA:$,1
+  else
+    l.pop();              //DA:$,0
+  //FNF:1
+  //FNH:1
+  //LF:4
+  //LH:3
+  //BRF:1
+  //BRH:1
+});
+
+checkLcov(function () { //FN:$,top-level //FNDA:1,%
+  var l = ",".split(','); //DA:$,1
+  if (l.length == 2)      //DA:$,1
+    l.push('');           //DA:$,1
+  else {
+    if (l.length == 1)    //DA:$,0
+      l.pop();            //DA:$,0
+  }
+  //FNF:1
+  //FNH:1
+  //LF:5
+  //LH:3
+  //BRF:2
+  //BRH:1
+});
+
+checkLcov(function () { //FN:$,top-level //FNDA:1,%
+  function f(i) { //FN:$,f //FNDA:2,%
+    var x = 0;    //DA:$,2
+    while (i--) { // Currently OSR wrongly count the loop header twice.
+                  // So instead of DA:$,12 , we have DA:$,13 .
+      x += i;     //DA:$,10
+      x = x / 2;  //DA:$,10
+    }
+    return x;     //DA:$,2
+    //BRF:1
+    //BRH:1
+  }
+
+  f(5);           //DA:$,1
+  f(5);           //DA:$,1
+  //FNF:2
+  //FNH:2
+});
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -1048,16 +1048,25 @@ JS_FRIEND_API(size_t)
 GetPCCountScriptCount(JSContext* cx);
 
 JS_FRIEND_API(JSString*)
 GetPCCountScriptSummary(JSContext* cx, size_t script);
 
 JS_FRIEND_API(JSString*)
 GetPCCountScriptContents(JSContext* cx, size_t script);
 
+// Generate lcov trace file content for the current compartment, and allocate a
+// new buffer and return the content in it, the size of the newly allocated
+// content within the buffer would be set to the length out-param.
+//
+// In case of out-of-memory, this function returns nullptr and does not set any
+// value to the length out-param.
+JS_FRIEND_API(char*)
+GetCodeCoverageSummary(JSContext* cx, size_t* length);
+
 JS_FRIEND_API(bool)
 ContextHasOutstandingRequests(const JSContext* cx);
 
 typedef void
 (* ActivityCallback)(void* arg, bool active);
 
 /*
  * Sets a callback that is run whenever the runtime goes idle - the
--- a/js/src/jsopcode.cpp
+++ b/js/src/jsopcode.cpp
@@ -5,19 +5,23 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*
  * JS bytecode descriptors, disassemblers, and (expression) decompilers.
  */
 
 #include "jsopcodeinlines.h"
 
+#define __STDC_FORMAT_MACROS
+
 #include "mozilla/SizePrintfMacros.h"
 
+#include <algorithm>
 #include <ctype.h>
+#include <inttypes.h>
 #include <stdio.h>
 #include <string.h>
 
 #include "jsapi.h"
 #include "jsatom.h"
 #include "jscntxt.h"
 #include "jscompartment.h"
 #include "jsfun.h"
@@ -1934,8 +1938,324 @@ js::GetPCCountScriptContents(JSContext* 
     {
         AutoCompartment ac(cx, &script->global());
         if (!GetPCCountJSON(cx, sac, buf))
             return nullptr;
     }
 
     return buf.finishString();
 }
+
+static bool
+LcovWriteScriptName(GenericPrinter& out, JSScript* script)
+{
+    JSFunction* fun = script->functionNonDelazifying();
+    if (fun && fun->displayAtom())
+        return EscapedStringPrinter(out, fun->displayAtom(), 0);
+    out.printf("top-level");
+    return true;
+}
+
+struct LcovSourceFile
+{
+    const char* filename;
+
+    LSprinter outFN;
+    LSprinter outFNDA;
+    size_t numFunctionsFound;
+    size_t numFunctionsHit;
+
+    LSprinter outBRDA;
+    size_t numBranchesFound;
+    size_t numBranchesHit;
+
+    LSprinter outDA;
+    size_t numLinesInstrumented;
+    size_t numLinesHit;
+
+    LcovSourceFile(LifoAlloc* alloc, JSScript *script)
+      : filename(script->filename()),
+        outFN(alloc),
+        outFNDA(alloc),
+        numFunctionsFound(0),
+        numFunctionsHit(0),
+        outBRDA(alloc),
+        numBranchesFound(0),
+        numBranchesHit(0),
+        outDA(alloc),
+        numLinesInstrumented(0),
+        numLinesHit(0)
+    { }
+};
+
+static bool
+LcovWriteScript(JSContext* cx, LcovSourceFile& lsf, JSScript* script)
+{
+    lsf.numFunctionsFound++;
+    lsf.outFN.printf("FN:%d,", script->lineno());
+    if (!LcovWriteScriptName(lsf.outFN, script))
+        return false;
+    lsf.outFN.put("\n", 1);
+
+    uint64_t hits = 0;
+    ScriptCounts* sc = nullptr;
+    if (script->hasScriptCounts()) {
+        sc = &script->getScriptCounts();
+        lsf.numFunctionsHit++;
+        const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(script->main()));
+        lsf.outFNDA.printf("FNDA:%" PRIu64 ",", counts->numExec());
+        if (!LcovWriteScriptName(lsf.outFNDA, script))
+            return false;
+        lsf.outFNDA.put("\n", 1);
+
+        // Set the hit count of the pre-main code to 1, if the function ever got
+        // visited.
+        hits = 1;
+    }
+
+    jsbytecode* snpc = script->code();
+    jssrcnote* sn = script->notes();
+    if (!SN_IS_TERMINATOR(sn))
+        snpc += SN_DELTA(sn);
+
+    size_t lineno = script->lineno();
+    jsbytecode* end = script->codeEnd();
+    size_t blockId = 0;
+    for (jsbytecode* pc = script->code(); pc != end; pc = GetNextPc(pc)) {
+        JSOp op = JSOp(*pc);
+        bool jump = IsJumpOpcode(op);
+        bool fallsthrough = BytecodeFallsThrough(op);
+
+        // If the current script & pc has a hit-count report, then update the
+        // current number of hits.
+        if (sc) {
+            const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(pc));
+            if (counts)
+                hits = counts->numExec();
+        }
+
+        // If we have additional source notes, walk all the source notes of the
+        // current pc.
+        if (snpc <= pc) {
+            size_t oldLine = lineno;
+            while (!SN_IS_TERMINATOR(sn) && snpc <= pc) {
+                SrcNoteType type = (SrcNoteType) SN_TYPE(sn);
+                if (type == SRC_SETLINE)
+                    lineno = size_t(GetSrcNoteOffset(sn, 0));
+                else if (type == SRC_NEWLINE)
+                    lineno++;
+
+                sn = SN_NEXT(sn);
+                snpc += SN_DELTA(sn);
+            }
+
+            if (oldLine != lineno && fallsthrough) {
+                lsf.outDA.printf("DA:%d,%" PRIu64 "\n", lineno, hits);
+
+                // Count the number of lines instrumented & hit.
+                lsf.numLinesInstrumented++;
+                if (hits)
+                    lsf.numLinesHit++;
+            }
+        }
+
+        // If the current pc corresponds to a conditional jump instruction, then reports
+        // branch hits.
+        if (jump && fallsthrough) {
+            jsbytecode* target = pc + GET_JUMP_OFFSET(pc);
+            jsbytecode* fallthroughTarget = GetNextPc(pc);
+            uint64_t fallthroughHits = 0;
+            if (sc) {
+                const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(fallthroughTarget));
+                if (counts)
+                    fallthroughHits = counts->numExec();
+            }
+
+            size_t targetId = script->pcToOffset(target);
+            uint64_t taken = hits - fallthroughHits;
+            lsf.outBRDA.printf("BRDA:%d,%d,%d,", lineno, blockId, targetId);
+            if (hits)
+                lsf.outBRDA.printf("%d\n", taken);
+            else
+                lsf.outBRDA.put("-\n", 2);
+
+            // Count the number of branches, and the number of branches hit.
+            lsf.numBranchesFound++;
+            if (hits)
+                lsf.numBranchesHit++;
+
+            // Update the blockId when there is a discontinuity.
+            blockId = script->pcToOffset(fallthroughTarget);
+        }
+    }
+
+    return true;
+}
+
+static bool
+GenerateLcovInfo(JSContext* cx, JSCompartment* comp, GenericPrinter& out)
+{
+    JSRuntime* rt = cx->runtime();
+
+    // Collect the list of scripts which are part of the current compartment.
+    AutoScriptVector topScripts(cx);
+    for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) {
+        for (ZoneCellIter i(zone, AllocKind::SCRIPT); !i.done(); i.next()) {
+            JSScript* script = i.get<JSScript>();
+            if (script->compartment() != comp)
+                continue;
+
+            if (script->functionNonDelazifying())
+                continue;
+
+            if (!topScripts.append(script))
+                return false;
+        }
+    }
+
+    if (topScripts.length() == 0)
+        return true;
+
+    // Sort the information to avoid generating multiple file entries, and to
+    // generate functions in the right order.
+    auto lessFun = [](const JSScript* lhs, const JSScript* rhs) -> bool {
+        int d = strcmp(lhs->filename(), rhs->filename());
+        /*
+          This should not be necessary as we are supposed to have only the
+          top-level script.
+
+          d = (d != 0) ? d : lhs->lineno() - rhs->lineno();
+          d = (d != 0) ? d : lhs->column() - rhs->column();
+        */
+        return d < 0;
+    };
+    std::sort(topScripts.begin(), topScripts.end(), lessFun);
+
+    // lcov trace files are starting with an optional test case name, that we
+    // recycle to be a compartment name.
+    out.put("TN:");
+    if (rt->compartmentNameCallback) {
+        char name[1024];
+        (*rt->compartmentNameCallback)(rt, comp, name, sizeof(name));
+        for (char *s = name; s < name + sizeof(name) && *s; s++) {
+            if (('a' <= *s && *s <= 'z') ||
+                ('A' <= *s && *s <= 'Z') ||
+                ('0' <= *s && *s <= '9'))
+            {
+                out.put(s, 1);
+                continue;
+            }
+            out.printf("_%p", (void*) size_t(*s));
+        }
+        out.put("\n", 1);
+    } else {
+        out.printf("Compartment_%p%p\n", (void*) size_t('_'), comp);
+    }
+
+    // For each source file
+    LifoAlloc printerAlloc(4096);
+    for (JSScript* topLevel: topScripts) {
+        LifoAllocScope printerScope(&printerAlloc);
+        LcovSourceFile lsf(&printerAlloc, topLevel);
+
+        // We found the top-level script, visit all the functions reachable
+        // from the top-level function.
+        AutoScriptVector queue(cx);
+        if (!queue.append(topLevel))
+            return false;
+
+        RootedScript script(cx);
+        do {
+            script = queue.popCopy();
+
+            // Code the current script before pushing.
+            if (!LcovWriteScript(cx, lsf, script))
+                return false;
+
+            // Iterate from the last to the first object in order to have
+            // the functions them visited in the opposite order when popping
+            // elements from the stack of remaining scripts, such that the
+            // functions are more-less listed with increasing line numbers.
+            if (!script->hasObjects())
+                continue;
+            size_t idx = script->objects()->length;
+            while (idx--) {
+                JSObject* obj = script->getObject(idx);
+
+                // Only continue on JSFunction objects.
+                if (!obj->is<JSFunction>())
+                    continue;
+                JSFunction& fun = obj->as<JSFunction>();
+
+                // Let's skip asm.js for now.
+                if (!fun.isInterpreted())
+                    continue;
+
+                // Queue the script in the list of script associated to the
+                // current source.
+                if (!queue.append(fun.getOrCreateScript(cx)))
+                    return false;
+            }
+        } while (!queue.empty());
+
+        if (lsf.outFN.hadOutOfMemory() ||
+            lsf.outFNDA.hadOutOfMemory() ||
+            lsf.outBRDA.hadOutOfMemory() ||
+            lsf.outDA.hadOutOfMemory())
+        {
+            out.reportOutOfMemory();
+            return false;
+        }
+
+        out.printf("SF:%s\n", lsf.filename);
+
+        lsf.outFN.exportInto(out);
+        lsf.outFNDA.exportInto(out);
+        out.printf("FNF:%d\n", lsf.numFunctionsFound);
+        out.printf("FNH:%d\n", lsf.numFunctionsHit);
+
+        lsf.outBRDA.exportInto(out);
+        out.printf("BRF:%d\n", lsf.numBranchesFound);
+        out.printf("BRH:%d\n", lsf.numBranchesHit);
+
+        lsf.outDA.exportInto(out);
+        out.printf("LF:%d\n", lsf.numLinesInstrumented);
+        out.printf("LH:%d\n", lsf.numLinesHit);
+
+        out.put("end_of_record\n");
+    }
+
+    if (out.hadOutOfMemory())
+        return false;
+    return true;
+}
+
+JS_FRIEND_API(char*)
+js::GetCodeCoverageSummary(JSContext* cx, size_t* length)
+{
+    Sprinter out(cx);
+
+    if (!out.init())
+        return nullptr;
+
+    if (!GenerateLcovInfo(cx, cx->compartment(), out)) {
+        JS_ReportOutOfMemory(cx);
+        return nullptr;
+    }
+
+    if (out.hadOutOfMemory()) {
+        JS_ReportOutOfMemory(cx);
+        return nullptr;
+    }
+
+    ptrdiff_t len = out.stringEnd() - out.string();
+    char* res = cx->pod_malloc<char>(len + 1);
+    if (!res) {
+        JS_ReportOutOfMemory(cx);
+        return nullptr;
+    }
+
+    js_memcpy(res, out.string(), len);
+    res[len] = 0;
+    if (length)
+        *length = len;
+    return res;
+}
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -1358,16 +1358,23 @@ static inline ScriptCountsMap::Ptr GetSc
 {
     MOZ_ASSERT(script->hasScriptCounts());
     ScriptCountsMap* map = script->compartment()->scriptCountsMap;
     ScriptCountsMap::Ptr p = map->lookup(script);
     MOZ_ASSERT(p);
     return p;
 }
 
+ScriptCounts&
+JSScript::getScriptCounts()
+{
+    ScriptCountsMap::Ptr p = GetScriptCountsMapEntry(this);
+    return p->value();
+}
+
 js::PCCounts*
 ScriptCounts::maybeGetPCCounts(size_t offset) {
     PCCounts searched = PCCounts(offset);
     PCCounts* elem = std::lower_bound(pcCounts_.begin(), pcCounts_.end(), searched);
     if (elem == pcCounts_.end() || elem->pcOffset() != offset)
         return nullptr;
     return elem;
 }
@@ -1405,23 +1412,16 @@ JSScript::setIonScript(JSContext* maybec
     MOZ_ASSERT_IF(ionScript != ION_DISABLED_SCRIPT, !baselineScript()->hasPendingIonBuilder());
     if (hasIonScript())
         js::jit::IonScript::writeBarrierPre(zone(), ion);
     ion = ionScript;
     MOZ_ASSERT_IF(hasIonScript(), hasBaselineScript());
     updateBaselineOrIonRaw(maybecx);
 }
 
-ScriptCounts&
-JSScript::getScriptCounts()
-{
-    ScriptCountsMap::Ptr p = GetScriptCountsMapEntry(this);
-    return p->value();
-}
-
 js::PCCounts*
 JSScript::maybeGetPCCounts(jsbytecode* pc) {
     MOZ_ASSERT(containsPC(pc));
     return getScriptCounts().maybeGetPCCounts(pcToOffset(pc));
 }
 
 const js::PCCounts*
 JSScript::maybeGetThrowCounts(jsbytecode* pc) {
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -126,16 +126,17 @@ static size_t gMaxStackSize = 128 * size
 static double MAX_TIMEOUT_INTERVAL = 1800.0;
 static double gTimeoutInterval = -1.0;
 static Atomic<bool> gServiceInterrupt;
 static JS::PersistentRootedValue gInterruptFunc;
 
 static bool gLastWarningEnabled = false;
 static JS::PersistentRootedValue gLastWarning;
 
+static bool enableCodeCoverage = false;
 static bool enableDisassemblyDumps = false;
 static bool offthreadCompilation = false;
 static bool enableBaseline = false;
 static bool enableIon = false;
 static bool enableAsmJS = false;
 static bool enableNativeRegExp = false;
 static bool enableUnboxedArrays = false;
 #ifdef JS_GC_ZEAL
@@ -6036,17 +6037,19 @@ SetRuntimeOptions(JSRuntime* rt, const O
     int32_t stopAt = op.getIntOption("mips-sim-stop-at");
     if (stopAt >= 0)
         jit::Simulator::StopSimAt = stopAt;
 #endif
 
     reportWarnings = op.getBoolOption('w');
     compileOnly = op.getBoolOption('c');
     printTiming = op.getBoolOption('b');
-    rt->profilingScripts = enableDisassemblyDumps = op.getBoolOption('D');
+    enableCodeCoverage = op.getBoolOption("code-coverage");
+    enableDisassemblyDumps = op.getBoolOption('D');
+    rt->profilingScripts = enableCodeCoverage || enableDisassemblyDumps;
 
     jsCacheDir = op.getStringOption("js-cache");
     if (jsCacheDir) {
         if (!op.getBoolOption("no-js-cache-per-process"))
             jsCacheDir = JS_smprintf("%s/%u", jsCacheDir, (unsigned)getpid());
         else
             jsCacheDir = JS_strdup(rt, jsCacheDir);
         jsCacheAsmJSPath = JS_smprintf("%s/asmjs.cache", jsCacheDir);
@@ -6075,17 +6078,17 @@ SetWorkerRuntimeOptions(JSRuntime* rt)
 {
     // Copy option values from the main thread.
     JS::RuntimeOptionsRef(rt).setBaseline(enableBaseline)
                              .setIon(enableIon)
                              .setAsmJS(enableAsmJS)
                              .setNativeRegExp(enableNativeRegExp)
                              .setUnboxedArrays(enableUnboxedArrays);
     rt->setOffthreadIonCompilationEnabled(offthreadCompilation);
-    rt->profilingScripts = enableDisassemblyDumps;
+    rt->profilingScripts = enableCodeCoverage || enableDisassemblyDumps;
 
 #ifdef JS_GC_ZEAL
     if (*gZealStr)
         rt->gc.parseAndSetZeal(gZealStr);
 #endif
 }
 
 static int
@@ -6216,16 +6219,17 @@ main(int argc, char** argv, char** envp)
                                "Enable the JS cache by specifying the path of the directory to use "
                                "to hold cache files")
         || !op.addBoolOption('\0', "no-js-cache-per-process",
                                "Deactivates cache per process. Otherwise, generate a separate cache"
                                "sub-directory for this process inside the cache directory"
                                "specified by --js-cache. This cache directory will be removed"
                                "when the js shell exits. This is useful for running tests in"
                                "parallel.")
+        || !op.addBoolOption('\0', "code-coverage", "Enable code coverage instrumentation.")
 #ifdef DEBUG
         || !op.addBoolOption('O', "print-alloc", "Print the number of allocations at exit")
 #endif
         || !op.addOptionalStringArg("script", "A script to execute (after all options)")
         || !op.addOptionalMultiStringArg("scriptArgs",
                                          "String arguments to bind as |scriptArgs| in the "
                                          "shell's global")
         || !op.addIntOption('\0', "thread-count", "COUNT", "Use COUNT auxiliary threads "