Bug 1354232 - Copy LSANLeaks to mozleak, r=ahal
authorJames Graham <james@hoppipolla.co.uk>
Wed, 16 May 2018 14:24:48 +0100
changeset 482439 05441f0a29b3e1de138727c3e41b00b01bf3d0e5
parent 482438 76540384ad3d56ccc16f62181321ae4efe98b7e0
child 482440 6eda80370f59eb70e4f3d64f8552d894e0313f13
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersahal
bugs1354232
milestone63.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 1354232 - Copy LSANLeaks to mozleak, r=ahal This is a copy for now rather than a move because followup patches are going to convert the LSAN support to use mozlog and I don't want to risk breaking mochitest by accident. MozReview-Commit-ID: I6NVgjDjsX2
testing/mozbase/mozleak/mozleak/__init__.py
testing/mozbase/mozleak/mozleak/lsan.py
--- a/testing/mozbase/mozleak/mozleak/__init__.py
+++ b/testing/mozbase/mozleak/mozleak/__init__.py
@@ -4,10 +4,11 @@
 
 """
 mozleak is a library for extracting memory leaks from leak logs files.
 """
 
 from __future__ import absolute_import
 
 from .leaklog import process_leak_log
+from .lsan import LSANLeaks
 
-__all__ = ['process_leak_log']
+__all__ = ['process_leak_log', 'LSANLeaks']
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozleak/mozleak/lsan.py
@@ -0,0 +1,132 @@
+import re
+
+
+class LSANLeaks(object):
+
+    """
+    Parses the log when running an LSAN build, looking for interesting stack frames
+    in allocation stacks, and prints out reports.
+    """
+
+    def __init__(self, logger):
+        self.logger = logger
+        self.inReport = False
+        self.fatalError = False
+        self.symbolizerError = False
+        self.foundFrames = set([])
+        self.recordMoreFrames = None
+        self.currStack = None
+        self.allowedMatch = None
+        self.maxNumRecordedFrames = 4
+
+        # Don't various allocation-related stack frames, as they do not help much to
+        # distinguish different leaks.
+        unescapedSkipList = [
+            "malloc", "js_malloc", "malloc_", "__interceptor_malloc", "moz_xmalloc",
+            "calloc", "js_calloc", "calloc_", "__interceptor_calloc", "moz_xcalloc",
+            "realloc", "js_realloc", "realloc_", "__interceptor_realloc", "moz_xrealloc",
+            "new",
+            "js::MallocProvider",
+        ]
+        self.skipListRegExp = re.compile(
+            "^" + "|".join([re.escape(f) for f in unescapedSkipList]) + "$")
+
+        self.startRegExp = re.compile(
+            "==\d+==ERROR: LeakSanitizer: detected memory leaks")
+        self.fatalErrorRegExp = re.compile(
+            "==\d+==LeakSanitizer has encountered a fatal error.")
+        self.symbolizerOomRegExp = re.compile(
+            "LLVMSymbolizer: error reading file: Cannot allocate memory")
+        self.stackFrameRegExp = re.compile("    #\d+ 0x[0-9a-f]+ in ([^(</]+)")
+        self.sysLibStackFrameRegExp = re.compile(
+            "    #\d+ 0x[0-9a-f]+ \(([^+]+)\+0x[0-9a-f]+\)")
+
+    def log(self, line):
+        if re.match(self.startRegExp, line):
+            self.inReport = True
+            return
+
+        if re.match(self.fatalErrorRegExp, line):
+            self.fatalError = True
+            return
+
+        if re.match(self.symbolizerOomRegExp, line):
+            self.symbolizerError = True
+            return
+
+        if not self.inReport:
+            return
+
+        if line.startswith("Direct leak") or line.startswith("Indirect leak"):
+            self._finishStack()
+            self.recordMoreFrames = True
+            self.currStack = []
+            return
+
+        if line.startswith("SUMMARY: AddressSanitizer"):
+            self._finishStack()
+            self.inReport = False
+            return
+
+        if not self.recordMoreFrames:
+            return
+
+        stackFrame = re.match(self.stackFrameRegExp, line)
+        if stackFrame:
+            # Split the frame to remove any return types.
+            frame = stackFrame.group(1).split()[-1]
+            if not re.match(self.skipListRegExp, frame):
+                self._recordFrame(frame)
+            return
+
+        sysLibStackFrame = re.match(self.sysLibStackFrameRegExp, line)
+        if sysLibStackFrame:
+            # System library stack frames will never match the skip list,
+            # so don't bother checking if they do.
+            self._recordFrame(sysLibStackFrame.group(1))
+
+        # If we don't match either of these, just ignore the frame.
+        # We'll end up with "unknown stack" if everything is ignored.
+
+    def process(self):
+        failures = 0
+
+        if self.fatalError:
+            self.logger.error("TEST-UNEXPECTED-FAIL | LeakSanitizer | LeakSanitizer "
+                              "has encountered a fatal error.")
+            failures += 1
+
+        if self.symbolizerError:
+            self.logger.error("TEST-UNEXPECTED-FAIL | LeakSanitizer | LLVMSymbolizer "
+                              "was unable to allocate memory.")
+            failures += 1
+            self.logger.info("TEST-INFO | LeakSanitizer | This will cause leaks that "
+                             "should be ignored to instead be reported as an error")
+
+        if self.foundFrames:
+            self.logger.info("TEST-INFO | LeakSanitizer | To show the "
+                             "addresses of leaked objects add report_objects=1 to LSAN_OPTIONS")
+            self.logger.info("TEST-INFO | LeakSanitizer | This can be done "
+                             "in testing/mozbase/mozrunner/mozrunner/utils.py")
+
+        for f in self.foundFrames:
+            self.logger.error(
+                "TEST-UNEXPECTED-FAIL | LeakSanitizer | leak at " + f)
+            failures += 1
+
+        return failures
+
+    def _finishStack(self):
+        if self.recordMoreFrames and len(self.currStack) == 0:
+            self.currStack = ["unknown stack"]
+        if self.currStack:
+            self.foundFrames.add(", ".join(self.currStack))
+            self.currStack = None
+        self.recordMoreFrames = False
+        self.numRecordedFrames = 0
+
+    def _recordFrame(self, frame):
+        self.currStack.append(frame)
+        self.numRecordedFrames += 1
+        if self.numRecordedFrames >= self.maxNumRecordedFrames:
+            self.recordMoreFrames = False