Bug 1479828 - Expand suppression to general limits, r=jonco
authorSteve Fink <sfink@mozilla.com>
Tue, 26 Sep 2017 21:03:09 -0700
changeset 489824 77ada590e04777dd99d3632cdcd2ed34ea5d4268
parent 489823 8a07eed99ca8315b5b275eec185561172f9e3ffe
child 489825 f6173c4ae224f1a7df7a23195003fe312331e85d
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersjonco
bugs1479828
milestone64.0a1
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)