| 1 |
#!/usr/bin/python |
| 2 |
# Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. |
| 3 |
# Use of this source code is governed by a BSD-style license that can be |
| 4 |
# found in the LICENSE file. |
| 5 |
|
| 6 |
# tsan_analyze.py |
| 7 |
|
| 8 |
''' Given a ThreadSanitizer output file, parses errors and uniques them.''' |
| 9 |
|
| 10 |
import gdb_helper |
| 11 |
|
| 12 |
import common |
| 13 |
import logging |
| 14 |
import optparse |
| 15 |
import os |
| 16 |
import re |
| 17 |
import subprocess |
| 18 |
import sys |
| 19 |
import time |
| 20 |
|
| 21 |
# Global symbol table (ugh) |
| 22 |
TheAddressTable = None |
| 23 |
|
| 24 |
class _StackTraceLine(object): |
| 25 |
def __init__(self, line, address, binary): |
| 26 |
self.raw_line_ = line |
| 27 |
self.address = address |
| 28 |
self.binary = binary |
| 29 |
def __str__(self): |
| 30 |
global TheAddressTable |
| 31 |
file, line = TheAddressTable.GetFileLine(self.binary, self.address) |
| 32 |
if (file is None) or (line is None): |
| 33 |
return self.raw_line_ |
| 34 |
else: |
| 35 |
return self.raw_line_.replace(self.binary, '%s:%s' % (file, line)) |
| 36 |
|
| 37 |
class TsanAnalyzer: |
| 38 |
''' Given a set of ThreadSanitizer output files, parse all the errors out of |
| 39 |
them, unique them and output the results.''' |
| 40 |
|
| 41 |
LOAD_LIB_RE = re.compile('--[0-9]+-- ([^(:]*) \((0x[0-9a-f]+)\)') |
| 42 |
TSAN_LINE_RE = re.compile('==[0-9]+==\s*[#0-9]+\s*' |
| 43 |
'([0-9A-Fa-fx]+):' |
| 44 |
'(?:[^ ]* )*' |
| 45 |
'([^ :\n]+)' |
| 46 |
'') |
| 47 |
TSAN_WARNING_LINE_RE = re.compile('==[0-9]+==\s*[#0-9]+\s*' |
| 48 |
'(?:[^ ]* )*' |
| 49 |
'([^ :\n]+)') |
| 50 |
|
| 51 |
THREAD_CREATION_STR = ("INFO: T.* " |
| 52 |
"(has been created by T.* at this point|is program's main thread)") |
| 53 |
|
| 54 |
SANITY_TEST_SUPPRESSION = "ThreadSanitizer sanity test" |
| 55 |
TSAN_RACE_DESCRIPTION = "Possible data race" |
| 56 |
TSAN_WARNING_DESCRIPTION = ("Unlocking a non-locked lock" |
| 57 |
"|accessing an invalid lock" |
| 58 |
"|which did not acquire this lock") |
| 59 |
def __init__(self, source_dir, use_gdb=False): |
| 60 |
'''Reads in a set of files. |
| 61 |
|
| 62 |
Args: |
| 63 |
source_dir: Path to top of source tree for this build |
| 64 |
''' |
| 65 |
|
| 66 |
self._use_gdb = use_gdb |
| 67 |
|
| 68 |
def ReadLine(self): |
| 69 |
self.line_ = self.cur_fd_.readline() |
| 70 |
self.stack_trace_line_ = None |
| 71 |
if not self._use_gdb: |
| 72 |
return |
| 73 |
global TheAddressTable |
| 74 |
match = TsanAnalyzer.LOAD_LIB_RE.match(self.line_) |
| 75 |
if match: |
| 76 |
binary, ip = match.groups() |
| 77 |
TheAddressTable.AddBinaryAt(binary, ip) |
| 78 |
return |
| 79 |
match = TsanAnalyzer.TSAN_LINE_RE.match(self.line_) |
| 80 |
if match: |
| 81 |
address, binary_name = match.groups() |
| 82 |
stack_trace_line = _StackTraceLine(self.line_, address, binary_name) |
| 83 |
TheAddressTable.Add(stack_trace_line.binary, stack_trace_line.address) |
| 84 |
self.stack_trace_line_ = stack_trace_line |
| 85 |
|
| 86 |
def ReadSection(self): |
| 87 |
result = [self.line_] |
| 88 |
if re.search("{{{", self.line_): |
| 89 |
while not re.search('}}}', self.line_): |
| 90 |
self.ReadLine() |
| 91 |
if self.stack_trace_line_ is None: |
| 92 |
result.append(self.line_) |
| 93 |
else: |
| 94 |
result.append(self.stack_trace_line_) |
| 95 |
return result |
| 96 |
|
| 97 |
def ParseReportFile(self, filename): |
| 98 |
self.cur_fd_ = open(filename, 'r') |
| 99 |
|
| 100 |
while True: |
| 101 |
# Read ThreadSanitizer reports. |
| 102 |
self.ReadLine() |
| 103 |
if (self.line_ == ''): |
| 104 |
break |
| 105 |
|
| 106 |
tmp = [] |
| 107 |
while re.search(TsanAnalyzer.THREAD_CREATION_STR, self.line_): |
| 108 |
tmp.extend(self.ReadSection()) |
| 109 |
self.ReadLine() |
| 110 |
if re.search(TsanAnalyzer.TSAN_RACE_DESCRIPTION, self.line_): |
| 111 |
tmp.extend(self.ReadSection()) |
| 112 |
self.reports.append(tmp) |
| 113 |
if (re.search(TsanAnalyzer.TSAN_WARNING_DESCRIPTION, self.line_) and |
| 114 |
not common.IsWindows()): # workaround for http://crbug.com/53198 |
| 115 |
tmp.extend(self.ReadSection()) |
| 116 |
self.reports.append(tmp) |
| 117 |
|
| 118 |
match = re.search(" used_suppression:\s+([0-9]+)\s(.*)", self.line_) |
| 119 |
if match: |
| 120 |
count, supp_name = match.groups() |
| 121 |
count = int(count) |
| 122 |
if supp_name in self.used_suppressions: |
| 123 |
self.used_suppressions[supp_name] += count |
| 124 |
else: |
| 125 |
self.used_suppressions[supp_name] = count |
| 126 |
self.cur_fd_.close() |
| 127 |
|
| 128 |
def Report(self, files, check_sanity=False): |
| 129 |
'''Reads in a set of files and prints ThreadSanitizer report. |
| 130 |
|
| 131 |
Args: |
| 132 |
files: A list of filenames. |
| 133 |
check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS |
| 134 |
''' |
| 135 |
|
| 136 |
global TheAddressTable |
| 137 |
if self._use_gdb: |
| 138 |
TheAddressTable = gdb_helper.AddressTable() |
| 139 |
else: |
| 140 |
TheAddressTable = None |
| 141 |
self.reports = [] |
| 142 |
self.used_suppressions = {} |
| 143 |
for file in files: |
| 144 |
self.ParseReportFile(file) |
| 145 |
if self._use_gdb: |
| 146 |
TheAddressTable.ResolveAll() |
| 147 |
|
| 148 |
is_sane = False |
| 149 |
print "-----------------------------------------------------" |
| 150 |
print "Suppressions used:" |
| 151 |
print " count name" |
| 152 |
for item in sorted(self.used_suppressions.items(), key=lambda (k,v): (v,k)): |
| 153 |
print "%7s %s" % (item[1], item[0]) |
| 154 |
if item[0].startswith(TsanAnalyzer.SANITY_TEST_SUPPRESSION): |
| 155 |
is_sane = True |
| 156 |
print "-----------------------------------------------------" |
| 157 |
sys.stdout.flush() |
| 158 |
|
| 159 |
retcode = 0 |
| 160 |
if len(self.reports) > 0: |
| 161 |
logging.error("FAIL! Found %i reports" % len(self.reports)) |
| 162 |
for report_list in self.reports: |
| 163 |
report = '' |
| 164 |
for line in report_list: |
| 165 |
report += str(line) |
| 166 |
logging.error('\n' + report) |
| 167 |
retcode = -1 |
| 168 |
|
| 169 |
# Report tool's insanity even if there were errors. |
| 170 |
if check_sanity and not is_sane: |
| 171 |
logging.error("FAIL! Sanity check failed!") |
| 172 |
retcode = -3 |
| 173 |
|
| 174 |
if retcode != 0: |
| 175 |
return retcode |
| 176 |
logging.info("PASS: No reports found") |
| 177 |
return 0 |
| 178 |
|
| 179 |
if __name__ == '__main__': |
| 180 |
'''For testing only. The TsanAnalyzer class should be imported instead.''' |
| 181 |
retcode = 0 |
| 182 |
parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") |
| 183 |
parser.add_option("", "--source_dir", |
| 184 |
help="path to top of source tree for this build" |
| 185 |
"(used to normalize source paths in baseline)") |
| 186 |
|
| 187 |
(options, args) = parser.parse_args() |
| 188 |
if len(args) == 0: |
| 189 |
parser.error("no filename specified") |
| 190 |
filenames = args |
| 191 |
|
| 192 |
analyzer = TsanAnalyzer(options.source_dir, use_gdb=True) |
| 193 |
retcode = analyzer.Report(filenames) |
| 194 |
|
| 195 |
sys.exit(retcode) |