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 441534 f6173c4ae224f1a7df7a23195003fe312331e85d
parent 441533 77ada590e04777dd99d3632cdcd2ed34ea5d4268
child 441535 c45f9de01d9e2d65c740f7386b5bf1f76e8bcf64
push id34867
push usershindli@mozilla.com
push dateWed, 17 Oct 2018 00:55:53 +0000
treeherdermozilla-central@778427bb6353 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjonco
bugs1479868
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 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