Bug 1479828 - Expand suppression to general limits, r=jonco
authorSteve Fink <sfink@mozilla.com>
Tue, 26 Sep 2017 21:03:09 -0700
changeset 500007 77ada590e04777dd99d3632cdcd2ed34ea5d4268
parent 500006 8a07eed99ca8315b5b275eec185561172f9e3ffe
child 500008 f6173c4ae224f1a7df7a23195003fe312331e85d
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjonco
bugs1479828
milestone64.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 1479828 - Expand suppression to general limits, r=jonco
js/src/devtools/rootAnalysis/CFG.js
js/src/devtools/rootAnalysis/analyze.py
js/src/devtools/rootAnalysis/analyzeRoots.js
js/src/devtools/rootAnalysis/annotations.js
js/src/devtools/rootAnalysis/callgraph.js
js/src/devtools/rootAnalysis/computeCallgraph.js
js/src/devtools/rootAnalysis/computeGCFunctions.js
js/src/devtools/rootAnalysis/dumpCFG.js
js/src/devtools/rootAnalysis/loadCallgraph.js
js/src/devtools/rootAnalysis/utility.js
--- a/js/src/devtools/rootAnalysis/CFG.js
+++ b/js/src/devtools/rootAnalysis/CFG.js
@@ -1,33 +1,33 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 
 "use strict";
 
 var functionBodies;
 
-function findAllPoints(bodies, blockId)
+function findAllPoints(bodies, blockId, limits)
 {
     var points = [];
     var body;
 
     for (var xbody of bodies) {
         if (sameBlockId(xbody.BlockId, blockId)) {
             assert(!body);
             body = xbody;
         }
     }
     assert(body);
 
     if (!("PEdge" in body))
         return;
     for (var edge of body.PEdge) {
-        points.push([body, edge.Index[0]]);
+        points.push([body, edge.Index[0], limits]);
         if (edge.Kind == "Loop")
-            Array.prototype.push.apply(points, findAllPoints(bodies, edge.BlockId));
+            points.push(...findAllPoints(bodies, edge.BlockId, limits));
     }
 
     return points;
 }
 
 function isMatchingDestructor(constructor, edge)
 {
     if (edge.Kind != "Call")
@@ -73,24 +73,25 @@ function allRAIIGuardedCallPoints(typeIn
     for (var edge of body.PEdge) {
         if (edge.Kind != "Call")
             continue;
         var callee = edge.Exp[0];
         if (callee.Kind != "Var")
             continue;
         var variable = callee.Variable;
         assert(variable.Kind == "Func");
-        if (!isConstructor(typeInfo, edge.Type, variable.Name))
+        const limits = isConstructor(typeInfo, edge.Type, variable.Name);
+        if (!limits)
             continue;
         if (!("PEdgeCallInstance" in edge))
             continue;
         if (edge.PEdgeCallInstance.Exp.Kind != "Var")
             continue;
 
-        Array.prototype.push.apply(points, pointsInRAIIScope(bodies, body, edge));
+        points.push(...pointsInRAIIScope(bodies, body, edge, limits));
     }
 
     return points;
 }
 
 // Test whether the given edge is the constructor corresponding to the given
 // destructor edge
 function isMatchingConstructor(destructor, edge)
@@ -136,32 +137,32 @@ function findMatchingConstructor(destruc
             for (var e of predecessors[edge.Index[0]])
                 worklist.push(e);
         }
     }
     printErr("Could not find matching constructor!");
     debugger;
 }
 
-function pointsInRAIIScope(bodies, body, constructorEdge) {
+function pointsInRAIIScope(bodies, body, constructorEdge, limits) {
     var seen = {};
     var worklist = [constructorEdge.Index[1]];
     var points = [];
     while (worklist.length) {
         var point = worklist.pop();
         if (point in seen)
             continue;
         seen[point] = true;
-        points.push([body, point]);
+        points.push([body, point, limits]);
         var successors = getSuccessors(body);
         if (!(point in successors))
             continue;
         for (var nedge of successors[point]) {
             if (isMatchingDestructor(constructorEdge, nedge))
                 continue;
             if (nedge.Kind == "Loop")
-                Array.prototype.push.apply(points, findAllPoints(bodies, nedge.BlockId));
+                points.push(...findAllPoints(bodies, nedge.BlockId, limits));
             worklist.push(nedge.Index[1]);
         }
     }
 
     return points;
 }
--- a/js/src/devtools/rootAnalysis/analyze.py
+++ b/js/src/devtools/rootAnalysis/analyze.py
@@ -83,17 +83,17 @@ def print_command(command, outfile=None,
 
 def generate_hazards(config, outfilename):
     jobs = []
     for i in range(int(config['jobs'])):
         command = fill(('%(js)s',
                         '%(analysis_scriptdir)s/analyzeRoots.js',
                         '%(gcFunctions_list)s',
                         '%(gcEdges)s',
-                        '%(suppressedFunctions_list)s',
+                        '%(limitedFunctions_list)s',
                         '%(gcTypes)s',
                         '%(typeInfo)s',
                         str(i+1), '%(jobs)s',
                         'tmp.%s' % (i+1,)),
                        config)
         outfile = 'rootingHazards.%s' % (i+1,)
         output = open(outfile, 'w')
         if config['verbose']:
@@ -134,18 +134,18 @@ JOBS = {'dbs':
 
         'callgraph':
         (('%(js)s', '%(analysis_scriptdir)s/computeCallgraph.js', '%(typeInfo)s',
           '[callgraph]'),
          ('callgraph.txt',)),
 
         'gcFunctions':
         (('%(js)s', '%(analysis_scriptdir)s/computeGCFunctions.js', '%(callgraph)s',
-          '[gcFunctions]', '[gcFunctions_list]', '[gcEdges]', '[suppressedFunctions_list]'),
-         ('gcFunctions.txt', 'gcFunctions.lst', 'gcEdges.txt', 'suppressedFunctions.lst')),
+          '[gcFunctions]', '[gcFunctions_list]', '[gcEdges]', '[limitedFunctions_list]'),
+         ('gcFunctions.txt', 'gcFunctions.lst', 'gcEdges.txt', 'limitedFunctions.lst')),
 
         'gcTypes':
         (('%(js)s', '%(analysis_scriptdir)s/computeGCTypes.js',
           '[gcTypes]', '[typeInfo]'),
          ('gcTypes.txt', 'typeInfo.txt')),
 
         'allFunctions':
         (('%(sixgill_bin)s/xdbkeys', 'src_body.xdb',),
--- a/js/src/devtools/rootAnalysis/analyzeRoots.js
+++ b/js/src/devtools/rootAnalysis/analyzeRoots.js
@@ -8,44 +8,46 @@ loadRelativeToScript('CFG.js');
 loadRelativeToScript('dumpCFG.js');
 
 var sourceRoot = (os.getenv('SOURCE') || '') + '/'
 
 var functionName;
 var functionBodies;
 
 if (typeof scriptArgs[0] != 'string' || typeof scriptArgs[1] != 'string')
-    throw "Usage: analyzeRoots.js [-f function_name] <gcFunctions.lst> <gcEdges.txt> <suppressedFunctions.lst> <gcTypes.txt> <typeInfo.txt> [start end [tmpfile]]";
+    throw "Usage: analyzeRoots.js [-f function_name] <gcFunctions.lst> <gcEdges.txt> <limitedFunctions.lst> <gcTypes.txt> <typeInfo.txt> [start end [tmpfile]]";
 
 var theFunctionNameToFind;
 if (scriptArgs[0] == '--function' || scriptArgs[0] == '-f') {
     theFunctionNameToFind = scriptArgs[1];
     scriptArgs = scriptArgs.slice(2);
 }
 
 var gcFunctionsFile = scriptArgs[0] || "gcFunctions.lst";
 var gcEdgesFile = scriptArgs[1] || "gcEdges.txt";
-var suppressedFunctionsFile = scriptArgs[2] || "suppressedFunctions.lst";
+var limitedFunctionsFile = scriptArgs[2] || "limitedFunctions.lst";
 var gcTypesFile = scriptArgs[3] || "gcTypes.txt";
 var typeInfoFile = scriptArgs[4] || "typeInfo.txt";
 var batch = (scriptArgs[5]|0) || 1;
 var numBatches = (scriptArgs[6]|0) || 1;
 var tmpfile = scriptArgs[7] || "tmp.txt";
 
 var gcFunctions = {};
 var text = snarf("gcFunctions.lst").split("\n");
 assert(text.pop().length == 0);
 for (var line of text)
     gcFunctions[mangled(line)] = true;
 
-var suppressedFunctions = {};
-var text = snarf(suppressedFunctionsFile).split("\n");
+var limitedFunctions = {};
+var text = snarf(limitedFunctionsFile).split("\n");
 assert(text.pop().length == 0);
 for (var line of text) {
-    suppressedFunctions[line] = true;
+    const [_, limits, func] = line.match(/(.*?) (.*)/);
+    assert(limits !== undefined);
+    limitedFunctions[func] = limits | 0;
 }
 text = null;
 
 var typeInfo = loadTypeInfo(typeInfoFile);
 
 var gcEdges = {};
 text = snarf(gcEdgesFile).split('\n');
 assert(text.pop().length == 0);
@@ -357,17 +359,21 @@ function edgeCanGC(edge)
     }
 
     if (callee.Kind == "Fld") {
         var field = callee.Field;
         var csuName = field.FieldCSU.Type.Name;
         var fullFieldName = csuName + "." + field.Name[0];
         if (fieldCallCannotGC(csuName, fullFieldName))
             return null;
-        return (fullFieldName in suppressedFunctions) ? null : fullFieldName;
+
+        if (fullFieldName in gcFunctions)
+            return fullFieldName;
+
+        return null;
     }
 }
 
 // Search recursively through predecessors from the use of a variable's value,
 // returning whether a GC call is reachable (in the reverse direction; this
 // means that the variable use is reachable from the GC call, and therefore the
 // variable is live after the GC call), along with some additional information.
 // What info we want depends on whether the variable turns out to be live
@@ -496,17 +502,17 @@ function findGCBeforeValueUse(start_body
 
                 // Otherwise, keep searching through the graph, but truncate
                 // this particular branch of the search at this edge.
                 continue;
             }
 
             var src_gcInfo = gcInfo;
             var src_preGCLive = preGCLive;
-            if (!gcInfo && !(source in body.suppressed) && !suppressed) {
+            if (!gcInfo && !(body.limits[source] & LIMIT_CANNOT_GC) && !suppressed) {
                 var gcName = edgeCanGC(edge, body);
                 if (gcName)
                     src_gcInfo = {name:gcName, body:body, ppoint:source};
             }
 
             if (edge_uses) {
                 // The live range starts at least this far back, so we're done
                 // for the same reason as with edge_kills. The only difference
@@ -778,17 +784,17 @@ function typeDesc(type)
         return '???';
     }
 }
 
 function processBodies(functionName)
 {
     if (!("DefineVariable" in functionBodies[0]))
         return;
-    var suppressed = (mangled(functionName) in suppressedFunctions);
+    var suppressed = Boolean(limitedFunctions[mangled(functionName)] & LIMIT_CANNOT_GC);
     for (var variable of functionBodies[0].DefineVariable) {
         var name;
         if (variable.Variable.Kind == "This")
             name = "this";
         else if (variable.Variable.Kind == "Return")
             name = "<returnvalue>";
         else
             name = variable.Variable.Name[0];
@@ -843,21 +849,29 @@ var N = (maxStream - minStream) + 1;
 var start = Math.floor((batch - 1) / numBatches * N) + minStream;
 var start_next = Math.floor(batch / numBatches * N) + minStream;
 var end = start_next - 1;
 
 function process(name, json) {
     functionName = name;
     functionBodies = JSON.parse(json);
 
+    // Annotate body with a table of all points within the body that may be in
+    // a limited scope (eg within the scope of a GC suppression RAII class.)
+    // body.limits is a plain object indexed by point, with the value being a
+    // bit set stored in an integer of the limit bits.
     for (var body of functionBodies)
-        body.suppressed = [];
+        body.limits = [];
+
     for (var body of functionBodies) {
-        for (var [pbody, id] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isSuppressConstructor))
-            pbody.suppressed[id] = true;
+        for (var [pbody, id, limits] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor))
+        {
+            if (limits)
+                pbody.limits[id] = limits;
+        }
     }
     processBodies(functionName);
 }
 
 if (theFunctionNameToFind) {
     var data = xdb.read_entry(theFunctionNameToFind);
     var json = data.readString();
     process(theFunctionNameToFind, json);
--- a/js/src/devtools/rootAnalysis/annotations.js
+++ b/js/src/devtools/rootAnalysis/annotations.js
@@ -335,41 +335,47 @@ function isRootedGCPointerTypeName(name)
 }
 
 function isUnsafeStorage(typeName)
 {
     typeName = stripUCSAndNamespace(typeName);
     return typeName.startsWith('UniquePtr<');
 }
 
-function isSuppressConstructor(typeInfo, edgeType, varName)
+// If edgeType is a constructor type, return whatever limits it implies for its
+// scope (or zero if not matching).
+function isLimitConstructor(typeInfo, edgeType, varName)
 {
     // Check whether this could be a constructor
     if (edgeType.Kind != 'Function')
-        return false;
+        return 0;
     if (!('TypeFunctionCSU' in edgeType))
-        return false;
+        return 0;
     if (edgeType.Type.Kind != 'Void')
-        return false;
+        return 0;
 
     // Check whether the type is a known suppression type.
     var type = edgeType.TypeFunctionCSU.Type.Name;
-    if (!(type in typeInfo.GCSuppressors))
-        return false;
+    let limit = 0;
+    if (type in typeInfo.GCSuppressors)
+        limit = limit | LIMIT_CANNOT_GC;
 
     // And now make sure this is the constructor, not some other method on a
     // suppression type. varName[0] contains the qualified name.
     var [ mangled, unmangled ] = splitFunction(varName[0]);
-    if (mangled.search(/C\dE/) == -1)
-        return false; // Mangled names of constructors have C<num>E
+    if (mangled.search(/C\d[EI]/) == -1)
+        return 0; // Mangled names of constructors have C<num>E or C<num>I
     var m = unmangled.match(/([~\w]+)(?:<.*>)?\(/);
     if (!m)
-        return false;
+        return 0;
     var type_stem = type.replace(/\w+::/g, '').replace(/\<.*\>/g, '');
-    return m[1] == type_stem;
+    if (m[1] != type_stem)
+        return 0;
+
+    return limit;
 }
 
 // nsISupports subclasses' methods may be scriptable (or overridden
 // via binary XPCOM), and so may GC. But some fields just aren't going
 // to get overridden with something that can GC.
 function isOverridableField(initialCSU, csu, field)
 {
     if (csu != 'nsISupports')
--- a/js/src/devtools/rootAnalysis/callgraph.js
+++ b/js/src/devtools/rootAnalysis/callgraph.js
@@ -13,20 +13,22 @@ function addToNamedSet(map, name, entry)
 {
     if (!map.has(name))
         map.set(name, new Set());
     map.get(name).add(entry);
 }
 
 function fieldKey(csuName, field)
 {
-    // Note: not dealing with overloading correctly.
+    // This makes a minimal attempt at dealing with overloading: it will not
+    // conflate two virtual methods with differing numbers of arguments. So
+    // far, that is all that has been needed.
     var nargs = 0;
     if (field.Type.Kind == "Function" && "TypeFunctionArguments" in field.Type)
-	nargs = field.Type.TypeFunctionArguments.length;
+        nargs = field.Type.TypeFunctionArguments.Type.length;
     return csuName + ":" + field.Name[0] + ":" + nargs;
 }
 
 // CSU is "Class/Struct/Union"
 function processCSU(csuName, csu)
 {
     if (!("FunctionField" in csu))
         return;
@@ -59,20 +61,21 @@ function nearestAncestorMethods(csu, fie
     if (superclasses.has(csu)) {
         for (const parent of superclasses.get(csu))
             functions.update(nearestAncestorMethods(parent, field));
     }
 
     return functions;
 }
 
-// Return [ instantations, suppressed ], where instantiations is a Set of all
+// Return [ instantiations, limits ], where instantiations is a Set of all
 // possible implementations of 'field' given static type 'initialCSU', plus
-// null if arbitrary other implementations are possible, and suppressed is true
-// if we the method is assumed to be non-GC'ing by annotation.
+// null if arbitrary other implementations are possible, and limits gives
+// information about what things are not possible within it (currently, that it
+// cannot GC).
 function findVirtualFunctions(initialCSU, field)
 {
     const fieldName = field.Name[0];
     const worklist = [initialCSU];
     const functions = new Set();
 
     // Loop through all methods of initialCSU (by looking at all methods of ancestor csus).
     //
@@ -82,17 +85,17 @@ function findVirtualFunctions(initialCSU
     // If this is a method that is annotated to be dangerous (eg, it could be
     // overridden with an implementation that could GC), then use null as a
     // signal value that it should be considered to GC, even though we'll also
     // collect all of the instantiations for other purposes.
 
     while (worklist.length) {
         const csu = worklist.pop();
         if (isSuppressedVirtualMethod(csu, fieldName))
-            return [ new Set(), true ];
+            return [ new Set(), LIMIT_CANNOT_GC ];
         if (isOverridableField(initialCSU, csu, fieldName)) {
             // We will still resolve the virtual function call, because it's
             // nice to have as complete a callgraph as possible for other uses.
             // But push a token saying that we can run arbitrary code.
             functions.add(null);
         }
 
         if (superclasses.has(csu))
@@ -115,17 +118,17 @@ function findVirtualFunctions(initialCSU
 
         if (classFunctions.has(key))
             functions.update(classFunctions.get(key));
 
         if (subclasses.has(csu))
             worklist.push(...subclasses.get(csu));
     }
 
-    return [ functions, false ];
+    return [ functions, LIMIT_NONE ];
 }
 
 // Return a list of all callees that the given edge might be a call to. Each
 // one is represented by an object with a 'kind' field that is one of
 // ('direct', 'field', 'resolved-field', 'indirect', 'unknown'), though note
 // that 'resolved-field' is really a global record of virtual method
 // resolutions, indepedent of this particular edge.
 function getCallees(edge)
@@ -136,17 +139,17 @@ function getCallees(edge)
     const callee = edge.Exp[0];
     if (callee.Kind == "Var") {
         assert(callee.Variable.Kind == "Func");
         return [{'kind': 'direct', 'name': callee.Variable.Name[0]}];
     }
 
     if (callee.Kind == "Int")
         return []; // Intentional crash
-  
+
     assert(callee.Kind == "Drf");
     const called = callee.Exp[0];
     if (called.Kind == "Var") {
         // indirect call through a variable.
         return [{'kind': "indirect", 'variable': callee.Exp[0].Variable.Name[0]}];
     }
 
     if (called.Kind != "Fld") {
@@ -154,25 +157,21 @@ function getCallees(edge)
         return [{'kind': "unknown"}];
     }
 
     const callees = [];
     const field = callee.Exp[0].Field;
     const fieldName = field.Name[0];
     const csuName = field.FieldCSU.Type.Name;
     let functions;
+    let limits = LIMIT_NONE;
     if ("FieldInstanceFunction" in field) {
-        let suppressed;
-        [ functions, suppressed ] = findVirtualFunctions(csuName, field, suppressed);
-        if (suppressed) {
-            // Field call known to not GC; mark it as suppressed so direct
-            // invocations will be ignored
-            callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
-                          'suppressed': true, 'isVirtual': true});
-        }
+        [ functions, limits ] = findVirtualFunctions(csuName, field);
+        callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
+                      'limits': limits, 'isVirtual': true});
     } else {
         functions = new Set([null]); // field call
     }
 
     // Known set of virtual call targets. Treat them as direct calls to all
     // possible resolved types, but also record edges from this field call to
     // each final callee. When the analysis is checking whether an edge can GC
     // and it sees an unrooted pointer held live across this field call, it
@@ -181,21 +180,21 @@ function getCallees(edge)
     let fullyResolved = true;
     for (const name of functions) {
         if (name === null) {
             // Unknown set of call targets, meaning either a function pointer
             // call ("field call") or a virtual method that can be overridden
             // in extensions. Use the isVirtual property so that callers can
             // tell which case holds.
             callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
+                          'limits': limits,
 			  'isVirtual': "FieldInstanceFunction" in field});
             fullyResolved = false;
         } else {
-            callees.push({'kind': "direct", 'name': name});
-            targets.push({'kind': "direct", 'name': name});
+            targets.push({'kind': "direct", name, limits });
         }
     }
     if (fullyResolved)
         callees.push({'kind': "resolved-field", 'csu': csuName, 'field': fieldName, 'callees': targets});
 
     return callees;
 }
 
--- a/js/src/devtools/rootAnalysis/computeCallgraph.js
+++ b/js/src/devtools/rootAnalysis/computeCallgraph.js
@@ -32,18 +32,23 @@ var lastline;
 function printOnce(line)
 {
     if (line != lastline) {
         print(line);
         lastline = line;
     }
 }
 
-// Returns a table mapping function name to lists of [annotation-name,
-// annotation-value] pairs: { function-name => [ [annotation-name, annotation-value] ] }
+// Returns a table mapping function name to lists of
+// [annotation-name, annotation-value] pairs:
+//   { function-name => [ [annotation-name, annotation-value] ] }
+//
+// Note that sixgill will only store certain attributes (annotation-names), so
+// this won't be *all* the attributes in the source, just the ones that sixgill
+// watches for.
 function getAnnotations(body)
 {
     var all_annotations = {};
     for (var v of (body.DefineVariable || [])) {
         if (v.Variable.Kind != 'Func')
             continue;
         var name = v.Variable.Name[0];
         var annotations = all_annotations[name] = [];
@@ -51,67 +56,80 @@ function getAnnotations(body)
         for (var ann of (v.Type.Annotation || [])) {
             annotations.push(ann.Name);
         }
     }
 
     return all_annotations;
 }
 
+// Get just the annotations understood by the hazard analysis.
 function getTags(functionName, body) {
     var tags = new Set();
     var annotations = getAnnotations(body);
     if (functionName in annotations) {
         for (var [ annName, annValue ] of annotations[functionName]) {
             if (annName == 'Tag')
                 tags.add(annValue);
         }
     }
     return tags;
 }
 
+// Scan through a function body, pulling out all annotations and calls and
+// recording them in callgraph.txt.
 function processBody(functionName, body)
 {
     if (!('PEdge' in body))
         return;
 
     for (var tag of getTags(functionName, body).values())
         print("T " + memo(functionName) + " " + tag);
 
     // Set of all callees that have been output so far, in order to suppress
-    // repeated callgraph edges from being recorded. Use a separate set for
-    // suppressed callees, since we don't want a suppressed edge (within one
-    // RAII scope) to prevent an unsuppressed edge from being recorded. The
-    // seen array is indexed by a boolean 'suppressed' variable.
-    var seen = [ new Set(), new Set() ];
+    // repeated callgraph edges from being recorded. This uses a Map from
+    // callees to limit sets, because we don't want a limited edge to prevent
+    // an unlimited edge from being recorded later. (So an edge will be skipped
+    // if it exists and is at least as limited as the previously seen edge.)
+    //
+    // Limit sets are implemented as integers interpreted as bitfields.
+    //
+    var seen = new Map();
 
     lastline = null;
     for (var edge of body.PEdge) {
         if (edge.Kind != "Call")
             continue;
 
-        // Whether this call is within the RAII scope of a GC suppression class
-        var edgeSuppressed = (edge.Index[0] in body.suppressed);
+        // The limits (eg LIMIT_CANNOT_GC) are determined by whatever RAII
+        // scopes might be active, which have been computed previously for all
+        // points in the body.
+        var edgeLimited = body.limits[edge.Index[0]] | 0;
 
         for (var callee of getCallees(edge)) {
-            var suppressed = Boolean(edgeSuppressed || callee.suppressed);
-            var prologue = suppressed ? "SUPPRESS_GC " : "";
+            // Individual callees may have additional limits. The only such
+            // limit currently is that nsISupports.{AddRef,Release} are assumed
+            // to never GC.
+            const limits = edgeLimited | callee.limits;
+            let prologue = limits ? `/${limits} ` : "";
             prologue += memo(functionName) + " ";
             if (callee.kind == 'direct') {
-                if (!seen[+suppressed].has(callee.name)) {
-                    seen[+suppressed].add(callee.name);
+                const prev_limits = seen.has(callee.name) ? seen.get(callee.name) : LIMIT_UNVISITED;
+                if (prev_limits & ~limits) {
+                    // Only output an edge if it loosens a limit.
+                    seen.set(callee.name, prev_limits & limits);
                     printOnce("D " + prologue + memo(callee.name));
                 }
             } else if (callee.kind == 'field') {
                 var { csu, field, isVirtual } = callee;
                 const tag = isVirtual ? 'V' : 'F';
-                printOnce(tag + " " + prologue + "CLASS " + csu + " FIELD " + field);
+                printOnce(tag + " " + prologue + memo(`${csu}.${field}`) + " CLASS " + csu + " FIELD " + field);
             } else if (callee.kind == 'resolved-field') {
                 // Fully-resolved field (virtual method) call. Record the
-                // callgraph edges. Do not consider suppression, since it is
+                // callgraph edges. Do not consider limits, since they are
                 // local to this callsite and we are writing out a global
                 // record here.
                 //
                 // Any field call that does *not* have an R entry must be
                 // assumed to call anything.
                 var { csu, field, callees } = callee;
                 var fullFieldName = csu + "." + field;
                 if (!virtualResolutionsSeen.has(fullFieldName)) {
@@ -150,21 +168,22 @@ if (theFunctionNameToFind) {
         quit(1);
     }
     minStream = maxStream = index;
 }
 
 function process(functionName, functionBodies)
 {
     for (var body of functionBodies)
-        body.suppressed = [];
+        body.limits = [];
 
     for (var body of functionBodies) {
-        for (var [pbody, id] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isSuppressConstructor))
-            pbody.suppressed[id] = true;
+        for (var [pbody, id, limits] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor)) {
+            pbody.limits[id] = limits;
+        }
     }
 
     for (var body of functionBodies)
         processBody(functionName, body);
 
     // GCC generates multiple constructors and destructors ("in-charge" and
     // "not-in-charge") to handle virtual base classes. They are normally
     // identical, and it appears that GCC does some magic to alias them to the
--- a/js/src/devtools/rootAnalysis/computeGCFunctions.js
+++ b/js/src/devtools/rootAnalysis/computeGCFunctions.js
@@ -2,25 +2,25 @@
 
 "use strict";
 
 loadRelativeToScript('utility.js');
 loadRelativeToScript('annotations.js');
 loadRelativeToScript('loadCallgraph.js');
 
 if (typeof scriptArgs[0] != 'string')
-    throw "Usage: computeGCFunctions.js <callgraph.txt> <out:gcFunctions.txt> <out:gcFunctions.lst> <out:gcEdges.txt> <out:suppressedFunctions.lst>";
+    throw "Usage: computeGCFunctions.js <callgraph.txt> <out:gcFunctions.txt> <out:gcFunctions.lst> <out:gcEdges.txt> <out:limitedFunctions.lst>";
 
 var start = "Time: " + new Date;
 
 var callgraph_filename = scriptArgs[0];
 var gcFunctions_filename = scriptArgs[1] || "gcFunctions.txt";
 var gcFunctionsList_filename = scriptArgs[2] || "gcFunctions.lst";
 var gcEdges_filename = scriptArgs[3] || "gcEdges.txt";
-var suppressedFunctionsList_filename = scriptArgs[4] || "suppressedFunctions.lst";
+var limitedFunctionsList_filename = scriptArgs[4] || "limitedFunctions.lst";
 
 loadCallgraph(callgraph_filename);
 
 printErr("Writing " + gcFunctions_filename);
 redirect(gcFunctions_filename);
 
 for (var name in gcFunctions) {
     for (let readable of readableNames[name]) {
@@ -57,13 +57,12 @@ printErr("Writing " + gcEdges_filename);
 redirect(gcEdges_filename);
 for (var block in gcEdges) {
   for (var edge in gcEdges[block]) {
       var func = gcEdges[block][edge];
     print([ block, edge, func ].join(" || "));
   }
 }
 
-printErr("Writing " + suppressedFunctionsList_filename);
-redirect(suppressedFunctionsList_filename);
-for (var name in suppressedFunctions) {
-    print(name);
-}
+printErr("Writing " + limitedFunctionsList_filename);
+redirect(limitedFunctionsList_filename);
+for (const [name, limits] of Object.entries(limitedFunctions))
+    print(`${limits} ${name}`);
--- a/js/src/devtools/rootAnalysis/dumpCFG.js
+++ b/js/src/devtools/rootAnalysis/dumpCFG.js
@@ -102,17 +102,17 @@ function str_value(val, env, options) {
     const exp = str_value(Exp[0], env);
     if (options && options.noderef)
       return exp;
     return "*" + exp;
   } else if (Kind == 'Fld') {
     const {Exp, Field} = val;
     const name = Field.Name[0];
     if ("FieldInstanceFunction" in Field) {
-      return Field.FieldCSU.Type.Name + "::" + name;
+      return Field.FieldCSU.Type.Name + "." + name;
     }
     const container = str_value(Exp[0]);
     if (container.startsWith("*"))
       return container.substring(1) + "->" + name;
     return container + "." + name;
   } else if (Kind == 'Empty') {
     return '<unknown>';
   } else if (Kind == 'Binop') {
--- a/js/src/devtools/rootAnalysis/loadCallgraph.js
+++ b/js/src/devtools/rootAnalysis/loadCallgraph.js
@@ -24,181 +24,325 @@ loadRelativeToScript('utility.js');
 // Note that callgraph.txt uses a compressed representation -- each name is
 // mapped to an integer, and those integers are what is recorded in the edges.
 // But the integers depend on the full name, whereas the true edge should only
 // consider the mangled name. And some of the names encoded in callgraph.txt
 // are FieldCalls, not just function names.
 
 var readableNames = {}; // map from mangled name => list of readable names
 var mangledName = {}; // map from demangled names => mangled names. Could be eliminated.
-var calleeGraph = {}; // map from mangled => list of tuples of {'callee':mangled, 'suppressed':bool}
-var callerGraph = {}; // map from mangled => list of tuples of {'caller':mangled, 'suppressed':bool}
+var calleesOf = {}; // map from mangled => list of tuples of {'callee':mangled, 'limits':intset}
+var callersOf; // map from mangled => list of tuples of {'caller':mangled, 'limits':intset}
 var gcFunctions = {}; // map from mangled callee => reason
-var suppressedFunctions = {}; // set of mangled names (map from mangled name => true)
+var limitedFunctions = {}; // set of mangled names (map from mangled name => limit intset)
 var gcEdges = {};
 
-function addGCFunction(caller, reason)
+// Map from identifier to mangled name (or to a Class.Field)
+var idToMangled = [""];
+
+// Map from identifier to full "mangled$readable" name. Or sometimes to a
+// Class.Field name.
+var functionNames = [""];
+
+var mangledToId = {};
+
+// Returns whether the function was added. (It will be refused if it was
+// already there, or if limits or annotations say it shouldn't be added.)
+function addGCFunction(caller, reason, functionLimits)
 {
-    if (caller in suppressedFunctions)
+    if (functionLimits[caller] & LIMIT_CANNOT_GC)
         return false;
 
-    if (ignoreGCFunction(caller))
+    if (ignoreGCFunction(idToMangled[caller]))
         return false;
 
     if (!(caller in gcFunctions)) {
         gcFunctions[caller] = reason;
         return true;
     }
 
     return false;
 }
 
-function addCallEdge(caller, callee, suppressed)
-{
-    addToKeyedList(calleeGraph, caller, {callee:callee, suppressed:suppressed});
-    addToKeyedList(callerGraph, callee, {caller:caller, suppressed:suppressed});
-}
+// Every caller->callee callsite is associated with a limit saying what is
+// allowed at that callsite (eg if it's in a GC suppression zone, it would have
+// LIMIT_CANNOT_GC set.) A given caller might call the same callee multiple
+// times, with different limits, so we want to associate the <caller,callee>
+// edge with the intersection ('AND') of all of the callsites' limits.
+//
+// Scan through all call edges and intersect the limits for all matching
+// <caller,callee> edges (so that the result is the least limiting of all
+// matching edges.) Preserve the original order.
+//
+// During the same scan, build callersOf from calleesOf.
+function merge_repeated_calls(calleesOf) {
+    const callersOf = Object.create(null);
+
+    for (const [caller, callee_limits] of Object.entries(calleesOf)) {
+        const ordered_callees = [];
 
-// Map from identifier to full "mangled|readable" name. Or sometimes to a
-// Class.Field name.
-var functionNames = [""];
+        // callee_limits is a list of {callee,limit} objects.
+        const callee2limit = new Map();
+        for (const {callee, limits} of callee_limits) {
+            const prev_limits = callee2limit.get(callee);
+            if (prev_limits === undefined) {
+                callee2limit.set(callee, limits);
+                ordered_callees.push(callee);
+            } else {
+                callee2limit.set(callee, prev_limits & limits);
+            }
+        }
 
-// Map from identifier to mangled name (or to a Class.Field)
-var idToMangled = [""];
+        // Update the contents of callee_limits to contain a single entry for
+        // each callee, with its limits set to the AND of the limits observed
+        // at all callsites within this caller function.
+        callee_limits.length = 0;
+        for (const callee of ordered_callees) {
+            const limits = callee2limit.get(callee);
+            callee_limits.push({callee, limits});
+            if (!(callee in callersOf))
+                callersOf[callee] = [];
+            callersOf[callee].push({caller, limits});
+        }
+    }
+
+    return callersOf;
+}
 
 function loadCallgraph(file)
 {
-    var suppressedFieldCalls = {};
-    var resolvedFunctions = {};
+    const fieldCallLimits = {};
+    const fieldCallCSU = new Map(); // map from full field name id => csu name
+    const resolvedFieldCalls = new Set();
 
-    var numGCCalls = 0;
+    // set of mangled names (map from mangled name => limit intset)
+    var functionLimits = {};
 
-    for (var line of readFileLines_gen(file)) {
+    let numGCCalls = 0;
+
+    for (let line of readFileLines_gen(file)) {
         line = line.replace(/\n/, "");
 
-        var match;
+        let match;
         if (match = line.charAt(0) == "#" && /^\#(\d+) (.*)/.exec(line)) {
             assert(functionNames.length == match[1]);
             functionNames.push(match[2]);
-            var [ mangled, readable ] = splitFunction(match[2]);
+            const [ mangled, readable ] = splitFunction(match[2]);
             if (mangled in readableNames)
                 readableNames[mangled].push(readable);
             else
                 readableNames[mangled] = [ readable ];
             mangledName[readable] = mangled;
+            mangledToId[mangled] = idToMangled.length;
             idToMangled.push(mangled);
             continue;
         }
-        var suppressed = false;
-        if (line.indexOf("SUPPRESS_GC") != -1) {
-            match = /^(..)SUPPRESS_GC (.*)/.exec(line);
-            line = match[1] + match[2];
-            suppressed = true;
+        let limits = 0;
+        // Example line: D /17 6 7
+        //
+        // This means a direct call from 6 -> 7, but within a scope that
+        // applies limits 0x1 and 0x10 to the callee.
+        //
+        // Look for a limit and remove it from the line if found.
+        if (line.indexOf("/") != -1) {
+            match = /^(..)\/(\d+) (.*)/.exec(line);
+            line = match[1] + match[3];
+            limits = match[2]|0;
         }
-        var tag = line.charAt(0);
+        const tag = line.charAt(0);
         if (match = tag == 'I' && /^I (\d+) VARIABLE ([^\,]*)/.exec(line)) {
-            var mangledCaller = idToMangled[match[1]];
-            var name = match[2];
-            if (!indirectCallCannotGC(functionNames[match[1]], name) && !suppressed)
-                addGCFunction(mangledCaller, "IndirectCall: " + name);
-        } else if (match = (tag == 'F' || tag == 'V') && /^[FV] (\d+) CLASS (.*?) FIELD (.*)/.exec(line)) {
-            var caller = idToMangled[match[1]];
-            var csu = match[2];
-            var fullfield = csu + "." + match[3];
-            if (suppressed)
-                suppressedFieldCalls[fullfield] = true;
-            else if (!fieldCallCannotGC(csu, fullfield))
-                addGCFunction(caller, "FieldCall: " + fullfield);
+            const caller = match[1]|0;
+            const name = match[2];
+            if (!indirectCallCannotGC(functionNames[caller], name) &&
+                !(limits & LIMIT_CANNOT_GC))
+            {
+                addGCFunction(caller, "IndirectCall: " + name, functionLimits);
+            }
+        } else if (match = (tag == 'F' || tag == 'V') && /^[FV] (\d+) (\d+) CLASS (.*?) FIELD (.*)/.exec(line)) {
+            const caller = match[1]|0;
+            const fullfield = match[2]|0;
+            const csu = match[3];
+            const fullfield_str = csu + "." + match[4];
+            assert(functionNames[fullfield] == fullfield_str);
+            if (limits)
+                fieldCallLimits[fullfield] = limits;
+            addToKeyedList(calleesOf, caller, {callee:fullfield, limits});
+            fieldCallCSU.set(fullfield, csu);
         } else if (match = tag == 'D' && /^D (\d+) (\d+)/.exec(line)) {
-            var caller = idToMangled[match[1]];
-            var callee = idToMangled[match[2]];
-            addCallEdge(caller, callee, suppressed);
+            const caller = match[1]|0;
+            const callee = match[2]|0;
+            addToKeyedList(calleesOf, caller, {callee:callee, limits:limits});
         } else if (match = tag == 'R' && /^R (\d+) (\d+)/.exec(line)) {
-            var callerField = idToMangled[match[1]];
-            var callee = idToMangled[match[2]];
-            addCallEdge(callerField, callee, false);
-            resolvedFunctions[callerField] = true;
+            const callerField = match[1]|0;
+            const callee = match[2]|0;
+            // Resolved virtual functions create a dummy node for the field
+            // call, and callers call it. It will then call all possible
+            // instantiations. No additional limits are placed on the callees;
+            // it's as if there were a function named BaseClass.foo:
+            //
+            //     void BaseClass.foo() {
+            //         Subclass1::foo();
+            //         Subclass2::foo();
+            //     }
+            //
+            addToKeyedList(calleesOf, callerField, {callee:callee, limits:0});
+            // Mark that we resolved this virtual method, so that it isn't
+            // assumed to call some random function that might do anything.
+            resolvedFieldCalls.add(callerField);
         } else if (match = tag == 'T' && /^T (\d+) (.*)/.exec(line)) {
-            var mangled = idToMangled[match[1]];
-            var tag = match[2];
+            const mangled = match[1]|0;
+            let tag = match[2];
             if (tag == 'GC Call') {
-                addGCFunction(mangled, "GC");
+                addGCFunction(mangled, "GC", functionLimits);
                 numGCCalls++;
             }
+        } else {
+            assert(false, "Invalid format in callgraph line: " + line);
         }
     }
 
+    // Callers have a list of callees, with duplicates (if the same function is
+    // called more than once.) Merge the repeated calls, only keeping limits
+    // that are in force for *every* callsite of that callee. Also, generate
+    // the callersOf table at the same time.
+    callersOf = merge_repeated_calls(calleesOf);
+
     // Add in any extra functions at the end. (If we did this early, it would
     // mess up the id <-> name correspondence. Also, we need to know if the
     // functions even exist in the first place.)
     for (var func of extraGCFunctions()) {
-        addGCFunction(func, "annotation");
+        addGCFunction(mangledToId[func], "annotation", functionLimits);
     }
 
-    // Initialize suppressedFunctions to the set of all functions, and the
-    // worklist to all toplevel callers.
+    // Compute functionLimits: it should contain the set of functions that
+    // are *always* called within some sort of limited context (eg GC
+    // suppression).
+
+    // Initialize functionLimits to the set of all functions, where each one
+    // is maximally limited, and initialize the worklist to all toplevel
+    // callers with dummy unlimited edges.
     var worklist = [];
-    for (var callee in callerGraph)
-        suppressedFunctions[callee] = true;
-    for (var caller in calleeGraph) {
-        if (!(caller in callerGraph)) {
-            suppressedFunctions[caller] = true;
-            worklist.push(caller);
+    for (let callee in callersOf)
+        functionLimits[callee] = LIMIT_UNVISITED;
+    for (var caller in calleesOf) {
+        if (!(caller in callersOf)) {
+            functionLimits[caller] = LIMIT_UNVISITED;
+            worklist.push([caller, LIMIT_NONE, 'root']);
         }
     }
 
-    // Find all functions reachable via an unsuppressed call chain, and remove
-    // them from the suppressedFunctions set. Everything remaining is only
-    // reachable when GC is suppressed.
-    var top = worklist.length;
-    while (top > 0) {
-        name = worklist[--top];
-        if (!(name in suppressedFunctions))
-            continue;
-        delete suppressedFunctions[name];
-        if (!(name in calleeGraph))
-            continue;
-        for (var entry of calleeGraph[name]) {
-            if (!entry.suppressed)
-                worklist[top++] = entry.callee;
+    // Add in limited field calls.
+    for (var [name, limits] of Object.entries(fieldCallLimits)) {
+        if (limits)
+            functionLimits[name] = limits;
+    }
+
+    // Recursively traverse the callgraph from the roots. Recurse through every
+    // edge that weakens the limits. (Limits that entirely disappear, aka go to
+    // a zero intset, will be removed from functionLimits.)
+    while (worklist.length > 0) {
+        // Consider caller where (graph) -> caller -> (0 or more callees)
+        // 'callercaller' is for debugging.
+        const [caller, edge_limits, callercaller] = worklist.pop();
+        const prev_limits = functionLimits[caller];
+        if (prev_limits & ~edge_limits) {
+            // Turning off a limit (or unvisited marker). Must recurse to the
+            // children. But first, update this caller's limits: we just found
+            // out it is reachable by an unlimited path, so it must be treated
+            // as unlimited (with respect to that bit).
+            const new_limits = prev_limits & edge_limits;
+            if (new_limits)
+                functionLimits[caller] = new_limits;
+            else
+                delete functionLimits[caller];
+            for (const {callee, limits} of (calleesOf[caller] || []))
+                worklist.push([callee, limits | edge_limits, caller]);
         }
     }
 
-    // Such functions are known to not GC.
+    // Not all functions are reachable by searching backwards from GC
+    // functions, so some functions will be LIMIT_UNVISITED. Remove them.
+    for (var func in functionLimits) {
+        if (functionLimits[func] == LIMIT_UNVISITED)
+            delete functionLimits[func];
+    }
+
+    // Eliminate GC-limited functions from the set of functions known to GC.
     for (var name in gcFunctions) {
-        if (name in suppressedFunctions)
+        if (functionLimits[name] & LIMIT_CANNOT_GC)
             delete gcFunctions[name];
     }
 
-    for (var name in suppressedFieldCalls) {
-        suppressedFunctions[name] = true;
-    }
+    // functionLimits should now contain all functions that are always called
+    // in a limited context.
 
     // Sanity check to make sure the callgraph has some functions annotated as
     // GC Calls. This is mostly a check to be sure the earlier processing
     // succeeded (as opposed to, say, running on empty xdb files because you
     // didn't actually compile anything interesting.)
     assert(numGCCalls > 0, "No GC functions found!");
 
     // Initialize the worklist to all known gcFunctions.
     var worklist = [];
-    for (var name in gcFunctions)
+    for (const name in gcFunctions)
         worklist.push(name);
 
-    // Recursively find all callers and add them to the set of gcFunctions.
+    // Include all field calls and unresolved virtual method calls.
+    for (const [name, csuName] of fieldCallCSU) {
+        if (resolvedFieldCalls.has(name))
+            continue; // Skip resolved virtual functions.
+        const fullFieldName = idToMangled[name];
+        if (!fieldCallCannotGC(csuName, fullFieldName)) {
+            gcFunctions[name] = 'unresolved ' + idToMangled[name];
+            worklist.push(name);
+        }
+    }
+
+    // Recursively find all callers not always called in a GC suppression
+    // context, and add them to the set of gcFunctions.
     while (worklist.length) {
         name = worklist.shift();
         assert(name in gcFunctions);
-        if (!(name in callerGraph))
+        if (!(name in callersOf))
             continue;
-        for (var entry of callerGraph[name]) {
-            if (!entry.suppressed && addGCFunction(entry.caller, name))
-                worklist.push(entry.caller);
+        for (const {caller, limits} of callersOf[name]) {
+            if (!(limits & LIMIT_CANNOT_GC)) {
+                if (addGCFunction(caller, name, functionLimits))
+                    worklist.push(caller);
+            }
         }
     }
 
-    // Any field call that has been resolved to all possible callees can be
-    // trusted to not GC if all of those callees are known to not GC.
-    for (var name in resolvedFunctions) {
-        if (!(name in gcFunctions))
-            suppressedFunctions[name] = true;
+    // Convert functionLimits to limitedFunctions (using mangled names instead
+    // of ids.)
+
+    for (const [id, limits] of Object.entries(functionLimits))
+        limitedFunctions[idToMangled[id]] = limits;
+
+    // The above code uses integer ids for efficiency. External code uses
+    // mangled names. Rewrite the various data structures to convert ids to
+    // mangled names.
+    remap_ids_to_mangled_names();
+}
+
+function remap_ids_to_mangled_names() {
+    var tmp = gcFunctions;
+    gcFunctions = {};
+    for (const [caller, reason] of Object.entries(tmp))
+        gcFunctions[idToMangled[caller]] = idToMangled[reason] || reason;
+
+    tmp = calleesOf;
+    calleesOf = {};
+    for (const [callerId, callees] of Object.entries(calleesOf)) {
+        const caller = idToMangled[callerId];
+        for (const {calleeId, limits} of callees)
+            calleesOf[caller][idToMangled[calleeId]] = limits;
+    }
+
+    tmp = callersOf;
+    callersOf = {};
+    for (const [calleeId, callers] of Object.entries(callersOf)) {
+        const callee = idToMangled[calleeId];
+        callersOf[callee] = {};
+        for (const {callerId, limits} of callers)
+            callersOf[callee][idToMangled[caller]] = limits;
     }
 }
--- a/js/src/devtools/rootAnalysis/utility.js
+++ b/js/src/devtools/rootAnalysis/utility.js
@@ -1,12 +1,28 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 
 "use strict";
 
+loadRelativeToScript('dumpCFG.js');
+
+// Limit inset bits - each call edge may carry a set of 'limit' bits, saying eg
+// that the edge takes place within a scope where GC is suppressed, for
+// example.
+var LIMIT_NONE = 0;
+var LIMIT_CANNOT_GC = 1;
+var LIMIT_ALL = 1;
+
+// The traversal algorithms we run will recurse into children if you change any
+// limit bit to zero. Use all bits set to maximally limited, including
+// additional bits that all just mean "unvisited", so that the first time we
+// see a node with this limit, we're guaranteed to turn at least one bit off
+// and thereby keep going.
+var LIMIT_UNVISITED = 0xffff;
+
 // gcc appends this to mangled function names for "not in charge"
 // constructors/destructors.
 var internalMarker = " *INTERNAL* ";
 
 if (! Set.prototype.hasOwnProperty("update")) {
     Object.defineProperty(Set.prototype, "update", {
         value: function (collection) {
             for (let elt of collection)