Commit 72477d86 authored by timurrrr@chromium.org's avatar timurrrr@chromium.org

Avoid duplicate error reports / suppressions when UI test reports are analyzed separately

Review URL: http://codereview.chromium.org/3056025

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@54118 0039d316-1c4b-4281-b951-d872f2087c98
parent b0db42f4
...@@ -24,16 +24,6 @@ import common ...@@ -24,16 +24,6 @@ import common
# Global symbol table (yuck) # Global symbol table (yuck)
TheAddressTable = None TheAddressTable = None
# Contains the time when the we started analyzing the first log file.
# This variable is used to skip incomplete logs after some timeout.
# TODO(timurrrr): Currently, this needs to be a global variable
# because analyzer can be called multiple times (e.g. ui_tests)
# unless we re-factor the analyze system to avoid creating multiple analyzers.
AnalyzeStartTime = None
# Max time to wait for memcheck logs to complete.
LOG_COMPLETION_TIMEOUT = 180.0
# These are functions (using C++ mangled names) that we look for in stack # These are functions (using C++ mangled names) that we look for in stack
# traces. We don't show stack frames while pretty printing when they are below # traces. We don't show stack frames while pretty printing when they are below
# any of the following: # any of the following:
...@@ -219,10 +209,11 @@ class ValgrindError: ...@@ -219,10 +209,11 @@ class ValgrindError:
def __str__(self): def __str__(self):
''' Pretty print the type and backtrace(s) of this specific error, ''' Pretty print the type and backtrace(s) of this specific error,
including suppression (which is just a mangled backtrace).''' including suppression (which is just a mangled backtrace).'''
output = self._kind + "\n" output = ""
if (self._commandline): if (self._commandline):
output += self._commandline + "\n" output += self._commandline + "\n"
output += self._kind + "\n"
for backtrace in self._backtraces: for backtrace in self._backtraces:
output += backtrace[0] + "\n" output += backtrace[0] + "\n"
filter = subprocess.Popen("c++filt -n", stdin=subprocess.PIPE, filter = subprocess.Popen("c++filt -n", stdin=subprocess.PIPE,
...@@ -255,37 +246,32 @@ class ValgrindError: ...@@ -255,37 +246,32 @@ class ValgrindError:
output += " (" + frame[OBJECT_FILE] + ")" output += " (" + frame[OBJECT_FILE] + ")"
output += "\n" output += "\n"
# TODO(dank): stop synthesizing suppressions once everyone has assert self._suppression != None, "Your Valgrind doesn't generate " \
# valgrind-3.5 and we can rely on xml "suppressions - is it too old?"
if (self._suppression == None):
output += "Suppression:\n" output += "Suppression (error hash=#%X#):" % self.__hash__()
for frame in backtrace[1]: # Widen suppression slightly to make portable between mac and linux
output += " fun:" + (frame[FUNCTION_NAME] or "*") + "\n" supp = self._suppression;
supp = supp.replace("fun:_Znwj", "fun:_Znw*")
if (self._suppression != None): supp = supp.replace("fun:_Znwm", "fun:_Znw*")
output += "Suppression:" # Split into lines so we can enforce length limits
# Widen suppression slightly to make portable between mac and linux supplines = supp.split("\n")
supp = self._suppression;
supp = supp.replace("fun:_Znwj", "fun:_Znw*") # Truncate at line 26 (VG_MAX_SUPP_CALLERS plus 2 for name and type)
supp = supp.replace("fun:_Znwm", "fun:_Znw*") # or at the first 'boring' caller.
# Split into lines so we can enforce length limits # (https://bugs.kde.org/show_bug.cgi?id=199468 proposes raising
supplines = supp.split("\n") # VG_MAX_SUPP_CALLERS, but we're probably fine with it as is.)
# TODO(dkegel): add more boring callers
# Truncate at line 26 (VG_MAX_SUPP_CALLERS plus 2 for name and type) newlen = 26;
# or at the first 'boring' caller. try:
# (https://bugs.kde.org/show_bug.cgi?id=199468 proposes raising newlen = min(newlen, supplines.index(" fun:_ZN11MessageLoop3RunEv"))
# VG_MAX_SUPP_CALLERS, but we're probably fine with it as is.) except ValueError:
# TODO(dkegel): add more boring callers pass
newlen = 26; if (len(supplines) > newlen):
try: supplines = supplines[0:newlen]
newlen = min(newlen, supplines.index(" fun:_ZN11MessageLoop3RunEv")) supplines.append("}")
except ValueError:
pass output += "\n".join(supplines) + "\n"
if (len(supplines) > newlen):
supplines = supplines[0:newlen]
supplines.append("}")
output += "\n".join(supplines) + "\n"
return output return output
...@@ -320,7 +306,7 @@ def find_and_truncate(f): ...@@ -320,7 +306,7 @@ def find_and_truncate(f):
f.truncate() f.truncate()
return True return True
class MemcheckAnalyze: class MemcheckAnalyzer:
''' Given a set of Valgrind XML files, parse all the errors out of them, ''' Given a set of Valgrind XML files, parse all the errors out of them,
unique them and output the results.''' unique them and output the results.'''
...@@ -354,15 +340,37 @@ class MemcheckAnalyze: ...@@ -354,15 +340,37 @@ class MemcheckAnalyze:
"bug_49253 Memcheck sanity test 08 (new/write left) or Memcheck sanity test 09 (new/write right) on Mac.": 2, "bug_49253 Memcheck sanity test 08 (new/write left) or Memcheck sanity test 09 (new/write right) on Mac.": 2,
} }
def __init__(self, source_dir, files, show_all_leaks=False, use_gdb=False): # Max time to wait for memcheck logs to complete.
'''Reads in a set of files. LOG_COMPLETION_TIMEOUT = 180.0
def __init__(self, source_dir, show_all_leaks=False, use_gdb=False):
'''Create a parser for Memcheck logs.
Args: Args:
source_dir: Path to top of source tree for this build source_dir: Path to top of source tree for this build
files: A list of filenames. show_all_leaks: Whether to show even less important leaks
show_all_leaks: whether to show even less important leaks use_gdb: Whether to use gdb to resolve source filenames and line numbers
in the report stacktraces
''' '''
self._source_dir = source_dir
self._show_all_leaks = show_all_leaks
self._use_gdb = use_gdb
# Contains the set of unique errors
self._errors = set()
# Contains the time when the we started analyzing the first log file.
# This variable is used to skip incomplete logs after some timeout.
self._analyze_start_time = None
def Report(self, files, check_sanity=False):
'''Reads in a set of files and prints Memcheck report.
Args:
files: A list of filenames.
check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS
'''
# Beyond the detailed errors parsed by ValgrindError above, # Beyond the detailed errors parsed by ValgrindError above,
# the xml file contain records describing suppressions that were used: # the xml file contain records describing suppressions that were used:
# <suppcounts> # <suppcounts>
...@@ -384,16 +392,19 @@ class MemcheckAnalyze: ...@@ -384,16 +392,19 @@ class MemcheckAnalyze:
# into the process. # into the process.
global TheAddressTable global TheAddressTable
if use_gdb: if self._use_gdb:
TheAddressTable = gdb_helper.AddressTable() TheAddressTable = gdb_helper.AddressTable()
self._errors = set() else:
self._suppcounts = {} TheAddressTable = None
cur_report_errors = set()
suppcounts = {}
badfiles = set() badfiles = set()
global AnalyzeStartTime if self._analyze_start_time == None:
if AnalyzeStartTime == None: self._analyze_start_time = time.time()
AnalyzeStartTime = time.time() start_time = self._analyze_start_time
self._parse_failed = False
parse_failed = False
for file in files: for file in files:
# Wait up to three minutes for valgrind to finish writing all files, # Wait up to three minutes for valgrind to finish writing all files,
# but after that, just skip incomplete files and warn. # but after that, just skip incomplete files and warn.
...@@ -407,7 +418,7 @@ class MemcheckAnalyze: ...@@ -407,7 +418,7 @@ class MemcheckAnalyze:
origsize = os.path.getsize(file) origsize = os.path.getsize(file)
while (running and not found and while (running and not found and
(firstrun or (firstrun or
((time.time() - AnalyzeStartTime) < LOG_COMPLETION_TIMEOUT))): ((time.time() - start_time) < self.LOG_COMPLETION_TIMEOUT))):
firstrun = False firstrun = False
f.seek(0) f.seek(0)
if pid: if pid:
...@@ -435,7 +446,7 @@ class MemcheckAnalyze: ...@@ -435,7 +446,7 @@ class MemcheckAnalyze:
try: try:
parsed_file = parse(file); parsed_file = parse(file);
except ExpatError, e: except ExpatError, e:
self._parse_failed = True parse_failed = True
logging.warn("could not parse %s: %s" % (file, e)) logging.warn("could not parse %s: %s" % (file, e))
lineno = e.lineno - 1 lineno = e.lineno - 1
context_lines = 5 context_lines = 5
...@@ -471,10 +482,20 @@ class MemcheckAnalyze: ...@@ -471,10 +482,20 @@ class MemcheckAnalyze:
raw_errors = parsed_file.getElementsByTagName("error") raw_errors = parsed_file.getElementsByTagName("error")
for raw_error in raw_errors: for raw_error in raw_errors:
# Ignore "possible" leaks for now by default. # Ignore "possible" leaks for now by default.
if (show_all_leaks or if (self._show_all_leaks or
getTextOf(raw_error, "kind") != "Leak_PossiblyLost"): getTextOf(raw_error, "kind") != "Leak_PossiblyLost"):
error = ValgrindError(source_dir, raw_error, commandline) error = ValgrindError(self._source_dir, raw_error, commandline)
self._errors.add(error) if error not in cur_report_errors:
# We haven't seen such errors doing this report yet...
if error in self._errors:
# ... but we saw it in earlier reports, e.g. previous UI test
cur_report_errors.add("This error was already printed "
"in some other test, see 'hash=#%X#'" % \
error.__hash__())
else:
# ... and we haven't seen it in other tests as well
self._errors.add(error)
cur_report_errors.add(error)
suppcountlist = parsed_file.getElementsByTagName("suppcounts") suppcountlist = parsed_file.getElementsByTagName("suppcounts")
if len(suppcountlist) > 0: if len(suppcountlist) > 0:
...@@ -482,10 +503,10 @@ class MemcheckAnalyze: ...@@ -482,10 +503,10 @@ class MemcheckAnalyze:
for node in suppcountlist.getElementsByTagName("pair"): for node in suppcountlist.getElementsByTagName("pair"):
count = getTextOf(node, "count"); count = getTextOf(node, "count");
name = getTextOf(node, "name"); name = getTextOf(node, "name");
if name in self._suppcounts: if name in suppcounts:
self._suppcounts[name] += int(count) suppcounts[name] += int(count)
else: else:
self._suppcounts[name] = int(count) suppcounts[name] = int(count)
if len(badfiles) > 0: if len(badfiles) > 0:
logging.warn("valgrind didn't finish writing %d files?!" % len(badfiles)) logging.warn("valgrind didn't finish writing %d files?!" % len(badfiles))
...@@ -493,8 +514,7 @@ class MemcheckAnalyze: ...@@ -493,8 +514,7 @@ class MemcheckAnalyze:
logging.warn("Last 20 lines of %s :" % file) logging.warn("Last 20 lines of %s :" % file)
os.system("tail -n 20 '%s' 1>&2" % file) os.system("tail -n 20 '%s' 1>&2" % file)
def Report(self, check_sanity=False): if parse_failed:
if self._parse_failed:
logging.error("FAIL! Couldn't parse Valgrind output file") logging.error("FAIL! Couldn't parse Valgrind output file")
return -2 return -2
...@@ -504,15 +524,15 @@ class MemcheckAnalyze: ...@@ -504,15 +524,15 @@ class MemcheckAnalyze:
print " count name" print " count name"
if common.IsLinux(): if common.IsLinux():
remaining_sanity_supp = MemcheckAnalyze.SANITY_TEST_SUPPRESSIONS_LINUX remaining_sanity_supp = MemcheckAnalyzer.SANITY_TEST_SUPPRESSIONS_LINUX
elif common.IsMac(): elif common.IsMac():
remaining_sanity_supp = MemcheckAnalyze.SANITY_TEST_SUPPRESSIONS_MAC remaining_sanity_supp = MemcheckAnalyzer.SANITY_TEST_SUPPRESSIONS_MAC
else: else:
remaining_sanity_supp = {} remaining_sanity_supp = {}
if check_sanity: if check_sanity:
logging.warn("No sanity test list for platform %s", sys.platform) logging.warn("No sanity test list for platform %s", sys.platform)
for (name, count) in sorted(self._suppcounts.items(), for (name, count) in sorted(suppcounts.items(),
key=lambda (k,v): (v,k)): key=lambda (k,v): (v,k)):
print "%7d %s" % (count, name) print "%7d %s" % (count, name)
if name in remaining_sanity_supp and remaining_sanity_supp[name] == count: if name in remaining_sanity_supp and remaining_sanity_supp[name] == count:
...@@ -526,11 +546,10 @@ class MemcheckAnalyze: ...@@ -526,11 +546,10 @@ class MemcheckAnalyze:
if self._errors: if self._errors:
logging.error("FAIL! There were %s errors: " % len(self._errors)) logging.error("FAIL! There were %s errors: " % len(self._errors))
global TheAddressTable
if TheAddressTable != None: if TheAddressTable != None:
TheAddressTable.ResolveAll() TheAddressTable.ResolveAll()
for error in self._errors: for error in cur_report_errors:
logging.error(error) logging.error(error)
retcode = -1 retcode = -1
...@@ -551,7 +570,7 @@ class MemcheckAnalyze: ...@@ -551,7 +570,7 @@ class MemcheckAnalyze:
return 0 return 0
def _main(): def _main():
'''For testing only. The MemcheckAnalyze class should be imported instead.''' '''For testing only. The MemcheckAnalyzer class should be imported instead.'''
retcode = 0 retcode = 0
parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") parser = optparse.OptionParser("usage: %prog [options] <files to analyze>")
parser.add_option("", "--source_dir", parser.add_option("", "--source_dir",
...@@ -563,8 +582,8 @@ def _main(): ...@@ -563,8 +582,8 @@ def _main():
parser.error("no filename specified") parser.error("no filename specified")
filenames = args filenames = args
analyzer = MemcheckAnalyze(options.source_dir, filenames, use_gdb=True) analyzer = MemcheckAnalyzer(options.source_dir, use_gdb=True)
retcode = analyzer.Report() retcode = analyzer.Report(filenames)
sys.exit(retcode) sys.exit(retcode)
......
...@@ -33,7 +33,7 @@ class _StackTraceLine(object): ...@@ -33,7 +33,7 @@ class _StackTraceLine(object):
else: else:
return self.raw_line_.replace(self.binary, '%s:%s' % (file, line)) return self.raw_line_.replace(self.binary, '%s:%s' % (file, line))
class TsanAnalyze: class TsanAnalyzer:
''' Given a set of ThreadSanitizer output files, parse all the errors out of ''' Given a set of ThreadSanitizer output files, parse all the errors out of
them, unique them and output the results.''' them, unique them and output the results.'''
...@@ -48,37 +48,27 @@ class TsanAnalyze: ...@@ -48,37 +48,27 @@ class TsanAnalyze:
"(has been created by T.* at this point|is program's main thread)") "(has been created by T.* at this point|is program's main thread)")
SANITY_TEST_SUPPRESSION = "ThreadSanitizer sanity test" SANITY_TEST_SUPPRESSION = "ThreadSanitizer sanity test"
def __init__(self, source_dir, files, use_gdb=False): def __init__(self, source_dir, use_gdb=False):
'''Reads in a set of files. '''Reads in a set of files.
Args: Args:
source_dir: Path to top of source tree for this build source_dir: Path to top of source tree for this build
files: A list of filenames.
''' '''
self.use_gdb = use_gdb self._use_gdb = use_gdb
if use_gdb:
global TheAddressTable
TheAddressTable = gdb_helper.AddressTable()
self.races = []
self.used_suppressions = {}
for file in files:
self.ParseReportFile(file)
if self.use_gdb:
TheAddressTable.ResolveAll()
def ReadLine(self): def ReadLine(self):
self.line_ = self.cur_fd_.readline() self.line_ = self.cur_fd_.readline()
self.stack_trace_line_ = None self.stack_trace_line_ = None
if not self.use_gdb: if not self._use_gdb:
return return
global TheAddressTable global TheAddressTable
match = TsanAnalyze.LOAD_LIB_RE.match(self.line_) match = TsanAnalyzer.LOAD_LIB_RE.match(self.line_)
if match: if match:
binary, ip = match.groups() binary, ip = match.groups()
TheAddressTable.AddBinaryAt(binary, ip) TheAddressTable.AddBinaryAt(binary, ip)
return return
match = TsanAnalyze.TSAN_LINE_RE.match(self.line_) match = TsanAnalyzer.TSAN_LINE_RE.match(self.line_)
if match: if match:
address, binary_name = match.groups() address, binary_name = match.groups()
stack_trace_line = _StackTraceLine(self.line_, address, binary_name) stack_trace_line = _StackTraceLine(self.line_, address, binary_name)
...@@ -106,7 +96,7 @@ class TsanAnalyze: ...@@ -106,7 +96,7 @@ class TsanAnalyze:
break break
tmp = [] tmp = []
while re.search(TsanAnalyze.THREAD_CREATION_STR, self.line_): while re.search(TsanAnalyzer.THREAD_CREATION_STR, self.line_):
tmp.extend(self.ReadSection()) tmp.extend(self.ReadSection())
self.ReadLine() self.ReadLine()
if re.search("Possible data race", self.line_): if re.search("Possible data race", self.line_):
...@@ -123,14 +113,29 @@ class TsanAnalyze: ...@@ -123,14 +113,29 @@ class TsanAnalyze:
self.used_suppressions[supp_name] = count self.used_suppressions[supp_name] = count
self.cur_fd_.close() self.cur_fd_.close()
def Report(self, check_sanity=False): def Report(self, files, check_sanity=False):
'''TODO!!!
files: A list of filenames.
'''
global TheAddressTable
if self._use_gdb:
TheAddressTable = gdb_helper.AddressTable()
else:
TheAddressTable = None
self.races = []
self.used_suppressions = {}
for file in files:
self.ParseReportFile(file)
if self._use_gdb:
TheAddressTable.ResolveAll()
is_sane = False is_sane = False
print "-----------------------------------------------------" print "-----------------------------------------------------"
print "Suppressions used:" print "Suppressions used:"
print " count name" print " count name"
for item in sorted(self.used_suppressions.items(), key=lambda (k,v): (v,k)): for item in sorted(self.used_suppressions.items(), key=lambda (k,v): (v,k)):
print "%7s %s" % (item[1], item[0]) print "%7s %s" % (item[1], item[0])
if item[0].startswith(TsanAnalyze.SANITY_TEST_SUPPRESSION): if item[0].startswith(TsanAnalyzer.SANITY_TEST_SUPPRESSION):
is_sane = True is_sane = True
print "-----------------------------------------------------" print "-----------------------------------------------------"
sys.stdout.flush() sys.stdout.flush()
...@@ -156,7 +161,7 @@ class TsanAnalyze: ...@@ -156,7 +161,7 @@ class TsanAnalyze:
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
'''For testing only. The TsanAnalyze class should be imported instead.''' '''For testing only. The TsanAnalyzer class should be imported instead.'''
retcode = 0 retcode = 0
parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") parser = optparse.OptionParser("usage: %prog [options] <files to analyze>")
parser.add_option("", "--source_dir", parser.add_option("", "--source_dir",
...@@ -168,7 +173,7 @@ if __name__ == '__main__': ...@@ -168,7 +173,7 @@ if __name__ == '__main__':
parser.error("no filename specified") parser.error("no filename specified")
filenames = args filenames = args
analyzer = TsanAnalyze(options.source_dir, filenames, use_gdb=True) analyzer = TsanAnalyzer(options.source_dir, use_gdb=True)
retcode = analyzer.Report() retcode = analyzer.Report(filenames)
sys.exit(retcode) sys.exit(retcode)
...@@ -437,9 +437,10 @@ class ValgrindTool(BaseTool): ...@@ -437,9 +437,10 @@ class ValgrindTool(BaseTool):
ppids = set([int(f.split(".")[-2]) \ ppids = set([int(f.split(".")[-2]) \
for f in filenames if re.search("\.[0-9]+\.[0-9]+$", f)]) for f in filenames if re.search("\.[0-9]+\.[0-9]+$", f)])
analyzer = self.CreateAnalyzer()
if len(ppids) == 0: if len(ppids) == 0:
# Fast path - no browser wrapper was set. # Fast path - no browser wrapper was set.
return self.CreateAnalyzer(filenames).Report(check_sanity) return analyzer.Report(filenames, check_sanity)
ret = 0 ret = 0
for ppid in ppids: for ppid in ppids:
...@@ -453,7 +454,7 @@ class ValgrindTool(BaseTool): ...@@ -453,7 +454,7 @@ class ValgrindTool(BaseTool):
if re.search("\.%d\.[0-9]+$" % ppid, f)] if re.search("\.%d\.[0-9]+$" % ppid, f)]
# check_sanity won't work with browser wrappers # check_sanity won't work with browser wrappers
assert check_sanity == False assert check_sanity == False
ret |= self.CreateAnalyzer(ppid_filenames).Report() ret |= analyzer.Report(ppid_filenames)
print "=====================================================" print "====================================================="
sys.stdout.flush() sys.stdout.flush()
...@@ -502,9 +503,9 @@ class Memcheck(ValgrindTool): ...@@ -502,9 +503,9 @@ class Memcheck(ValgrindTool):
return ret return ret
def CreateAnalyzer(self, filenames): def CreateAnalyzer(self):
use_gdb = common.IsMac() use_gdb = common.IsMac()
return memcheck_analyze.MemcheckAnalyze(self._source_dir, filenames, return memcheck_analyze.MemcheckAnalyzer(self._source_dir,
self._options.show_all_leaks, self._options.show_all_leaks,
use_gdb=use_gdb) use_gdb=use_gdb)
...@@ -656,9 +657,9 @@ class ThreadSanitizerPosix(ThreadSanitizerBase, ValgrindTool): ...@@ -656,9 +657,9 @@ class ThreadSanitizerPosix(ThreadSanitizerBase, ValgrindTool):
proc += ["-v"] proc += ["-v"]
return proc return proc
def CreateAnalyzer(self, filenames): def CreateAnalyzer(self):
use_gdb = common.IsMac() use_gdb = common.IsMac()
return tsan_analyze.TsanAnalyze(self._source_dir, filenames) return tsan_analyze.TsanAnalyzer(self._source_dir)
def Analyze(self, check_sanity=False): def Analyze(self, check_sanity=False):
ret = self.GetAnalyzeResults(check_sanity) ret = self.GetAnalyzeResults(check_sanity)
...@@ -702,8 +703,8 @@ class ThreadSanitizerWindows(ThreadSanitizerBase, PinTool): ...@@ -702,8 +703,8 @@ class ThreadSanitizerWindows(ThreadSanitizerBase, PinTool):
def Analyze(self, check_sanity=False): def Analyze(self, check_sanity=False):
filenames = glob.glob(self.TMP_DIR + "/tsan.*") filenames = glob.glob(self.TMP_DIR + "/tsan.*")
analyzer = tsan_analyze.TsanAnalyze(self._source_dir, filenames) analyzer = tsan_analyze.TsanAnalyzer(self._source_dir)
ret = analyzer.Report(check_sanity) ret = analyzer.Report(filenames, check_sanity)
if ret != 0: if ret != 0:
logging.info(self.INFO_MESSAGE) logging.info(self.INFO_MESSAGE)
return ret return ret
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment