Bug 1479868 - Add tests for virtual function resolution and field calls, r=jonco
authorSteve Fink <sfink@mozilla.com>
Mon, 16 Jul 2018 17:04:01 -0700
changeset 489825 f6173c4ae224f1a7df7a23195003fe312331e85d
parent 489824 77ada590e04777dd99d3632cdcd2ed34ea5d4268
child 489826 c45f9de01d9e2d65c740f7386b5bf1f76e8bcf64
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersjonco
bugs1479868
milestone64.0a1
Bug 1479868 - Add tests for virtual function resolution and field calls, r=jonco
js/src/devtools/rootAnalysis/run-test.py
js/src/devtools/rootAnalysis/t/testlib.py
js/src/devtools/rootAnalysis/t/virtual/source.cpp
js/src/devtools/rootAnalysis/t/virtual/test.py
--- a/js/src/devtools/rootAnalysis/run-test.py
+++ b/js/src/devtools/rootAnalysis/run-test.py
@@ -38,17 +38,22 @@ parser.add_argument(
     help='Path to gcc')
 parser.add_argument(
     '--cxx', default=os.environ.get('CXX'),
     help='Path to g++')
 parser.add_argument(
     '--verbose', '-v', action='store_true',
     help='Display verbose output, including commands executed')
 parser.add_argument(
-    'tests', nargs='*', default=['sixgill-tree', 'suppression', 'hazards', 'exceptions'],
+    'tests', nargs='*', default=[
+        'sixgill-tree',
+        'suppression',
+        'hazards',
+        'exceptions',
+        'virtual'],
     help='tests to run')
 
 cfg = parser.parse_args()
 
 if not cfg.js:
     exit('Must specify JS binary through environment variable or --js option')
 if not cfg.cc:
     if cfg.gccdir:
--- a/js/src/devtools/rootAnalysis/t/testlib.py
+++ b/js/src/devtools/rootAnalysis/t/testlib.py
@@ -3,18 +3,31 @@ import os
 import re
 import subprocess
 
 from sixgill import Body
 from collections import defaultdict, namedtuple
 
 scriptdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
 
-HazardSummary = namedtuple(
-    'HazardSummary', ['function', 'variable', 'type', 'GCFunction', 'location'])
+HazardSummary = namedtuple('HazardSummary', [
+    'function',
+    'variable',
+    'type',
+    'GCFunction',
+    'location'])
+
+Callgraph = namedtuple('Callgraph', [
+    'functionNames',
+    'nameToId',
+    'calleesOf',
+    'callersOf',
+    'tags',
+    'calleeGraph',
+    'callerGraph'])
 
 
 def equal(got, expected):
     if got != expected:
         print("Got '%s', expected '%s'" % (got, expected))
 
 
 def extract_unmangled(func):
@@ -85,37 +98,89 @@ sixgill_bin = '{bindir}'
         self.run_analysis_script("gcTypes")
 
     def load_text_file(self, filename, extract=lambda l: l):
         fullpath = os.path.join(self.outdir, filename)
         values = (extract(line.strip()) for line in open(fullpath, "r"))
         return list(filter(lambda _: _ is not None, values))
 
     def load_suppressed_functions(self):
-        return set(self.load_text_file("suppressedFunctions.lst"))
+        return set(self.load_text_file("limitedFunctions.lst", extract=lambda l: l.split(' ')[1]))
 
     def load_gcTypes(self):
         def grab_type(line):
             m = re.match(r'^(GC\w+): (.*)', line)
             if m:
                 return (m.group(1) + 's', m.group(2))
             return None
 
         gctypes = defaultdict(list)
         for collection, typename in self.load_text_file('gcTypes.txt', extract=grab_type):
             gctypes[collection].append(typename)
         return gctypes
 
     def load_gcFunctions(self):
         return self.load_text_file('gcFunctions.lst', extract=extract_unmangled)
 
+    def load_callgraph(self):
+        data = Callgraph(
+            functionNames=['dummy'],
+            nameToId={},
+            calleesOf=defaultdict(list),
+            callersOf=defaultdict(list),
+            tags=defaultdict(set),
+            calleeGraph=defaultdict(dict),
+            callerGraph=defaultdict(dict),
+        )
+
+        def lookup(id):
+            return data.functionNames[int(id)]
+
+        def add_call(caller, callee, limit):
+            data.calleesOf[caller].append(callee)
+            data.callersOf[callee].append(caller)
+            data.calleeGraph[caller][callee] = True
+            data.callerGraph[callee][caller] = True
+
+        def process(line):
+            if line.startswith('#'):
+                name = line.split(" ", 1)[1]
+                if '$' in name:
+                    name = name[name.index('$') + 1:]
+                data.nameToId[name] = len(data.functionNames)
+                data.functionNames.append(name)
+                return
+
+            limit = 0
+            m = re.match(r'^\w (?:/(\d+))? ', line)
+            if m:
+                limit = int(m[1])
+
+            tokens = line.split(' ')
+            if tokens[0] in ('D', 'R'):
+                _, caller, callee = tokens
+                add_call(lookup(caller), lookup(callee), limit)
+            elif tokens[0] == 'T':
+                data.tags[tokens[1]].add(line.split(' ', 2)[2])
+            elif tokens[0] in ('F', 'V'):
+                m = re.match(r'^[FV] (\d+) (\d+) CLASS (.*?) FIELD (.*)', line)
+                caller, callee, csu, field = m.groups()
+                add_call(lookup(caller), lookup(callee), limit)
+
+            elif tokens[0] == 'I':
+                m = re.match(r'^I (\d+) VARIABLE ([^\,]*)', line)
+                pass
+
+        self.load_text_file('callgraph.txt', extract=process)
+        return data
+
     def load_hazards(self):
         def grab_hazard(line):
             m = re.match(
-                r"Function '(.*?)' has unrooted '(.*?)' of type '(.*?)' live across GC call '(.*?)' at (.*)", line)  # NOQA: E501
+                r"Function '(.*?)' has unrooted '(.*?)' of type '(.*?)' live across GC call '?(.*?)'? at (.*)", line)  # NOQA: E501
             if m:
                 info = list(m.groups())
                 info[0] = info[0].split("$")[-1]
                 info[3] = info[3].split("$")[-1]
                 return HazardSummary(*info)
             return None
 
         return self.load_text_file('rootingHazards.txt', extract=grab_hazard)
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
@@ -0,0 +1,84 @@
+#define ANNOTATE(property) __attribute__((tag(property)))
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC()
+{
+    // If the implementation is too trivial, the function body won't be emitted at all.
+    asm("");
+}
+
+struct Cell { int f; } ANNOTATE("GC Thing");
+
+extern void foo();
+
+typedef void (*func_t)();
+
+class Base {
+  public:
+    int dummy;
+    virtual void someGC() = 0;
+    func_t functionField;
+};
+
+class Super : public Base {
+  public:
+    virtual void noneGC() = 0;
+    virtual void allGC() = 0;
+};
+
+void bar() {
+    GC();
+}
+
+class Sub1 : public Super {
+  public:
+    void noneGC() override { foo(); }
+    void someGC() override { foo(); }
+    void allGC() override { foo(); bar(); }
+};
+
+class Sub2 : public Super {
+  public:
+    void noneGC() override { foo(); }
+    void someGC() override { foo(); bar(); }
+    void allGC() override { foo(); bar(); }
+};
+
+class Sibling : public Base {
+  public:
+    virtual void noneGC() { foo(); }
+    void someGC() override { foo(); bar(); }
+    virtual void allGC() { foo(); bar(); }
+};
+
+class AutoSuppressGC {
+  public:
+    AutoSuppressGC() {}
+    ~AutoSuppressGC() {}
+} ANNOTATE("Suppress GC");
+
+void use(Cell*) {
+    asm("");
+}
+
+void f() {
+    Sub1 s1;
+    Sub2 s2;
+
+    Cell cell;
+    { Cell* c1 = &cell; s1.noneGC(); use(c1); }
+    { Cell* c2 = &cell; s2.someGC(); use(c2); }
+    { Cell* c3 = &cell; s1.allGC(); use(c3); }
+    { Cell* c4 = &cell; s2.noneGC(); use(c4); }
+    { Cell* c5 = &cell; s2.someGC(); use(c5); }
+    { Cell* c6 = &cell; s2.allGC(); use(c6); }
+
+    Super* super = &s2;
+    { Cell* c7 = &cell; super->noneGC(); use(c7); }
+    { Cell* c8 = &cell; super->someGC(); use(c8); }
+    { Cell* c9 = &cell; super->allGC(); use(c9); }
+
+    { Cell* c10 = &cell; s1.functionField(); use(c10); }
+    { Cell* c11 = &cell; super->functionField(); use(c11); }
+}
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/test.py
@@ -0,0 +1,38 @@
+test.compile("source.cpp")
+test.run_analysis_script('gcTypes')
+
+# The suppressions file uses only mangled names since it's for internal use,
+# though I may change that soon given (1) the unfortunate non-uniqueness of
+# mangled constructor names, and (2) the usefulness of this file for
+# mrgiggles's reporting.
+suppressed = test.load_suppressed_functions()
+
+# gcFunctions should be the inverse, but we get to rely on unmangled names here.
+gcFunctions = test.load_gcFunctions()
+
+assert 'void Sub1::noneGC()' not in gcFunctions
+assert 'void Sub1::someGC()' not in gcFunctions
+assert 'void Sub1::allGC()' in gcFunctions
+assert 'void Sub2::noneGC()' not in gcFunctions
+assert 'void Sub2::someGC()' in gcFunctions
+assert 'void Sub2::allGC()' in gcFunctions
+
+callgraph = test.load_callgraph()
+assert callgraph.calleeGraph['void f()']['Super.noneGC']
+assert callgraph.calleeGraph['Super.noneGC']['void Sub1::noneGC()']
+assert callgraph.calleeGraph['Super.noneGC']['void Sub2::noneGC()']
+assert 'void Sibling::noneGC()' not in callgraph.calleeGraph['Super.noneGC']
+
+hazards = test.load_hazards()
+hazmap = {haz.variable: haz for haz in hazards}
+assert 'c1' not in hazmap
+assert 'c2' in hazmap
+assert 'c3' in hazmap
+assert 'c4' not in hazmap
+assert 'c5' in hazmap
+assert 'c6' in hazmap
+assert 'c7' not in hazmap
+assert 'c8' in hazmap
+assert 'c9' in hazmap
+assert 'c10' in hazmap
+assert 'c11' in hazmap