[gjs/wip/ptomato/develop: 1/2] coverage: Refactor to use Debugger.Script.getOffsetsCoverage()



commit 33096049c23c064da1f19d38a36d12509fb85888
Author: Philip Chimento <philip chimento gmail com>
Date:   Thu Sep 28 00:00:09 2017 -0700

    coverage: Refactor to use Debugger.Script.getOffsetsCoverage()
    
    SpiderMonkey's debugger can keep track of coverage information
    internally. Taking advantage of this feature allows us to get more
    accurate coverage data and remove a lot of our coverage counters code.
    
    For line coverage we request a map of line/column positions of all
    opcodes in a script (getAllOffsets()), which tells us which lines are
    executable. Then we use getOffsetsCoverage() to discover which of those
    lines were actually executed. (A "script" in the SpiderMonkey debugger
    corresponds to either a top-level file, or a function.) For function
    counters, we count the number of times the script's first opcode was
    executed.
    
    Running with code coverage does become a lot faster. We also get
    access to the JS engine's internal naming convention for functions, so we
    don't have the problem of being unable to distinguish two functions'
    names. We now only have to cache the branch coverage info, so the cache
    becomes smaller too.
    
    We can remove the previous implementation of expression counters and
    function counters, since the same info is available from SpiderMonkey. We
    do still need to keep track of branch counters, so for now we still have
    to parse each JS script and analyze its AST.
    
    This implementation has a few quirks: if SpiderMonkey mistakenly marks a
    line executable or executed then we have less recourse to change it. For
    example, in code such as the following:
    
        if (room_on_fire)
            throw new Error('This is fine');
    
    The throw statement will be marked as executed even if it wasn't. This is
    presumably a bug in SpiderMonkey that can be fixed.

 gjs/coverage.cpp                   |   15 +-
 installed-tests/js/testCoverage.js |  855 +++---------------------------------
 modules/_bootstrap/coverage.js     |  570 ++++++-------------------
 test/gjs-test-coverage.cpp         |  123 ++----
 4 files changed, 227 insertions(+), 1336 deletions(-)
---
diff --git a/gjs/coverage.cpp b/gjs/coverage.cpp
index d79fad3..21c1c7f 100644
--- a/gjs/coverage.cpp
+++ b/gjs/coverage.cpp
@@ -548,8 +548,9 @@ convert_and_insert_function_decl(GArray         *array,
     JS::RootedObject object(context, &element.toObject());
     JS::RootedValue function_name_property_value(context);
 
-    if (!gjs_object_require_property(context, object, NULL, GJS_STRING_NAME,
-                                     &function_name_property_value))
+    JS::RootedId key_name(context, gjs_intern_string_to_id(context, "key"));
+    if (!gjs_object_require_property(context, object, "function element",
+                                     key_name, &function_name_property_value))
         return false;
 
     GjsAutoJSChar utf8_string(context);
@@ -1062,15 +1063,11 @@ gjs_deserialize_cache_to_object(GjsCoverage *coverage,
      *     'filename': {
      *         contents: (file contents),
      *         nLines: (number of lines in file),
-     *         lines: Number[nLines + 1],
      *         branches: Array for n_branches of {
      *             point: branch_point,
      *             exits: Number[nLines + 1]
-     *         },
-     *         functions: Array for n_functions of {
-     *             key: function_name,r
-     *             line: line
      *         }
+     *     }
      * }
      */
 
@@ -1546,11 +1543,9 @@ bootstrap_coverage(GjsCoverage *coverage)
         /* Now create the array to pass the desired prefixes over */
         JSObject *prefixes = gjs_build_string_array(context, -1, priv->prefixes);
 
-        JS::AutoValueArray<3> coverage_statistics_constructor_args(context);
+        JS::AutoValueArray<2> coverage_statistics_constructor_args(context);
         coverage_statistics_constructor_args[0].setObject(*prefixes);
         coverage_statistics_constructor_args[1].set(cache_value);
-        coverage_statistics_constructor_args[2]
-            .setBoolean(g_getenv("GJS_DEBUG_COVERAGE_EXECUTED_LINES"));
 
         JSObject *coverage_statistics = JS_New(context,
                                                coverage_statistics_constructor,
diff --git a/installed-tests/js/testCoverage.js b/installed-tests/js/testCoverage.js
index 56d878f..bd35dbe 100644
--- a/installed-tests/js/testCoverage.js
+++ b/installed-tests/js/testCoverage.js
@@ -1,584 +1,5 @@
 const Coverage = imports._bootstrap.coverage;
 
-describe('Coverage.expressionLinesForAST', function () {
-    let testTable = {
-        'works with no trailing newline': [
-            "let x;\n" +
-            "let y;",
-            [1, 2],
-        ],
-
-        'finds lines on both sides of an assignment expression': [
-            "var x;\n" +
-            "x = (function() {\n" +
-            "    return 10;\n" +
-            "})();\n",
-            [2, 3],
-        ],
-
-        'finds lines inside functions': [
-            "function f(a, b) {\n" +
-            "    let x = a;\n" +
-            "    let y = b;\n" +
-            "    return x + y;\n" +
-            "}\n" +
-            "\n" +
-            "var z = f(1, 2);\n",
-            [2, 3, 4, 7],
-        ],
-
-        'finds lines inside anonymous functions': [
-            "var z = (function f(a, b) {\n" +
-            "     let x = a;\n" +
-            "     let y = b;\n" +
-            "     return x + y;\n" +
-            " })();\n",
-            [1, 2, 3, 4],
-        ],
-
-        'finds lines inside body of function property': [
-            "var o = {\n" +
-            "    foo: function() {\n" +
-            "        let x = a;\n" +
-            "    }\n" +
-            "};\n",
-            [1, 2, 3],
-        ],
-
-        'finds lines inside arguments of function property': [
-            "function f(a) {\n" +
-            "}\n" +
-            "f({\n" +
-            "    foo: function() {\n" +
-            "        let x = a;\n" +
-            "    }\n" +
-            "});\n",
-            [1, 3, 4, 5],
-        ],
-
-        'finds lines inside multiline function arguments': [
-            `function f(a, b, c) {
-            }
-            f(1,
-                2 + 3,
-                3 + 4);`,
-            [1, 3, 4, 5],
-        ],
-
-        'finds lines inside function argument that is an object': [
-            `function f(o) {
-            }
-            let obj = {
-                Name: new f({ a: 1,
-                    b: 2 + 3,
-                    c: 3 + 4,
-                })
-            } `,
-            [1, 3, 4, 5, 6],
-        ],
-
-        'finds lines inside a while loop': [
-            "var a = 0;\n" +
-            "while (a < 1) {\n" +
-            "    let x = 0;\n" +
-            "    let y = 1;\n" +
-            "    a++;" +
-            "\n" +
-            "}\n",
-            [1, 2, 3, 4, 5],
-        ],
-
-        'finds lines inside try, catch, and finally': [
-            "var a = 0;\n" +
-            "try {\n" +
-            "    a++;\n" +
-            "} catch (e) {\n" +
-            "    a++;\n" +
-            "} finally {\n" +
-            "    a++;\n" +
-            "}\n",
-            [1, 2, 3, 4, 5, 7],
-        ],
-
-        'finds lines inside case statements': [
-            "var a = 0;\n" +
-            "switch (a) {\n" +
-            "case 1:\n" +
-            "    a++;\n" +
-            "    break;\n" +
-            "case 2:\n" +
-            "    a++;\n" +
-            "    break;\n" +
-            "}\n",
-            [1, 2, 4, 5, 7, 8],
-        ],
-
-        'finds lines inside case statements with character cases': [
-            "var a = 'a';\n" +
-            "switch (a) {\n" +
-            "case 'a':\n" +
-            "    a++;\n" +
-            "    break;\n" +
-            "case 'b':\n" +
-            "    a++;\n" +
-            "    break;\n" +
-            "}\n",
-            [1, 2, 4, 5, 7, 8],
-        ],
-
-        'finds lines inside a for loop': [
-            "for (let i = 0; i < 1; i++) {\n" +
-            "    let x = 0;\n" +
-            "    let y = 1;\n" +
-            "\n" +
-            "}\n",
-            [1, 2, 3],
-        ],
-
-        'finds lines inside if-statement branches': [
-            "if (1 > 0) {\n" +
-            "    let i = 0;\n" +
-            "} else {\n" +
-            "    let j = 1;\n" +
-            "}\n",
-            [1, 2, 4],
-        ],
-
-        'finds all lines of multiline if-conditions': [
-            "if (1 > 0 &&\n" +
-            "    2 > 0 &&\n" +
-            "    3 > 0) {\n" +
-            "    let a = 3;\n" +
-            "}\n",
-            [1, 2, 3, 4],
-        ],
-
-        'finds lines for object property literals': [
-            `var a = {
-                Name: 'foo' + 'bar',
-                Ex: 'bar' + 'foo',
-            }`,
-            [1, 2, 3],
-        ],
-
-        'finds lines for function-valued object properties': [
-            "var a = {\n" +
-            "    Name: function() {},\n" +
-            "};\n",
-            [1, 2],
-        ],
-
-        'finds lines inside object-valued object properties': [
-            "var a = {\n" +
-            "    Name: {},\n" +
-            "};\n",
-            [1, 2],
-        ],
-
-        'finds lines inside array-valued object properties': [
-            "var a = {\n" +
-            "    Name: [],\n" +
-            "};\n",
-            [1, 2],
-        ],
-
-        'finds lines inside object-valued argument to return statement': [
-            "function f() {\n" +
-            "    return {};\n" +
-            "}\n",
-            [2],
-        ],
-
-        'finds lines inside object-valued argument to throw statement': [
-            `function f() {
-                throw {
-                    a: 1 + 2,
-                    b: 2 + 3,
-                }
-            }`,
-            [2, 3, 4],
-        ],
-
-        'does not find lines in empty var declarations': [
-            'var foo;',
-            [],
-        ],
-
-        'finds lines in empty let declarations': [
-            'let foo;',
-            [1],
-        ],
-    };
-
-    Object.keys(testTable).forEach(testcase => {
-        it(testcase, function () {
-            const ast = Reflect.parse(testTable[testcase][0]);
-            let foundLines = Coverage.expressionLinesForAST(ast);
-            expect(foundLines).toEqual(testTable[testcase][1]);
-        });
-    });
-});
-
-describe('Coverage.functionsForAST', function () {
-    let testTable = {
-        'works with no trailing newline': [
-            "function f1() {}\n" +
-            "function f2() {}",
-            [
-                { key: "f1:1:0", line: 1, n_params: 0 },
-                { key: "f2:2:0", line: 2, n_params: 0 },
-            ],
-        ],
-
-        'finds functions': [
-            "function f1() {}\n" +
-            "function f2() {}\n" +
-            "function f3() {}\n",
-            [
-                { key: "f1:1:0", line: 1, n_params: 0 },
-                { key: "f2:2:0", line: 2, n_params: 0 },
-                { key: "f3:3:0", line: 3, n_params: 0 }
-            ],
-        ],
-
-        'finds nested functions': [
-            "function f1() {\n" +
-            "    let f2 = function() {\n" +
-            "        let f3 = function() {\n" +
-            "        }\n" +
-            "    }\n" +
-            "}\n",
-            [
-                { key: "f1:1:0", line: 1, n_params: 0 },
-                { key: "(anonymous):2:0", line: 2, n_params: 0 },
-                { key: "(anonymous):3:0", line: 3, n_params: 0 }
-            ],
-        ],
-
-        /* Note the lack of newlines. This is all on one line */
-        'finds functions on the same line but with different arguments': [
-            "function f1() {" +
-            "    return (function(a) {" +
-            "        return function(a, b) {}" +
-            "    });" +
-            "}",
-            [
-                { key: "f1:1:0", line: 1, n_params: 0 },
-                { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                { key: "(anonymous):1:2", line: 1, n_params: 2 }
-            ],
-        ],
-
-        'finds functions inside an array expression': [
-            "let a = [function() {}];\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 },
-            ],
-        ],
-
-        'finds functions inside an arrow expression': [
-            "(a) => (function() {})();\n",
-            [
-                { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside a sequence': [
-            "(function(a) {})()," +
-            "(function(a, b) {})();\n",
-            [
-                { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                { key: "(anonymous):1:2", line: 1, n_params: 2 },
-            ],
-        ],
-
-        'finds functions inside a unary expression': [
-            "let a = (function() {}())++;\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 },
-            ],
-        ],
-
-        'finds functions inside a binary expression': [
-            "let a = function(a) {}() +" +
-            " function(a, b) {}();\n",
-            [
-                { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                { key: "(anonymous):1:2", line: 1, n_params: 2 }
-            ],
-        ],
-
-        'finds functions inside an assignment expression': [
-            "let a = function() {}();\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside a reflexive assignment expression': [
-            "let a;\n" +
-            "a += function() {}();\n",
-            [
-                { key: "(anonymous):2:0", line: 2, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside if-statement conditions': [
-            "if (function(a) {}(a) >" +
-            "    function(a, b) {}(a, b)) {}\n",
-            [
-                { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                { key: "(anonymous):1:2", line: 1, n_params: 2 }
-            ],
-        ],
-
-        'finds functions inside while-statement conditions': [
-            "while (function(a) {}(a) >" +
-            "       function(a, b) {}(a, b)) {};\n",
-            [
-                { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                { key: "(anonymous):1:2", line: 1, n_params: 2 }
-            ],
-        ],
-
-        'finds functions inside for-statement initializer': [
-            "for (function() {}; ;) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        /* SpiderMonkey parses for (let i = <init>; <cond>; <update>) as though
-         * they were let i = <init> { for (; <cond> <update>) } so test the
-         * LetStatement initializer case too */
-        'finds functions inside let-statement in for-statement initializer': [
-            "for (let i = function() {}; ;) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside var-statement inside for-statement initializer': [
-            "for (var i = function() {}; ;) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside for-statement condition': [
-            "for (; function() {}();) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside for-statement increment': [
-            "for (; ;function() {}()) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside for-in-statement': [
-            "for (let x in function() {}()) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside for-each statement': [
-            "for each (x in function() {}()) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions inside for-of statement': [
-            "for (x of (function() {}())) {}\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds function literals used as an object': [
-            "f = function() {}.bind();\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds function literals used as an object in a dynamic property expression': [
-            "f = function() {}['bind']();\n",
-            [
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions on either side of a logical expression': [
-            "let f = function(a) {} ||" +
-            " function(a, b) {};\n",
-            [
-                { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                { key: "(anonymous):1:2", line: 1, n_params: 2 }
-            ],
-        ],
-
-        'finds functions on either side of a conditional expression': [
-            "let a\n" +
-            "let f = a ? function(a) {}() :" +
-            " function(a, b) {}();\n",
-            [
-                { key: "(anonymous):2:1", line: 2, n_params: 1 },
-                { key: "(anonymous):2:2", line: 2, n_params: 2 }
-            ],
-        ],
-
-        'finds functions as the argument of a yield statement': [
-            "function a() { yield function (){} };\n",
-            [
-                { key: "a:1:0", line: 1, n_params: 0 },
-                { key: "(anonymous):1:0", line: 1, n_params: 0 }
-            ],
-        ],
-
-        'finds functions in an array comprehension body': [
-            "let a = new Array(1);\n" +
-            "let b = [for (i of a) function() {}];\n",
-            [
-                { key: "(anonymous):2:0", line: 2, n_params: 0 }
-            ],
-        ],
-
-        'finds functions in an array comprehension block': [
-            "let a = new Array(1);\n" +
-            "let b = [for (i of function() {}) i];\n",
-            [
-                { key: "(anonymous):2:0", line: 2, n_params: 0 }
-            ],
-        ],
-
-        'finds functions in an array comprehension filter': [
-            "let a = new Array(1);\n" +
-            "let b = [for (i of a) if (function() {}()) i];\n",
-            [
-                { key: "(anonymous):2:0", line: 2, n_params: 0 }
-            ],
-        ],
-
-        'finds class methods': [
-            `class Foo {
-                bar() {}
-            }`,
-            [
-                { key: 'bar:2:0', line: 2, n_params: 0 },
-            ],
-        ],
-
-        'finds class property setters': [
-            `class Foo {
-                set bar(value) {}
-            }`,
-            [
-                { key: 'set bar:2:1', line: 2, n_params: 1 },
-            ],
-        ],
-
-        'finds class property getters': [
-            `class Foo {
-                get bar() {}
-            }`,
-            [
-                { key: 'get bar:2:0', line: 2, n_params: 0 },
-            ],
-        ],
-
-        'finds class constructors': [
-            `class Foo {
-                constructor(baz) {}
-            }`,
-            [
-                { key: 'Foo:2:1', line: 2, n_params: 1 },
-            ],
-        ],
-
-        'finds class expression methods': [
-            `void class {
-                baz() {}
-            }`,
-            [
-                { key: 'baz:2:0', line: 2, n_params: 0 },
-            ],
-        ],
-
-        'finds class expression property setters': [
-            `void class {
-                set baz(value) {}
-            }`,
-            [
-                { key: 'set baz:2:1', line: 2, n_params: 1 },
-            ],
-        ],
-
-        'finds class expression property getters': [
-            `void class {
-                get baz() {}
-            }`,
-            [
-                { key: 'get baz:2:0', line: 2, n_params: 0 },
-            ],
-        ],
-
-        'finds class expression constructors': [
-            `void class {
-                constructor(baz) {}
-            }`,
-            [
-                { key: '(anonymous):2:1', line: 2, n_params: 1 },
-            ],
-        ],
-
-        'finds functions inside labeled statement': [
-            `loop:
-            for (function () {}; ; ) {}`,
-            [
-                { key: '(anonymous):2:0', line: 2, n_params: 0 },
-            ],
-        ],
-
-        'finds functions inside switch expression': [
-            'switch (function () {}) {}',
-            [
-                { key: '(anonymous):1:0', line: 1, n_params: 0 },
-            ],
-        ],
-
-        'finds functions inside function default arguments': [
-            'function foo(bar=function () {}) {}',
-            [
-                { key: 'foo:1:1', line: 1, n_params: 1 },
-                { key: '(anonymous):1:0', line: 1, n_params: 0 },
-            ],
-        ],
-
-        'finds functions inside function expression default arguments': [
-            'void function foo(bar=function () {}) {}',
-            [
-                { key: 'foo:1:1', line: 1, n_params: 1 },
-                { key: '(anonymous):1:0', line: 1, n_params: 0 },
-            ],
-        ],
-    };
-
-    Object.keys(testTable).forEach(testcase => {
-        it(testcase, function () {
-            const ast = Reflect.parse(testTable[testcase][0]);
-            let foundFuncs = Coverage.functionsForAST(ast);
-            expect(foundFuncs).toEqual(testTable[testcase][1]);
-        });
-    });
-});
-
 describe('Coverage.branchesForAST', function () {
     let testTable = {
         'works with no trailing newline': [
@@ -745,22 +166,6 @@ describe('Coverage', function () {
         expect(number).toEqual(3);
     });
 
-    it('turns zero expression lines into counters', function () {
-        let expressionLines = [];
-        let nLines = 1;
-        let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
-
-        expect(counters).toEqual([undefined, undefined]);
-    });
-
-    it('turns a single expression line into counters', function () {
-        let expressionLines = [1, 2];
-        let nLines = 4;
-        let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
-
-        expect(counters).toEqual([undefined, 0, 0, undefined, undefined]);
-    });
-
     it('returns empty array for no branches', function () {
         let counters = Coverage._branchesToBranchCounters([], 1);
         expect(counters).toEqual([undefined, undefined]);
@@ -835,72 +240,15 @@ describe('Coverage', function () {
         });
     });
 
-    it('function key from function with name matches schema', function () {
-        let ast = Reflect.parse('function f(a, b) {}').body[0];
-        let functionKeyForFunctionName =
-            Coverage._getFunctionKeyFromReflectedFunction(ast);
-        expect(functionKeyForFunctionName).toEqual('f:1:2');
-    });
-
-    it('function key from function without name is anonymous', function () {
-        let ast = Reflect.parse('\nvoid function (a, b, c) {}').body[0].expression.argument;
-        let functionKeyForAnonymousFunction =
-            Coverage._getFunctionKeyFromReflectedFunction(ast);
-        expect(functionKeyForAnonymousFunction).toEqual('(anonymous):2:3');
-    });
-
-    it('returns a function counter map for function keys', function () {
-        let ast = Reflect.parse('function name() {}');
-        let detectedFunctions = Coverage.functionsForAST(ast);
-        let functionCounters =
-            Coverage._functionsToFunctionCounters('script', detectedFunctions);
-        expect(functionCounters.name['1']['0'].hitCount).toEqual(0);
-    });
-
-    it('reports an error when two indistinguishable functions are present', function () {
-        spyOn(window, 'log');
-        let ast = Reflect.parse('() => {}; () => {}');
-        let detectedFunctions = Coverage.functionsForAST(ast);
-        Coverage._functionsToFunctionCounters('script', detectedFunctions);
-
-        expect(window.log).toHaveBeenCalledWith('script:1 Function ' +
-            'identified as (anonymous):1:0 already seen in this file. ' +
-            'Function coverage will be incomplete.');
-    });
-
-    it('populates a known functions array', function () {
-        let functions = [
-            { line: 1 },
-            { line: 2 }
-        ];
-
-        let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 4);
-
-        expect(knownFunctionsArray)
-            .toEqual([undefined, true, true, undefined, undefined]);
-    });
-
     it('converts function counters to an array', function () {
-        let functionsMap = {
-            '(anonymous)': {
-                '2': {
-                    '0': {
-                        hitCount: 1
-                    },
-                },
-            },
-            'name': {
-                '1': {
-                    '0': {
-                        hitCount: 0
-                    },
-                },
-            }
-        };
+        let functionsMap = new Map([
+            ['(anonymous):2@123', {line: 2, hitCount: 1}],
+            ['name:1@456', {line: 1, hitCount: 0}],
+        ]);
 
         let expectedFunctionCountersArray = [
-            jasmine.objectContaining({ name: '(anonymous):2:0', hitCount: 1 }),
-            jasmine.objectContaining({ name: 'name:1:0', hitCount: 0 })
+            jasmine.objectContaining({ name: '(anonymous):2@123', hitCount: 1 }),
+            jasmine.objectContaining({ name: 'name:1@456', hitCount: 0 }),
         ];
 
         let convertedFunctionCounters = Coverage._convertFunctionCountersToArray(functionsMap);
@@ -910,135 +258,72 @@ describe('Coverage', function () {
 });
 
 describe('Coverage.incrementFunctionCounters', function () {
-    it('increments for function on same execution start line', function () {
-        let functionCounters = Coverage._functionsToFunctionCounters('script', [
-            { key: 'f:1:0',
-              line: 1,
-              n_params: 0 }
-        ]);
-        Coverage._incrementFunctionCounters(functionCounters, null, 'f', 1, 0);
-
-        expect(functionCounters.f['1']['0'].hitCount).toEqual(1);
-    });
+    const coverage = [
+        {lineNumber: 1, columnNumber: 0, offset: 0, count: 5},
+        {lineNumber: 2, columnNumber: 5, offset: 5, count: 10},
+        {lineNumber: 2, columnNumber: 10, offset: 20, count: 5},
+        {lineNumber: 4, columnNumber: 4, offset: 10, count: 5},
+    ];
+    const key = 'f:1@123';
+    const line = 1;
+    let counters;
 
-    it('can disambiguate two functions with the same name', function () {
-        let functionCounters = Coverage._functionsToFunctionCounters('script', [
-            { key: '(anonymous):1:0',
-              line: 1,
-              n_params: 0 },
-            { key: '(anonymous):2:0',
-              line: 2,
-              n_params: 0 }
-        ]);
-        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0);
-        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 2, 0);
-
-        expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(1);
-        expect(functionCounters['(anonymous)']['2']['0'].hitCount).toEqual(1);
+    beforeEach(function () {
+        counters = new Map();
     });
 
-    it('can disambiguate two functions on same line with different params', function () {
-        let functionCounters = Coverage._functionsToFunctionCounters('script', [
-            { key: '(anonymous):1:0',
-              line: 1,
-              n_params: 0 },
-            { key: '(anonymous):1:1',
-              line: 1,
-              n_params: 1 }
-        ]);
-        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0);
-        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 1);
-
-        expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(1);
-        expect(functionCounters['(anonymous)']['1']['1'].hitCount).toEqual(1);
+    it('determines hit count for Debugger.Script function coverage array', function () {
+        Coverage._incrementFunctionCounters(counters, key, line, coverage);
+        expect(counters.get(key)).toEqual({line, hitCount: 5});
     });
 
-    it('can disambiguate two functions on same line by guessing closest params', function () {
-        let functionCounters = Coverage._functionsToFunctionCounters('script', [
-            { key: '(anonymous):1:0',
-              line: 1,
-              n_params: 0 },
-            { key: '(anonymous):1:3',
-              line: 1,
-              n_params: 3 }
-        ]);
-
-        /* Eg, we called the function with 3 params with just two arguments. We
-         * should be able to work out that we probably intended to call the
-         * latter function as opposed to the former. */
-        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 2);
+    // See comments in modules/_bootstrap/coverage.js; the debugger for some
+    // reason has two scripts for each function, only one with coverage
 
-        expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(0);
-        expect(functionCounters['(anonymous)']['1']['3'].hitCount).toEqual(1);
+    it('handles null coverage after the real coverage', function () {
+        Coverage._incrementFunctionCounters(counters, key, line, coverage);
+        Coverage._incrementFunctionCounters(counters, key, line, null);
+        expect(counters.get(key)).toEqual({line, hitCount: 5});
     });
 
-    it('increments for function on earlier start line', function () {
-        let ast = Reflect.parse('function name() {}');
-        let detectedFunctions = Coverage.functionsForAST(ast);
-        let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
-        let functionCounters = Coverage._functionsToFunctionCounters('script',
-                                                                     detectedFunctions);
-
-        /* We're entering at line two, but the function definition was actually
-         * at line one */
-        Coverage._incrementFunctionCounters(functionCounters, knownFunctionsArray, 'name', 2, 0);
-
-        expect(functionCounters.name['1']['0'].hitCount).toEqual(1);
+    it('handles null coverage before the real coverage', function () {
+        Coverage._incrementFunctionCounters(counters, key, line, null);
+        Coverage._incrementFunctionCounters(counters, key, line, coverage);
+        expect(counters.get(key)).toEqual({line, hitCount: 5});
     });
 
-    it('throws an error on unexpected function', function () {
-        let ast = Reflect.parse('function name() {}');
-        let detectedFunctions = Coverage.functionsForAST(ast);
-        let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
-        let functionCounters = Coverage._functionsToFunctionCounters('script',
-                                                                     detectedFunctions);
-
-        /* We're entering at line two, but the function definition was actually
-         * at line one */
-        expect(() => {
-            Coverage._incrementFunctionCounters(functionCounters,
-                                                knownFunctionsArray,
-                                                'doesnotexist',
-                                                2,
-                                                0);
-        }).toThrow();
+    it('determines hit count 0 for no coverage at all', function () {
+        Coverage._incrementFunctionCounters(counters, key, line, null);
+        Coverage._incrementFunctionCounters(counters, key, line, null);
+        expect(counters.get(key)).toEqual({line, hitCount: 0});
     });
+});
 
-    it('throws if line out of range', function () {
-        let expressionCounters = [
-            undefined,
-            0
-        ];
+describe('Coverage.incrementLineCounters', function () {
+    let counters, offsets;
 
-        expect(() => {
-            Coverage._incrementExpressionCounters(expressionCounters, 'script', 2);
-        }).toThrow();
+    beforeEach(function () {
+        counters = [];
+        offsets = Array(5);
+        offsets[1] = [0];
+        offsets[2] = [5, 20];
+        offsets[4] = [10];
     });
 
-    it('increments if in range', function () {
-        let expressionCounters = [
-            undefined,
-            0
+    it('increments lines in the array', function () {
+        const coverage = [
+            {lineNumber: 1, columnNumber: 0, offset: 0, count: 5},
+            {lineNumber: 2, columnNumber: 5, offset: 5, count: 10},
+            {lineNumber: 2, columnNumber: 10, offset: 20, count: 5},
+            {lineNumber: 4, columnNumber: 4, offset: 10, count: 0},
         ];
-
-        Coverage._incrementExpressionCounters(expressionCounters, 'script', 1);
-        expect(expressionCounters[1]).toEqual(1);
+        Coverage._incrementLineCounters(counters, offsets, coverage);
+        expect(counters).toEqual([undefined, 5, 10, undefined, 0]);
     });
 
-    it('warns if we hit a non-executable line', function () {
-        spyOn(window, 'log');
-        let expressionCounters = [
-            undefined,
-            0,
-            undefined
-        ];
-
-        Coverage._incrementExpressionCounters(expressionCounters, 'script', 2,
-            true);
-
-        expect(window.log).toHaveBeenCalledWith("script:2 Executed line " +
-            "previously marked non-executable by Reflect");
-        expect(expressionCounters[2]).toEqual(1);
+    it('marks executable lines even if there was no coverage', function () {
+        Coverage._incrementLineCounters(counters, offsets, null);
+        expect(counters).toEqual([undefined, 0, 0, undefined, 0]);
     });
 });
 
@@ -1096,18 +381,11 @@ describe('Coverage statistics container', function () {
         "filename": { \
             "mtime": [1, 2], \
             "checksum": null, \
-            "lines": [2, 4, 5], \
             "branches": [ \
                 { \
                     "point": 4, \
                     "exits": [5] \
                 } \
-            ], \
-            "functions": [ \
-                { \
-                    "key": "f:1:0", \
-                    "line": 1 \
-                } \
             ] \
         } \
     }';
@@ -1115,19 +393,19 @@ describe('Coverage statistics container', function () {
     describe('with cache', function () {
         let container;
         beforeEach(function () {
-            spyOn(Coverage, '_fetchCountersFromReflection').and.callThrough();
+            spyOn(Coverage, '_analyzeBranchInfoFromReflection').and.callThrough();
             container = new Coverage.CoverageStatisticsContainer(MockFilenames,
                                                                  MockCache);
         });
 
         it('fetches counters from cache', function () {
             container.fetchStatistics('filename');
-            expect(Coverage._fetchCountersFromReflection).not.toHaveBeenCalled();
+            expect(Coverage._analyzeBranchInfoFromReflection).not.toHaveBeenCalled();
         });
 
         it('fetches counters from reflection if missed', function () {
             container.fetchStatistics('uncached');
-            expect(Coverage._fetchCountersFromReflection).toHaveBeenCalled();
+            expect(Coverage._analyzeBranchInfoFromReflection).toHaveBeenCalled();
         });
 
         it('cache is not stale if all hit', function () {
@@ -1153,26 +431,15 @@ describe('Coverage statistics container', function () {
             statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
         });
 
-        it('have same executable lines as reflection', function () {
-            expect(statisticsWithNoCaching.expressionCounters)
-                .toEqual(statistics.expressionCounters);
-        });
-
         it('have same branch exits as reflection', function () {
             /* Branch starts on line 4 */
-            expect(statisticsWithNoCaching.branchCounters[4].exits[0].line)
-                .toEqual(statistics.branchCounters[4].exits[0].line);
+            expect(statisticsWithNoCaching[4].exits[0].line)
+                .toEqual(statistics[4].exits[0].line);
         });
 
         it('have same branch points as reflection', function () {
-            expect(statisticsWithNoCaching.branchCounters[4].point)
-                .toEqual(statistics.branchCounters[4].point);
-        });
-
-        it('have same function keys as reflection', function () {
-            /* Functions start on line 1 */
-            expect(Object.keys(statisticsWithNoCaching.functionCounters))
-                .toEqual(Object.keys(statistics.functionCounters));
+            expect(statisticsWithNoCaching[4].point)
+                .toEqual(statistics[4].point);
         });
     });
 });
diff --git a/modules/_bootstrap/coverage.js b/modules/_bootstrap/coverage.js
index bd17783..b6b7770 100644
--- a/modules/_bootstrap/coverage.js
+++ b/modules/_bootstrap/coverage.js
@@ -188,58 +188,6 @@ function collectForSubNodes(subNodes, collector) {
     return result;
 }
 
-function _getFunctionKeyFromReflectedFunction(node) {
-    let name = node.id !== null ? node.id.name : '(anonymous)';
-    let line = node.loc.start.line;
-    let n_params = node.params.length;
-
-    return [name, line, n_params].join(':');
-}
-
-/* Unfortunately, the Reflect API doesn't give us enough information to
- * uniquely identify a function. A function might be anonymous, in which
- * case the JS engine uses some heurisitics to get a unique string identifier
- * but that isn't available to us here.
- *
- * There's also the edge-case where functions with the same name might be
- * defined within the same scope, or multiple anonymous functions might
- * be defined on the same line. In that case, it will look like we entered
- * the same function multiple times since we can't get column information
- * from the engine-side.
- *
- * For instance:
- *
- * 1. function f() {
- *       function f() {
- *       }
- *    }
- *
- * 2. let a = function() { function(a, b) {} };
- *
- * 3. let a = function() { function () {} }
- *
- * We can work-around case 1 by using the line numbers to get a unique identifier.
- * We can work-around case 2 by using the arguments length to get a unique identifier
- * We can't work-around case 3. The best thing we can do is warn that coverage
- * reports might be inaccurate as a result */
-function functionsForNode(node) {
-    let functionNames = [];
-    switch (node.type) {
-    case 'FunctionDeclaration':
-    case 'FunctionExpression':
-    case 'ArrowFunctionExpression':
-        functionNames.push({ key: _getFunctionKeyFromReflectedFunction(node),
-                             line: node.loc.start.line,
-                             n_params: node.params.length });
-    }
-
-    return functionNames;
-}
-
-function functionsForAST(ast) {
-    return collectForSubNodes(ast.body, functionsForNode);
-}
-
 /* If a branch' consequent is a block statement, there's
  * a chance that it could start on the same line, although
  * that's not where execution really starts. If it is
@@ -338,65 +286,6 @@ function branchesForAST(ast) {
     return collectForSubNodes(ast.body, branchesForNode);
 }
 
-function expressionLinesForNode(statement) {
-    let expressionLines = [];
-
-    let expressionNodeTypes = ['Expression',
-                               'Declaration',
-                               'Statement',
-                               'Clause',
-                               'Literal',
-                               'Identifier'];
-
-    if (expressionNodeTypes.some(function(type) {
-            return statement.type.indexOf(type) !== -1;
-        })) {
-
-        /* These expressions aren't executable on their own */
-        switch (statement.type) {
-        case 'FunctionDeclaration':
-        case 'Literal':
-            break;
-        /* Perplexingly, an empty block statement is actually executable,
-         * push it if it is */
-        case 'BlockStatement':
-            if (statement.body.length !== 0)
-                break;
-            expressionLines.push(statement.loc.start.line);
-            break;
-        case 'VariableDeclaration':
-            if (statement.kind === 'var') {
-                /* empty 'var foo;' declarations are not executable */
-                let nonEmpty = statement.declarations.filter(decl =>
-                    decl.init !== null);
-                nonEmpty.forEach(decl => {
-                    expressionLines.push(decl.loc.start.line);
-                });
-                break;
-            }
-            /* fall through */
-        default:
-            expressionLines.push(statement.loc.start.line);
-            break;
-        }
-    }
-
-    return expressionLines;
-}
-
-function deduplicate(list) {
-    return list.filter(function(elem, pos, self) {
-        return self.indexOf(elem) === pos;
-    });
-}
-
-function expressionLinesForAST(ast) {
-    let allExpressions = collectForSubNodes(ast.body, expressionLinesForNode);
-    allExpressions = deduplicate(allExpressions);
-
-    return allExpressions;
-}
-
 function _getNumberOfLinesForScript(scriptContents) {
     let scriptLines = scriptContents.split("\n");
     let scriptLineCount = scriptLines.length;
@@ -405,7 +294,7 @@ function _getNumberOfLinesForScript(scriptContents) {
 }
 
 /*
- * The created array is a 1-1 representation of the hitcount in the filename. Each
+ * The @counters array is a 1-1 representation of the hit counts. Each
  * element refers to an individual line. In order to avoid confusion, our array
  * is zero indexed, but the zero'th line is always ignored and the first element
  * refers to the first line of the file.
@@ -425,26 +314,24 @@ function _getNumberOfLinesForScript(scriptContents) {
  *      we thought was non-executable is not as much of a problem as noise generated by
  *      ostensible "misses" which could in fact never be executed.
  *
+ * @offsets: Array returned by Debugger.Script.getAllOffsets()
+ * @coverage: Array returned by Debugger.Script.getOffsetsCoverage() or null
+ * (https://developer.mozilla.org/en-US/docs/Tools/Debugger-API/Debugger.Script)
  */
-function _expressionLinesToCounters(expressionLines, nLines) {
-    expressionLines.sort(function(left, right) { return left - right; });
-
-    let expressionLinesIndex = 0;
-    let counters = new Array(nLines + 1);
-
-    if (expressionLines.length === 0)
-        return counters;
-
-    for (let i = 1; i < counters.length; i++) {
-        if (!expressionLines.hasOwnProperty(expressionLinesIndex))
-            continue;
-        if (expressionLines[expressionLinesIndex] == i) {
-            counters[i] = 0;
-            expressionLinesIndex++;
-        }
-    }
+function _incrementLineCounters(counters, offsets, coverage) {
+    // Set all executable lines to 0 that don't already have counts
+    offsets.forEach((pcs, line) => {
+        if (counters[line] === undefined)
+            counters[line] = 0;
+    });
 
-    return counters;
+    if (!coverage)
+        return;
+    coverage.forEach(({lineNumber, count}) => {
+        // There can be multiple instructions in one line, so only report the
+        // max number of times any instruction in the line was executed
+        counters[lineNumber] = Math.max(counters[lineNumber], count);
+    });
 }
 
 /* As above, we are creating a 1-1 representation of script lines to potential branches
@@ -498,184 +385,38 @@ function _branchesToBranchCounters(branches, nLines) {
     return counters;
 }
 
-function _functionsToFunctionCounters(script, functions) {
-    let functionCounters = {};
-
-    functions.forEach(function(func) {
-        let [name, line, args] = func.key.split(':');
-
-        if (functionCounters[name] === undefined) {
-            functionCounters[name] = {};
-        }
-
-        if (functionCounters[name][line] === undefined) {
-            functionCounters[name][line] = {};
-        }
-
-        if (functionCounters[name][line][args] === undefined) {
-            functionCounters[name][line][args] = { hitCount: 0 };
-        } else {
-            log(script + ':' + line + ' Function identified as ' +
-                func.key + ' already seen in this file. Function coverage ' +
-                'will be incomplete.');
-        }
-    });
-
-    return functionCounters;
-}
-
-function _populateKnownFunctions(functions, nLines) {
-    let knownFunctions = new Array(nLines + 1);
-
-    functions.forEach(function(func) {
-        knownFunctions[func.line] = true;
-    });
-
-    return knownFunctions;
-}
-
-function _identifyFunctionCounterInLinePartForDescription(linePart,
-                                                          nArgs) {
-    /* There is only one potential option for this line. We might have been
-     * called with the wrong number of arguments, but it doesn't matter. */
-    if (Object.getOwnPropertyNames(linePart).length === 1)
-        return linePart[Object.getOwnPropertyNames(linePart)[0]];
-
-    /* Have to disambiguate using nArgs and we have an exact match. */
-    if (linePart[nArgs] !== undefined)
-        return linePart[nArgs];
-
-    /* There are multiple options on this line. Pick the one where
-     * we satisfy the number of arguments exactly, or failing that,
-     * go through each and pick the closest. */
-    let allNArgsOptions = Object.keys(linePart).map(function(key) {
-        return parseInt(key);
-    });
-
-    let closest = allNArgsOptions.reduce(function(prev, current, index, array) {
-        let nArgsOption = array[index];
-        if (Math.abs(nArgsOption - nArgs) < Math.abs(current - nArgs)) {
-            return nArgsOption;
-        }
-
-        return current;
-    });
-
-    return linePart[String(closest)];
-}
-
-function _identifyFunctionCounterForDescription(functionCounters,
-                                                name,
-                                                line,
-                                                nArgs) {
-    let candidateCounter = functionCounters[name];
-
-    if (candidateCounter === undefined)
-        return null;
-
-    if (Object.getOwnPropertyNames(candidateCounter).length === 1) {
-        let linePart = candidateCounter[Object.getOwnPropertyNames(candidateCounter)[0]];
-        return _identifyFunctionCounterInLinePartForDescription(linePart, nArgs);
-    }
-
-    let linePart = functionCounters[name][line];
-
-    if (linePart === undefined) {
-        return null;
-    }
-
-    return _identifyFunctionCounterInLinePartForDescription(linePart, nArgs);
-}
-
 /**
  * _incrementFunctionCounters
  *
- * functionCounters: An object which is a key-value pair with the following schema:
+ * @functionCounters: a Map object, can be empty, will be modified by this
+ * function and has the following schema:
  * {
  *      "key" : { line, hitCount }
  * }
- * linesWithKnownFunctions: An array of either "true" or undefined, with true set to
- * each element corresponding to a line that we know has a function on it.
- * name: The name of the function or "(anonymous)" if it has no name
+ * @key: a unique key for the function whose coverage is being determined
  * line: The line at which execution first started on this function.
- * nArgs: The number of arguments this function has.
+ * @coverage: array returned from Debugger.Script.getOffsetsCoverage() or null
+ * (https://developer.mozilla.org/en-US/docs/Tools/Debugger-API/Debugger.Script)
  */
 function _incrementFunctionCounters(functionCounters,
-                                    linesWithKnownFunctions,
-                                    name,
+                                    key,
                                     line,
-                                    nArgs) {
-    let functionCountersForKey = _identifyFunctionCounterForDescription(functionCounters,
-                                                                        name,
-                                                                        line,
-                                                                        nArgs);
-
-    /* Its possible that the JS Engine might enter a funciton
-     * at an executable line which is a little bit past the
-     * actual definition. Roll backwards until we reach the
-     * last known function definition line which we kept
-     * track of earlier to see if we can find this function first */
-    if (functionCountersForKey === null) {
-        do {
-            --line;
-            functionCountersForKey = _identifyFunctionCounterForDescription(functionCounters,
-                                                                            name,
-                                                                            line,
-                                                                            nArgs);
-        } while (linesWithKnownFunctions[line] !== true && line > 0);
-    }
-
-    if (functionCountersForKey !== null) {
-        functionCountersForKey.hitCount++;
-    } else {
-        let functionKey = [name, line, nArgs].join(':');
-        throw new Error("expected Reflect to find function " + functionKey);
-    }
-}
-
-/**
- * _incrementExpressionCounters
- *
- * expressonCounters: An array of either a hit count for a found
- * executable line or undefined for a known non-executable line.
- * line: an executed line
- * @shouldWarn: if true, print a mostly harmless warning about executing a line
- * that was thought non-executable.
- */
-function _incrementExpressionCounters(expressionCounters,
-                                      script,
-                                      offsetLine, shouldWarn) {
-    let expressionCountersLen = expressionCounters.length;
-
-    if (offsetLine >= expressionCountersLen)
-        throw new Error("Executed line " + offsetLine + " which was past the highest-found line " + 
expressionCountersLen);
-
-    /* If this happens it is not a huge problem - though it does
-     * mean that the reflection machinery is not doing its job, so we should
-     * print a debug message about it in case someone is interested.
-     *
-     * The reason why we don't have a proper log is because it
-     * is difficult to determine what the SpiderMonkey program counter
-     * will actually pass over, especially function declarations for some
-     * reason:
-     *
-     *     function f(a,b) {
-     *         a = 1;
-     *     }
-     *
-     * In some cases, the declaration itself will be executed
-     * but in other cases it won't be. Reflect.parse tells us that
-     * the only two expressions on that line are a FunctionDeclaration
-     * and BlockStatement, neither of which would ordinarily be
-     * executed */
-    if (expressionCounters[offsetLine] === undefined) {
-        if (shouldWarn)
-            log(script + ':' + offsetLine + ' Executed line previously marked ' +
-                'non-executable by Reflect');
-        expressionCounters[offsetLine] = 0;
-    }
-
-    expressionCounters[offsetLine]++;
+                                    coverage) {
+    let hitCount = 0;
+    // The number of times the first opcode was executed is the number of times
+    // the function was executed?
+    if (coverage)
+        hitCount = coverage[0].count;
+
+    // There are, for some reason, two debugger scripts for each function, one
+    // with non-null coverage and one with null coverage. However, if the
+    // function was not executed, then both have null coverage. If there's
+    // already an entry with a nonzero hit count, then this is the null script.
+    let entry = functionCounters.get(key);
+    if (entry && entry.hitCount)  // present and nonzero
+        return;
+
+    functionCounters.set(key, {line, hitCount});
 }
 
 var _BranchTracker = class {
@@ -711,40 +452,15 @@ var _BranchTracker = class {
 
 function _convertFunctionCountersToArray(functionCounters) {
     let arrayReturn = [];
-    /* functionCounters is an object so explore it to create a
-     * set of function keys and then convert it to
-     * an array-of-object using the key as a property
-     * of that object */
-    for (let name of Object.getOwnPropertyNames(functionCounters)) {
-        let namePart = functionCounters[name];
-
-        for (let line of Object.getOwnPropertyNames(namePart)) {
-            let linePart = functionCounters[name][line];
-
-            for (let nArgs of Object.getOwnPropertyNames(linePart)) {
-                let functionKey = [name, line, nArgs].join(':');
-                arrayReturn.push({ name: functionKey,
-                                   line: Number(line),
-                                   nArgs: nArgs,
-                                   hitCount: linePart[nArgs].hitCount });
-            }
-        }
-    }
+    for (let [key, {line, hitCount}] of functionCounters)
+        arrayReturn.push({key, line, hitCount});
 
-    arrayReturn.sort(function(left, right) {
-        if (left.name < right.name)
-            return -1;
-        else if (left.name > right.name)
-            return 1;
-        else
-            return 0;
-    });
     return arrayReturn;
 }
 
-/* Looks up filename in cache and fetches statistics
+/* Looks up filename in cache and fetches branch info
  * directly from the cache */
-function _fetchCountersFromCache(filename, cache, nLines) {
+function _fetchBranchInfoFromCache(filename, cache) {
     if (!cache)
         return null;
 
@@ -762,41 +478,26 @@ function _fetchCountersFromCache(filename, cache, nLines) {
                 return null;
         }
 
-        let functions = cache_for_file.functions;
-
-        return {
-            expressionCounters: _expressionLinesToCounters(cache_for_file.lines, nLines),
-            branchCounters: _branchesToBranchCounters(cache_for_file.branches, nLines),
-            functionCounters: _functionsToFunctionCounters(filename, functions),
-            linesWithKnownFunctions: _populateKnownFunctions(functions, nLines),
-            nLines: nLines
-        };
+        return cache_for_file.branches;
     }
 
     return null;
 }
 
-function _fetchCountersFromReflection(filename, contents, nLines) {
+function _analyzeBranchInfoFromReflection(filename, contents) {
     // Shebang is illegal character sequence to JS parser
     if (contents.startsWith('#!'))
         contents = '//' + contents;
     let reflection = Reflect.parse(contents);
-    let functions = functionsForAST(reflection);
-
-    return {
-        expressionCounters: _expressionLinesToCounters(expressionLinesForAST(reflection), nLines),
-        branchCounters: _branchesToBranchCounters(branchesForAST(reflection), nLines),
-        functionCounters: _functionsToFunctionCounters(filename, functions),
-        linesWithKnownFunctions: _populateKnownFunctions(functions, nLines),
-        nLines: nLines
-    };
+
+    return branchesForAST(reflection);
 }
 
 var CoverageStatisticsContainer = class {
     constructor(prefixes, cache) {
         /* Copy the files array, so that it can be re-used in the tests */
         this._cachedASTs = cache ? JSON.parse(cache) : null;
-        this._coveredFiles = {};
+        this._coveredFiles = new Map();
         this._cacheMisses = 0;
     }
 
@@ -804,75 +505,55 @@ var CoverageStatisticsContainer = class {
         let contents = getFileContents(filename);
         let nLines = _getNumberOfLinesForScript(contents);
 
-        let counters = _fetchCountersFromCache(filename, this._cachedASTs,
-            nLines);
-        if (counters === null) {
+        let info = _fetchBranchInfoFromCache(filename, this._cachedASTs);
+        if (info === null) {
             this._cacheMisses++;
-            counters = _fetchCountersFromReflection(filename, contents, nLines);
+            info = _analyzeBranchInfoFromReflection(filename, contents);
         }
 
-        if (counters === null)
+        if (info === null)
             throw new Error('Failed to parse and reflect file ' + filename);
 
-        /* Set contents here as we don't pass it to _fetchCountersFromCache. */
-        counters.contents = contents;
-
-        return counters;
+        return _branchesToBranchCounters(info, nLines);
     }
 
     _ensureStatisticsFor(filename) {
         // Skip scripts fed to JS engine programmatically.
         if (filename.startsWith('<') && filename.endsWith('>'))
             return undefined;
-        if (!this._coveredFiles[filename])
-            this._coveredFiles[filename] = this._createStatisticsFor(filename);
-        return this._coveredFiles[filename];
+
+        let info = this._coveredFiles.get(filename);
+        if (info === undefined) {
+            info = this._createStatisticsFor(filename);
+            this._coveredFiles.set(filename, info);
+        }
+
+        return info;
     }
 
     stringify() {
         let cache_data = {};
-        Object.keys(this._coveredFiles).forEach(filename => {
-            let statisticsForFilename = this._coveredFiles[filename];
+        for (let [filename, branchCounters] of this._coveredFiles) {
+            // Filter out undefined lines, since the index represents the
+            // current line number on the file (see _incrementLineCounters())
+            let branches = branchCounters.filter(c => c !== undefined)
+            .map(({point, exits}) => ({
+                point,
+                exits: exits.map(({line}) => line),
+            }));
+
             let mtime = getFileModificationTime(filename);
-            let cacheDataForFilename = {
-                mtime: mtime,
+            cache_data[filename] = {
+                mtime,
                 checksum: mtime === null ? getFileChecksum(filename) : null,
-                lines: [],
-                branches: [],
-                functions: 
_convertFunctionCountersToArray(statisticsForFilename.functionCounters).map(function(func) {
-                    return {
-                        key: func.name,
-                        line: func.line
-                    };
-                })
+                branches,
             };
-
-            /* We're using a index based loop here since we need access to the
-             * index, since it actually represents the current line number
-             * on the file (see _expressionLinesToCounters). */
-            for (let line_index = 0;
-                 line_index < statisticsForFilename.expressionCounters.length;
-                 ++line_index) {
-                 if (statisticsForFilename.expressionCounters[line_index] !== undefined)
-                     cacheDataForFilename.lines.push(line_index);
-
-                 if (statisticsForFilename.branchCounters[line_index] !== undefined) {
-                     let branchCounters = statisticsForFilename.branchCounters[line_index];
-                     cacheDataForFilename.branches.push({
-                         point: statisticsForFilename.branchCounters[line_index].point,
-                         exits: statisticsForFilename.branchCounters[line_index].exits.map(function(exit) {
-                             return exit.line;
-                         })
-                     });
-                 }
-            }
-            cache_data[filename] = cacheDataForFilename;
-        });
+        }
         return JSON.stringify(cache_data);
     }
 
     getCoveredFiles() {
-        return Object.keys(this._coveredFiles);
+        return [...this._coveredFiles.keys()];
     }
 
     fetchStatistics(filename) {
@@ -884,7 +565,7 @@ var CoverageStatisticsContainer = class {
     }
 
     deleteStatistics(filename) {
-        this._coveredFiles[filename] = undefined;
+        this._coveredFiles.delete(filename);
     }
 };
 
@@ -894,14 +575,18 @@ var CoverageStatisticsContainer = class {
  * It isn't possible to unit test this class because it depends on running
  * Debugger which in turn depends on objects injected in from another compartment */
 var CoverageStatistics = class {
-    constructor(prefixes, cache, shouldWarn) {
-        let _shouldWarn = shouldWarn;  // capture in closure
+    constructor(prefixes, cache) {
         let container = new CoverageStatisticsContainer(prefixes, cache);
         this.container = container;
 
         /* 'debuggee' comes from the invocation from
          * a separate compartment inside of coverage.cpp */
         this.dbg = new Debugger(debuggee);
+        this.dbg.collectCoverageInfo = true;
+        this.scripts = new Set(this.dbg.findScripts());
+
+        this.dbg.onNewScript = this._addScript.bind(this);
+
         this.dbg.onEnterFrame = function (frame) {
             let statistics;
 
@@ -916,57 +601,32 @@ var CoverageStatistics = class {
                 return undefined;
             }
 
-            function _logExceptionAndReset(exception, callee, line) {
-                log(`${exception.fileName}:${exception.lineNumber} (processing ` +
-                    `${frame.script.url}:${callee}:${line}) - ${exception.message}`);
-                log('Will not log statistics for this file');
-                frame.onStep = undefined;
-                frame._branchTracker = undefined;
-                container.deleteStatistics(frame.script.url);
-            }
-
-            /* Log function calls */
-            if (frame.callee !== null && frame.callee.callable) {
-                let name = frame.callee.name || '(anonymous)';
-                let {lineNumber} = frame.script.getOffsetLocation(frame.offset);
-                let nArgs = frame.callee.parameterNames.length;
-
-                try {
-                    _incrementFunctionCounters(statistics.functionCounters,
-                        statistics.linesWithKnownFunctions, name, lineNumber,
-                        nArgs);
-                } catch (e) {
-                    /* Something bad happened. Log the exception and delete
-                     * statistics for this file */
-                    _logExceptionAndReset(e, name, lineNumber);
-                    return undefined;
-                }
-            }
-
             /* Upon entering the frame, the active branch is always inactive */
-            frame._branchTracker = new _BranchTracker(statistics.branchCounters);
+            frame._branchTracker = new _BranchTracker(statistics);
 
             /* Set single-step hook */
             frame.onStep = function() {
-                /* Line counts */
-                let {offset} = this;
-                let {lineNumber} = this.script.getOffsetLocation(offset);
-
-                try {
-                    _incrementExpressionCounters(statistics.expressionCounters,
-                        frame.script.url, lineNumber, _shouldWarn);
-                    this._branchTracker.incrementBranchCounters(lineNumber);
-                } catch (e) {
-                    /* Something bad happened. Log the exception and delete
-                     * statistics for this file */
-                    _logExceptionAndReset(e, frame.callee, lineNumber);
-                }
+                let {lineNumber} = this.script.getOffsetLocation(this.offset);
+                this._branchTracker.incrementBranchCounters(lineNumber);
             };
 
             return undefined;
         };
     }
 
+    _addScript(script) {
+        // Skip scripts fed to JS engine programmatically.
+        if (script.url.startsWith('<') && script.url.endsWith('>'))
+            return;
+
+        this.scripts.add(script);
+        script.getChildScripts().forEach(this._addScript, this);
+    }
+
+    _findScriptsForFilename(filename) {
+        return [...this.scripts].filter(({url}) => url === filename);
+    }
+
     getCoveredFiles() {
         return this.container.getCoveredFiles();
     }
@@ -979,21 +639,39 @@ var CoverageStatistics = class {
         return this.container.stringify();
     }
 
-    getNumberOfLinesFor(filename) {
-        return this.container.fetchStatistics(filename).nLines;
-    }
-
     getExecutedLinesFor(filename) {
-        return this.container.fetchStatistics(filename).expressionCounters;
+        let scripts = this._findScriptsForFilename(filename);
+        let counters = [];
+
+        scripts.forEach(script => {
+            let offsets = script.getAllOffsets();
+            let coverage = script.getOffsetsCoverage();
+            _incrementLineCounters(counters, offsets, coverage);
+        });
+
+        return counters;
     }
 
     getBranchesFor(filename) {
-        return this.container.fetchStatistics(filename).branchCounters;
+        return this.container.fetchStatistics(filename);
     }
 
     getFunctionsFor(filename) {
-        let {functionCounters} = this.container.fetchStatistics(filename);
-        return _convertFunctionCountersToArray(functionCounters);
+        let scripts = this._findScriptsForFilename(filename);
+        let counters = new Map();
+
+        scripts.forEach(script => {
+            // Ignore top-level file script
+            if (script.sourceStart === 0)
+                return;
+
+            let name = script.displayName || '(anonymous)';
+            let key = `${name}:${script.startLine}@${script.sourceStart}`;
+            let coverage = script.getOffsetsCoverage();
+            _incrementFunctionCounters(counters, key, script.startLine, coverage);
+        });
+
+        return _convertFunctionCountersToArray(counters);
     }
 
     deactivate() {
diff --git a/test/gjs-test-coverage.cpp b/test/gjs-test-coverage.cpp
index 10d08a7..e1a0b02 100644
--- a/test/gjs-test-coverage.cpp
+++ b/test/gjs-test-coverage.cpp
@@ -820,11 +820,9 @@ test_function_names_written_to_coverage_data(gpointer      fixture_data,
                                           fixture->lcov_output,
                                           NULL);
 
-    /* The internal hash table is sorted in alphabetical order
-     * so the function names need to be in this order too */
     const char * expected_function_names[] = {
-        "(anonymous):2:0",
-        "f:1:0"
+        "f:1@10",
+        "b:2@31"
     };
     const gsize expected_function_names_len = G_N_ELEMENTS(expected_function_names);
 
@@ -953,11 +951,9 @@ test_function_hit_counts_for_big_functions_written_to_coverage_data(gpointer
                                           fixture->lcov_output,
                                           NULL);
 
-    /* The internal hash table is sorted in alphabetical order
-     * so the function names need to be in this order too */
     FunctionHitCountData expected_hit_counts[] = {
-        { "(anonymous):6:0", 1 },
-        { "f:1:0", 1 }
+        { "f:1@10", 1 },
+        { "b:6@45", 1 }
     };
 
     const gsize expected_hit_count_len = G_N_ELEMENTS(expected_hit_counts);
@@ -999,12 +995,10 @@ test_function_hit_counts_for_little_functions_written_to_coverage_data(gpointer
                                           fixture->lcov_output,
                                           NULL);
 
-    /* The internal hash table is sorted in alphabetical order
-     * so the function names need to be in this order too */
     FunctionHitCountData expected_hit_counts[] = {
-        { "(anonymous):2:0", 0 },
-        { "(anonymous):4:0", 1 },
-        { "f:1:0", 1 }
+        { "f:1@10", 1 },
+        { "f/x:2@30", 0 },
+        { "b:4@54", 1 }
     };
 
     const gsize expected_hit_count_len = G_N_ELEMENTS(expected_hit_counts);
@@ -1042,11 +1036,9 @@ test_function_hit_counts_written_to_coverage_data(gpointer      fixture_data,
                                           fixture->lcov_output,
                                           NULL);
 
-    /* The internal hash table is sorted in alphabetical order
-     * so the function names need to be in this order too */
     FunctionHitCountData expected_hit_counts[] = {
-        { "(anonymous):2:0", 1 },
-        { "f:1:0", 1 }
+        { "f:1@10", 1 },
+        { "b:2@31", 1 }
     };
 
     const gsize expected_hit_count_len = G_N_ELEMENTS(expected_hit_counts);
@@ -1459,22 +1451,17 @@ static GString *
 format_expected_cache_object_notation(const char *mtimes,
                                       const char *hash,
                                       GFile      *script,
-                                      const char *expected_executable_lines_array,
-                                      const char *expected_branches,
-                                      const char *expected_functions)
+                                      const char *expected_branches)
 {
     char *script_name = get_script_identifier(script);
     GString *string = g_string_new("");
     g_string_append_printf(string,
-                           "{\"%s\":{\"mtime\":%s,\"checksum\":%s,\"lines\":[%s],\"branches\":[",
+                           "{\"%s\":{\"mtime\":%s,\"checksum\":%s,\"branches\":[",
                            script_name,
                            mtimes,
-                           hash,
-                           expected_executable_lines_array);
+                           hash);
     g_free(script_name);
     append_tuples_to_array_in_object_notation(string, expected_branches);
-    g_string_append_printf(string, "],\"functions\":[");
-    append_tuples_to_array_in_object_notation(string, expected_functions);
     g_string_append_printf(string, "]}}");
     return string;
 }
@@ -1483,9 +1470,7 @@ typedef struct _GjsCoverageCacheObjectNotationTestTableData {
     const char *test_name;
     const char *script;
     const char *uri;
-    const char *expected_executable_lines;
     const char *expected_branches;
-    const char *expected_functions;
 } GjsCoverageCacheObjectNotationTableTestData;
 
 static GBytes *
@@ -1550,9 +1535,7 @@ test_coverage_cache_data_in_expected_format(gpointer      fixture_data,
     GString *expected_cache_object_notation = format_expected_cache_object_notation(mtime_string,
                                                                                     "null",
                                                                                     fixture->tmp_js_script,
-                                                                                    
table_data->expected_executable_lines,
-                                                                                    
table_data->expected_branches,
-                                                                                    
table_data->expected_functions);
+                                                                                    
table_data->expected_branches);
 
     g_assert_cmpstr(cache_in_object_notation, ==, expected_cache_object_notation->str);
 
@@ -1578,9 +1561,7 @@ test_coverage_cache_data_in_expected_format_resource(gpointer      fixture_data,
     GString *expected_cache_object_notation = format_expected_cache_object_notation("null",
                                                                                     hash_string,
                                                                                     resource,
-                                                                                    
table_data->expected_executable_lines,
-                                                                                    
table_data->expected_branches,
-                                                                                    
table_data->expected_functions);
+                                                                                    
table_data->expected_branches);
 
     g_clear_object(&fixture->coverage);
     fixture->coverage = create_coverage_for_script(fixture->context, resource,
@@ -1619,20 +1600,20 @@ generate_coverage_compartment_verify_script(GFile      *coverage_script,
     return retval;
 }
 
-typedef struct _GjsCoverageCacheJSObjectTableTestData {
-    const char *test_name;
-    const char *script;
-    const char *verify_js_script;
-} GjsCoverageCacheJSObjectTableTestData;
-
 static void
-test_coverage_cache_as_js_object_has_expected_properties(gpointer      fixture_data,
-                                                         gconstpointer user_data)
-{
-    GjsCoverageFixture *fixture = (GjsCoverageFixture *) fixture_data;
-    GjsCoverageCacheJSObjectTableTestData *table_data = (GjsCoverageCacheJSObjectTableTestData *) user_data;
-
-    replace_file(fixture->tmp_js_script, table_data->script);
+test_coverage_cache_as_js_object_has_branches_prop(void       *fixture_data,
+                                                   const void *unused)
+{
+    auto fixture = static_cast<GjsCoverageFixture *>(fixture_data);
+    const char *script =
+        "let i = 0;\n"
+        "if (i) {\n"
+        "    i = 1;\n"
+        "} else {\n"
+        "    i = 2;\n"
+        "}\n";
+
+    replace_file(fixture->tmp_js_script, script);
     eval_script(fixture->context, fixture->tmp_js_script);
 
     char *script_filename = g_file_get_path(fixture->tmp_js_script);
@@ -1649,8 +1630,11 @@ test_coverage_cache_as_js_object_has_expected_properties(gpointer      fixture_d
                                                cache_result_value,
                                                "coverage_cache");
 
+    const char *verify_script =
+        "assertEquals(2, JSON.parse(coverage_cache)[covered_script_filename].branches[0].point);\n"
+        "assertArrayEquals([3, 5], 
JSON.parse(coverage_cache)[covered_script_filename].branches[0].exits);\n";
     char *verify_script_complete = generate_coverage_compartment_verify_script(fixture->tmp_js_script,
-                                                                               table_data->verify_js_script);
+                                                                               verify_script);
     gjs_run_script_in_coverage_compartment(fixture->coverage,
                                            verify_script_complete);
     g_free(verify_script_complete);
@@ -2278,8 +2262,6 @@ void gjs_test_add_tests_for_coverage()
             "simple_executable_lines",
             "let i = 0;\n",
             "resource://org/gnome/gjs/mock/test/gjs-test-coverage/cache_notation/simple_executable_lines.js",
-            "1",
-            "",
             ""
         },
         {
@@ -2291,18 +2273,14 @@ void gjs_test_add_tests_for_coverage()
             "    i = 2;\n"
             "}\n",
             "resource://org/gnome/gjs/mock/test/gjs-test-coverage/cache_notation/simple_branch.js",
-            "1,2,3,5",
-            "\"point\":2,\"exits\":[3,5]",
-            ""
+            "\"point\":2,\"exits\":[3,5]"
         },
         {
             "simple_function",
             "function f() {\n"
             "}\n",
             "resource://org/gnome/gjs/mock/test/gjs-test-coverage/cache_notation/simple_function.js",
-            "1,2",
-            "",
-            "\"key\":\"f:1:0\",\"line\":1"
+            ""
         }
     };
 
@@ -2320,37 +2298,10 @@ void gjs_test_add_tests_for_coverage()
                                       G_N_ELEMENTS(data_in_expected_format_table),
                                       (const TestTableDataHeader *) data_in_expected_format_table);
 
-    static GjsCoverageCacheJSObjectTableTestData object_has_expected_properties_table[] = {
-        {
-            "simple_executable_lines",
-            "let i = 0;\n",
-            "assertArrayEquals(JSON.parse(coverage_cache)[covered_script_filename].lines, [1]);\n"
-        },
-        {
-            "simple_branch",
-            "let i = 0;\n"
-            "if (i) {\n"
-            "    i = 1;\n"
-            "} else {\n"
-            "    i = 2;\n"
-            "}\n",
-            "assertEquals(2, JSON.parse(coverage_cache)[covered_script_filename].branches[0].point);\n"
-            "assertArrayEquals([3, 5], 
JSON.parse(coverage_cache)[covered_script_filename].branches[0].exits);\n"
-        },
-        {
-            "simple_function",
-            "function f() {\n"
-            "}\n",
-            "assertEquals('f:1:0', JSON.parse(coverage_cache)[covered_script_filename].functions[0].key);\n"
-        }
-    };
-
-    add_table_driven_test_for_fixture("/gjs/coverage/cache/object_props",
-                                      &coverage_fixture,
-                                      test_coverage_cache_as_js_object_has_expected_properties,
-                                      sizeof(GjsCoverageCacheJSObjectTableTestData),
-                                      G_N_ELEMENTS(object_has_expected_properties_table),
-                                      (const TestTableDataHeader *) object_has_expected_properties_table);
+    add_test_for_fixture("/gjs/coverage/cache/object_branches_prop",
+                         &coverage_fixture,
+                         test_coverage_cache_as_js_object_has_branches_prop,
+                         NULL);
 
     static GjsCoverageCacheEqualResultsTableTestData equal_results_table[] = {
         {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]