Bug 1176880 part 2 - Add Debugger.Script.getOffsetsCoverage. r=shu
authorNicolas B. Pierron <nicolas.b.pierron@mozilla.com>
Wed, 16 Sep 2015 21:11:35 +0200
changeset 295473 b617f0a6a77c649d1f4c721b0548501a6f54c2a0
parent 295472 1ac20ebb3bd71fbdc7cf901c637d1af16d549cf8
child 295474 26ca3396791f227f6402bb51a58ff727784ca939
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 2 - Add Debugger.Script.getOffsetsCoverage. r=shu
js/src/jit-test/tests/coverage/simple.js
js/src/jit-test/tests/debug/Script-getOffsetsCoverage-01.js
js/src/vm/Debugger.cpp
--- a/js/src/jit-test/tests/coverage/simple.js
+++ b/js/src/jit-test/tests/coverage/simple.js
@@ -6,16 +6,25 @@
 //  - 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);
 
+/*
+ * These test cases are annotated with the output produced by LCOV [1].  Comment
+ * starting with //<key> without any spaces are used as a reference for the code
+ * coverage output.  Any "$" in these line comments are replaced by the current
+ * line number, and any "%" are replaced with the current function name (defined
+ * by the FN key).
+ *
+ * [1]  http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
+ */
 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;
@@ -208,8 +217,11 @@ checkLcov(function () { //FN:$,top-level
   }
   //FNF:2
   //FNH:2
   //LF:7
   //LH:5
   //BRF:0
   //BRH:0
 });
+
+// If you add a test case here, do the same in
+// jit-test/tests/debug/Script-getOffsetsCoverage-01.js
copy from js/src/jit-test/tests/coverage/simple.js
copy to js/src/jit-test/tests/debug/Script-getOffsetsCoverage-01.js
--- a/js/src/jit-test/tests/coverage/simple.js
+++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-01.js
@@ -1,145 +1,221 @@
-// |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) {
+/*
+ * These test cases are annotated with the output produced by LCOV [1].  Comment
+ * starting with //<key> without any spaces are used as a reference for the code
+ * coverage output.  Any "$" in these line comments are replaced by the current
+ * line number, and any "%" are replaced with the current function name (defined
+ * by the FN key).
+ *
+ * [1]  http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
+ */
+function checkGetOffsetsCoverage(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.
+  // Extract comment starting with the previous keys, as a reference.
   var lcovRef = [];
   var currLine = 0;
-  var currFun = "<badfunction>";
+  var currFun = [{name: "top-level", braces: 1}];
   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);
+        currFun.push({ name: comment.split(',')[1], braces: 0 });
+      var name = currFun[currFun.length - 1].name;
+      if (!comment.startsWith("DA:"))
+        continue;
+      comment = {
+        offset: null,
+        lineNumber: currLine,
+        columnNumber: null,
+        count: comment.split(",")[1] | 0,
+        script: (name == "top-level" ? undefined : name)
+      };
       lcovRef.push(comment);
     }
+
+    var deltaBraces = line.split('{').length - line.split('}').length;
+    currFun[currFun.length - 1].braces += deltaBraces;
+    if (currFun[currFun.length - 1].braces == 0)
+      currFun.pop();
   }
 
-  // Evaluate the code, and generate the Lcov result from the execution.
+  // Create a new global and instrument it with a debugger, to find all scripts,
+  // created in the current global.
   var g = newGlobal();
+  var dbg = Debugger(g);
+  dbg.collectCoverageInfo = true;
+
+  var topLevel = null;
+  dbg.onNewScript = function (s) {
+    topLevel = s;
+    dbg.onNewScript = function () {};
+  };
+
+  // Evaluate the code, and collect the hit counts for each scripts / lines.
   g.eval(source);
-  var lcovResRaw = getLcovInfo(g);
+
+  var coverageRes = [];
+  function collectCoverage(s) {
+    var res = s.getOffsetsCoverage();
+    if (res == null)
+      res = [{
+        offset: null,
+        lineNumber: null,
+        columnNumber: null,
+        script: s.displayName,
+        count: 0
+      }];
+    else {
+      res.map(function (e) {
+        e.script = s.displayName;
+        return e;
+      });
+    }
+    coverageRes.push(res);
+    s.getChildScripts().forEach(collectCoverage);
+  };
+  collectCoverage(topLevel);
+  coverageRes = [].concat(...coverageRes);
 
   // Check that all the lines are present the result.
-  var lcovRes = lcovResRaw.split('\n');
+  function match(ref) {
+    return function (entry) {
+      return ref.lineNumber == entry.lineNumber && ref.script == entry.script;
+    }
+  }
+  function ppObj(entry) {
+    var str = "{";
+    for (var k in entry) {
+      if (entry[k] != null)
+        str += " '" + k + "': " + entry[k] + ",";
+    }
+    str += "}";
+    return str;
+  }
   for (ref of lcovRef) {
-    if (lcovRes.indexOf(ref) == -1) {
-      print("Cannot find `" + ref + "` in the following Lcov result:\n", lcovResRaw);
+    var res = coverageRes.find(match(ref));
+    if (!res) {
+      // getOffsetsCoverage returns null if we have no result for the
+      // script. We added a fake entry with an undefined lineNumber, which is
+      // used to match against the modified reference.
+      var missRef = Object.create(ref);
+      missRef.lineNumber = null;
+      res = coverageRes.find(match(missRef));
+    }
+
+    if (!res || res.count != ref.count) {
+      print("Cannot find `" + ppObj(ref) + "` in the following results:\n", coverageRes.map(ppObj).join("\n"));
       print("In the following source:\n", source);
       assertEq(true, false);
     }
   }
 }
 
-checkLcov(function () { //FN:$,top-level //FNDA:1,%
+checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,%
   ",".split(','); //DA:$,1
   //FNF:1
   //FNH:1
   //LF:1
   //LH:1
 });
 
-checkLcov(function () { //FN:$,top-level //FNDA:1,%
+checkGetOffsetsCoverage(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,%
+checkGetOffsetsCoverage(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,%
+checkGetOffsetsCoverage(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,%
+checkGetOffsetsCoverage(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,%
+checkGetOffsetsCoverage(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,%
+checkGetOffsetsCoverage(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
@@ -148,17 +224,17 @@ checkLcov(function () { //FN:$,top-level
   }
 
   f(5);           //DA:$,1
   f(5);           //DA:$,1
   //FNF:2
   //FNH:2
 });
 
-checkLcov(function () { //FN:$,top-level //FNDA:1,%
+checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,%
   try {                     //DA:$,1
     var l = ",".split(','); //DA:$,1
     if (l.length == 2) {    //DA:$,1 // BRDA:$,0
       l.push('');           //DA:$,1
       throw l;              //DA:$,1
     }
     l.pop();                //DA:$,0
   } catch (x) {             //DA:$,1
@@ -168,17 +244,17 @@ checkLcov(function () { //FN:$,top-level
   //FNH:1
   //LF:9 // Expected LF:8 , Apparently if the first statement is a try, the
          // statement following the "try{" statement is visited twice.
   //LH:8 // Expected LH:7
   //BRF:1
   //BRH:1
 });
 
-checkLcov(function () { //FN:$,top-level //FNDA:1,%
+checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,%
   var l = ",".split(',');   //DA:$,1
   try {                     //DA:$,1
     try {                   //DA:$,1
       if (l.length == 2) {  //DA:$,1 // BRDA:$,0
         l.push('');         //DA:$,1
         throw l;            //DA:$,1
       }
       l.pop();              //DA:$,0 // BRDA:$,-
@@ -190,17 +266,17 @@ checkLcov(function () { //FN:$,top-level
   //FNF:1
   //FNH:1
   //LF:10
   //LH:9
   //BRF:2
   //BRH:1
 });
 
-checkLcov(function () { //FN:$,top-level //FNDA:1,%
+checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,%
   function f() {            //FN:$,f //FNDA:1,%
     throw 1;                //DA:$,1
     f();                    //DA:$,0
   }
   var l = ",".split(',');   //DA:$,1
   try {                     //DA:$,1
     f();                    //DA:$,1
     f();                    //DA:$,0
@@ -208,8 +284,11 @@ checkLcov(function () { //FN:$,top-level
   }
   //FNF:2
   //FNH:2
   //LF:7
   //LH:5
   //BRF:0
   //BRH:0
 });
+
+// If you add a test case here, do the same in
+// jit-test/tests/coverage/simple.js
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -5519,16 +5519,97 @@ DebuggerScript_isInCatchScope(JSContext*
             }
             ++tnBegin;
         }
     }
     return true;
 }
 
 static bool
+DebuggerScript_getOffsetsCoverage(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getOffsetsCoverage", args, obj, script);
+
+    // If the script has no coverage information, then skip this and return null
+    // instead.
+    if (!script->hasScriptCounts()) {
+        args.rval().setNull();
+        return true;
+    }
+
+    ScriptCounts* sc = &script->getScriptCounts();
+
+    // If the main ever got visited, then assume that any code before main got
+    // visited once.
+    uint64_t hits = 0;
+    const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(script->main()));
+    if (counts->numExec())
+        hits = 1;
+
+    // Build an array of objects which are composed of 4 properties:
+    //  - offset          PC offset of the current opcode.
+    //  - lineNumber      Line of the current opcode.
+    //  - columnNumber    Column of the current opcode.
+    //  - count           Number of times the instruction got executed.
+    RootedObject result(cx, NewDenseEmptyArray(cx));
+    if (!result)
+        return false;
+
+    RootedId offsetId(cx, AtomToId(cx->names().offset));
+    RootedId lineNumberId(cx, AtomToId(cx->names().lineNumber));
+    RootedId columnNumberId(cx, AtomToId(cx->names().columnNumber));
+    RootedId countId(cx, AtomToId(cx->names().count));
+
+    RootedObject item(cx);
+    RootedValue offsetValue(cx);
+    RootedValue lineNumberValue(cx);
+    RootedValue columnNumberValue(cx);
+    RootedValue countValue(cx);
+
+    // Iterate linearly over the bytecode.
+    for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
+        size_t offset = r.frontOffset();
+
+        // The beginning of each non-branching sequences of instruction set the
+        // number of execution of the current instruction and any following
+        // instruction.
+        counts = sc->maybeGetPCCounts(offset);
+        if (counts)
+            hits = counts->numExec();
+
+        offsetValue.setNumber(double(offset));
+        lineNumberValue.setNumber(double(r.frontLineNumber()));
+        columnNumberValue.setNumber(double(r.frontColumnNumber()));
+        countValue.setNumber(double(hits));
+
+        // Create a new object with the offset, line number, column number, the
+        // number of hit counts, and append it to the array.
+        item = NewObjectWithGivenProto<PlainObject>(cx, nullptr);
+        if (!item ||
+            !DefineProperty(cx, item, offsetId, offsetValue) ||
+            !DefineProperty(cx, item, lineNumberId, lineNumberValue) ||
+            !DefineProperty(cx, item, columnNumberId, columnNumberValue) ||
+            !DefineProperty(cx, item, countId, countValue) ||
+            !NewbornArrayPush(cx, result, ObjectValue(*item)))
+        {
+            return false;
+        }
+
+        // If the current instruction has thrown, then decrement the hit counts
+        // with the number of throws.
+        counts = sc->maybeGetThrowCounts(offset);
+        if (counts)
+            hits -= counts->numExec();
+    }
+
+    args.rval().setObject(*result);
+    return true;
+}
+
+static bool
 DebuggerScript_construct(JSContext* cx, unsigned argc, Value* vp)
 {
     JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
                          "Debugger.Script");
     return false;
 }
 
 static const JSPropertySpec DebuggerScript_properties[] = {
@@ -5549,16 +5630,17 @@ static const JSFunctionSpec DebuggerScri
     JS_FN("getAllColumnOffsets", DebuggerScript_getAllColumnOffsets, 0, 0),
     JS_FN("getLineOffsets", DebuggerScript_getLineOffsets, 1, 0),
     JS_FN("getOffsetLine", DebuggerScript_getOffsetLine, 0, 0),
     JS_FN("setBreakpoint", DebuggerScript_setBreakpoint, 2, 0),
     JS_FN("getBreakpoints", DebuggerScript_getBreakpoints, 1, 0),
     JS_FN("clearBreakpoint", DebuggerScript_clearBreakpoint, 1, 0),
     JS_FN("clearAllBreakpoints", DebuggerScript_clearAllBreakpoints, 0, 0),
     JS_FN("isInCatchScope", DebuggerScript_isInCatchScope, 1, 0),
+    JS_FN("getOffsetsCoverage", DebuggerScript_getOffsetsCoverage, 0, 0),
     JS_FS_END
 };
 
 
 /*** Debugger.Source *****************************************************************************/
 
 static inline ScriptSourceObject*
 GetSourceReferent(JSObject* obj)