| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 # tsan_analyze.py | 6 # tsan_analyze.py |
| 7 | 7 |
| 8 ''' Given a ThreadSanitizer output file, parses errors and uniques them.''' | 8 ''' Given a ThreadSanitizer output file, parses errors and uniques them.''' |
| 9 | 9 |
| 10 import gdb_helper | 10 import gdb_helper |
| 11 | 11 |
| 12 import common | 12 from collections import defaultdict |
| 13 import hashlib | 13 import hashlib |
| 14 import logging | 14 import logging |
| 15 import optparse | 15 import optparse |
| 16 import os | 16 import os |
| 17 import re | 17 import re |
| 18 import subprocess | 18 import subprocess |
| 19 import sys | 19 import sys |
| 20 import time | 20 import time |
| 21 | 21 |
| 22 import common |
| 23 |
| 22 # Global symbol table (ugh) | 24 # Global symbol table (ugh) |
| 23 TheAddressTable = None | 25 TheAddressTable = None |
| 24 | 26 |
| 25 class _StackTraceLine(object): | 27 class _StackTraceLine(object): |
| 26 def __init__(self, line, address, binary): | 28 def __init__(self, line, address, binary): |
| 27 self.raw_line_ = line | 29 self.raw_line_ = line |
| 28 self.address = address | 30 self.address = address |
| 29 self.binary = binary | 31 self.binary = binary |
| 30 def __str__(self): | 32 def __str__(self): |
| 31 global TheAddressTable | 33 global TheAddressTable |
| 32 file, line = TheAddressTable.GetFileLine(self.binary, self.address) | 34 file, line = TheAddressTable.GetFileLine(self.binary, self.address) |
| 33 if (file is None) or (line is None): | 35 if (file is None) or (line is None): |
| 34 return self.raw_line_ | 36 return self.raw_line_ |
| 35 else: | 37 else: |
| 36 return self.raw_line_.replace(self.binary, '%s:%s' % (file, line)) | 38 return self.raw_line_.replace(self.binary, '%s:%s' % (file, line)) |
| 37 | 39 |
| 38 class TsanAnalyzer(object): | 40 class TsanAnalyzer(object): |
| 39 ''' Given a set of ThreadSanitizer output files, parse all the errors out of | 41 ''' Given a set of ThreadSanitizer output files, parse all the errors out of |
| 40 them, unique them and output the results.''' | 42 them, unique them and output the results.''' |
| 41 | 43 |
| 42 LOAD_LIB_RE = re.compile('--[0-9]+-- ([^(:]*) \((0x[0-9a-f]+)\)') | 44 LOAD_LIB_RE = re.compile('--[0-9]+-- ([^(:]*) \((0x[0-9a-f]+)\)') |
| 43 TSAN_LINE_RE = re.compile('==[0-9]+==\s*[#0-9]+\s*' | 45 TSAN_LINE_RE = re.compile('==[0-9]+==\s*[#0-9]+\s*' |
| 44 '([0-9A-Fa-fx]+):' | 46 '([0-9A-Fa-fx]+):' |
| 45 '(?:[^ ]* )*' | 47 '(?:[^ ]* )*' |
| 46 '([^ :\n]+)' | 48 '([^ :\n]+)' |
| 47 '') | 49 '') |
| 48 THREAD_CREATION_STR = ("INFO: T.* " | 50 THREAD_CREATION_STR = ("INFO: T.* " |
| 49 "(has been created by T.* at this point|is program's main thread)") | 51 "(has been created by T.* at this point|is program's main thread)") |
| 50 | 52 |
| 51 SANITY_TEST_SUPPRESSION = "ThreadSanitizer sanity test" | 53 SANITY_TEST_SUPPRESSION = ("ThreadSanitizer sanity test " |
| 54 "(ToolsSanityTest.DataRace)") |
| 52 TSAN_RACE_DESCRIPTION = "Possible data race" | 55 TSAN_RACE_DESCRIPTION = "Possible data race" |
| 53 TSAN_WARNING_DESCRIPTION = ("Unlocking a non-locked lock" | 56 TSAN_WARNING_DESCRIPTION = ("Unlocking a non-locked lock" |
| 54 "|accessing an invalid lock" | 57 "|accessing an invalid lock" |
| 55 "|which did not acquire this lock") | 58 "|which did not acquire this lock") |
| 56 RACE_VERIFIER_LINE = "Confirmed a race|unexpected race" | 59 RACE_VERIFIER_LINE = "Confirmed a race|unexpected race" |
| 57 TSAN_ASSERTION = "Assertion failed: " | 60 TSAN_ASSERTION = "Assertion failed: " |
| 58 | 61 |
| 59 def __init__(self, source_dir, use_gdb=False): | 62 def __init__(self, source_dir, use_gdb=False): |
| 60 '''Reads in a set of files. | 63 '''Reads in a set of files. |
| 61 | 64 |
| (...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 179 tmp = [] | 182 tmp = [] |
| 180 if re.search(TsanAnalyzer.TSAN_ASSERTION, self.line_): | 183 if re.search(TsanAnalyzer.TSAN_ASSERTION, self.line_): |
| 181 tmp.extend(self.ReadTillTheEnd()) | 184 tmp.extend(self.ReadTillTheEnd()) |
| 182 ret.append(tmp) | 185 ret.append(tmp) |
| 183 break | 186 break |
| 184 | 187 |
| 185 match = re.search("used_suppression:\s+([0-9]+)\s(.*)", self.line_) | 188 match = re.search("used_suppression:\s+([0-9]+)\s(.*)", self.line_) |
| 186 if match: | 189 if match: |
| 187 count, supp_name = match.groups() | 190 count, supp_name = match.groups() |
| 188 count = int(count) | 191 count = int(count) |
| 189 if supp_name in self.used_suppressions: | 192 self.used_suppressions[supp_name] += count |
| 190 self.used_suppressions[supp_name] += count | |
| 191 else: | |
| 192 self.used_suppressions[supp_name] = count | |
| 193 self.cur_fd_.close() | 193 self.cur_fd_.close() |
| 194 return ret | 194 return ret |
| 195 | 195 |
| 196 def GetReports(self, files): | 196 def GetReports(self, files): |
| 197 '''Extracts reports from a set of files. | 197 '''Extracts reports from a set of files. |
| 198 | 198 |
| 199 Reads a set of files and returns a list of all discovered | 199 Reads a set of files and returns a list of all discovered |
| 200 ThreadSanitizer race reports. As a side effect, populates | 200 ThreadSanitizer race reports. As a side effect, populates |
| 201 self.used_suppressions with appropriate info. | 201 self.used_suppressions with appropriate info. |
| 202 ''' | 202 ''' |
| 203 | 203 |
| 204 global TheAddressTable | 204 global TheAddressTable |
| 205 if self._use_gdb: | 205 if self._use_gdb: |
| 206 TheAddressTable = gdb_helper.AddressTable() | 206 TheAddressTable = gdb_helper.AddressTable() |
| 207 else: | 207 else: |
| 208 TheAddressTable = None | 208 TheAddressTable = None |
| 209 reports = [] | 209 reports = [] |
| 210 self.used_suppressions = {} | 210 self.used_suppressions = defaultdict(int) |
| 211 for file in files: | 211 for file in files: |
| 212 reports.extend(self.ParseReportFile(file)) | 212 reports.extend(self.ParseReportFile(file)) |
| 213 if self._use_gdb: | 213 if self._use_gdb: |
| 214 TheAddressTable.ResolveAll() | 214 TheAddressTable.ResolveAll() |
| 215 # Make each line of each report a string. | 215 # Make each line of each report a string. |
| 216 reports = map(lambda(x): map(str, x), reports) | 216 reports = map(lambda(x): map(str, x), reports) |
| 217 return [''.join(report_lines) for report_lines in reports] | 217 return [''.join(report_lines) for report_lines in reports] |
| 218 | 218 |
| 219 def Report(self, files, testcase, check_sanity=False): | 219 def Report(self, files, testcase, check_sanity=False): |
| 220 '''Reads in a set of files and prints ThreadSanitizer report. | 220 '''Reads in a set of files and prints ThreadSanitizer report. |
| 221 | 221 |
| 222 Args: | 222 Args: |
| 223 files: A list of filenames. | 223 files: A list of filenames. |
| 224 check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS | 224 check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS |
| 225 ''' | 225 ''' |
| 226 | 226 |
| 227 # We set up _cur_testcase class-wide variable to avoid passing it through | 227 # We set up _cur_testcase class-wide variable to avoid passing it through |
| 228 # about 5 functions. | 228 # about 5 functions. |
| 229 self._cur_testcase = testcase | 229 self._cur_testcase = testcase |
| 230 reports = self.GetReports(files) | 230 reports = self.GetReports(files) |
| 231 self._cur_testcase = None # just in case, shouldn't be used anymore | 231 self._cur_testcase = None # just in case, shouldn't be used anymore |
| 232 | 232 |
| 233 is_sane = False | 233 common.PrintUsedSuppressionsList(self.used_suppressions) |
| 234 print "-----------------------------------------------------" | 234 |
| 235 print "Suppressions used:" | |
| 236 print " count name" | |
| 237 for item in sorted(self.used_suppressions.items(), key=lambda (k,v): (v,k)): | |
| 238 print "%7s %s" % (item[1], item[0]) | |
| 239 if item[0].startswith(TsanAnalyzer.SANITY_TEST_SUPPRESSION): | |
| 240 is_sane = True | |
| 241 print "-----------------------------------------------------" | |
| 242 sys.stdout.flush() | |
| 243 | 235 |
| 244 retcode = 0 | 236 retcode = 0 |
| 245 if reports: | 237 if reports: |
| 246 logging.error("FAIL! Found %i report(s)" % len(reports)) | 238 logging.error("FAIL! Found %i report(s)" % len(reports)) |
| 247 for report in reports: | 239 for report in reports: |
| 248 logging.error('\n' + report) | 240 logging.error('\n' + report) |
| 249 retcode = -1 | 241 retcode = -1 |
| 250 | 242 |
| 251 # Report tool's insanity even if there were errors. | 243 # Report tool's insanity even if there were errors. |
| 252 if check_sanity and not is_sane: | 244 if (check_sanity and |
| 245 TsanAnalyzer.SANITY_TEST_SUPPRESSION not in self.used_suppressions): |
| 253 logging.error("FAIL! Sanity check failed!") | 246 logging.error("FAIL! Sanity check failed!") |
| 254 retcode = -3 | 247 retcode = -3 |
| 255 | 248 |
| 256 if retcode != 0: | 249 if retcode != 0: |
| 257 return retcode | 250 return retcode |
| 251 |
| 258 logging.info("PASS: No reports found") | 252 logging.info("PASS: No reports found") |
| 259 return 0 | 253 return 0 |
| 260 | 254 |
| 261 if __name__ == '__main__': | 255 if __name__ == '__main__': |
| 262 '''For testing only. The TsanAnalyzer class should be imported instead.''' | 256 '''For testing only. The TsanAnalyzer class should be imported instead.''' |
| 263 retcode = 0 | 257 retcode = 0 |
| 264 parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") | 258 parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") |
| 265 parser.add_option("", "--source_dir", | 259 parser.add_option("", "--source_dir", |
| 266 help="path to top of source tree for this build" | 260 help="path to top of source tree for this build" |
| 267 "(used to normalize source paths in baseline)") | 261 "(used to normalize source paths in baseline)") |
| 268 | 262 |
| 269 (options, args) = parser.parse_args() | 263 (options, args) = parser.parse_args() |
| 270 if not args: | 264 if not args: |
| 271 parser.error("no filename specified") | 265 parser.error("no filename specified") |
| 272 filenames = args | 266 filenames = args |
| 273 | 267 |
| 274 analyzer = TsanAnalyzer(options.source_dir, use_gdb=True) | 268 analyzer = TsanAnalyzer(options.source_dir, use_gdb=True) |
| 275 retcode = analyzer.Report(filenames, None) | 269 retcode = analyzer.Report(filenames, None) |
| 276 | 270 |
| 277 sys.exit(retcode) | 271 sys.exit(retcode) |
| OLD | NEW |