1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Wrapper for running the test under heapchecker and analyzing the output.""" 6 7 import datetime 8 import logging 9 import os 10 import re 11 12 import common 13 import path_utils 14 import suppressions 15 16 17 class HeapcheckWrapper(object): 18 TMP_FILE = 'heapcheck.log' 19 SANITY_TEST_SUPPRESSION = "Heapcheck sanity test" 20 LEAK_REPORT_RE = re.compile( 21 'Leak of ([0-9]*) bytes in ([0-9]*) objects allocated from:') 22 # Workaround for http://crbug.com/132867, see below. 23 HOOKED_ALLOCATOR_RE = re.compile( 24 'Hooked allocator frame not found, returning empty trace') 25 STACK_LINE_RE = re.compile('\s*@\s*(?:0x)?[0-9a-fA-F]+\s*([^\n]*)') 26 BORING_CALLERS = common.BoringCallers(mangled=False, use_re_wildcards=True) 27 28 def __init__(self, supp_files): 29 self._mode = 'strict' 30 self._timeout = 3600 31 self._nocleanup_on_exit = False 32 self._suppressions = [] 33 for fname in supp_files: 34 self._suppressions.extend(suppressions.ReadSuppressionsFromFile(fname)) 35 if os.path.exists(self.TMP_FILE): 36 os.remove(self.TMP_FILE) 37 38 def PutEnvAndLog(self, env_name, env_value): 39 """Sets the env var |env_name| to |env_value| and writes to logging.info. 40 """ 41 os.putenv(env_name, env_value) 42 logging.info('export %s=%s', env_name, env_value) 43 44 def Execute(self): 45 """Executes the app to be tested.""" 46 logging.info('starting execution...') 47 proc = ['sh', path_utils.ScriptDir() + '/heapcheck_std.sh'] 48 proc += self._args 49 self.PutEnvAndLog('G_SLICE', 'always-malloc') 50 self.PutEnvAndLog('NSS_DISABLE_ARENA_FREE_LIST', '1') 51 self.PutEnvAndLog('NSS_DISABLE_UNLOAD', '1') 52 self.PutEnvAndLog('GTEST_DEATH_TEST_USE_FORK', '1') 53 self.PutEnvAndLog('HEAPCHECK', self._mode) 54 self.PutEnvAndLog('HEAP_CHECK_ERROR_EXIT_CODE', '0') 55 self.PutEnvAndLog('HEAP_CHECK_MAX_LEAKS', '-1') 56 self.PutEnvAndLog('KEEP_SHADOW_STACKS', '1') 57 self.PutEnvAndLog('PPROF_PATH', 58 path_utils.ScriptDir() + 59 '/../../third_party/tcmalloc/chromium/src/pprof') 60 self.PutEnvAndLog('LD_LIBRARY_PATH', 61 '/usr/lib/debug/:/usr/lib32/debug/') 62 # CHROME_DEVEL_SANDBOX causes problems with heapcheck 63 self.PutEnvAndLog('CHROME_DEVEL_SANDBOX', ''); 64 65 return common.RunSubprocess(proc, self._timeout) 66 67 def Analyze(self, log_lines, check_sanity=False): 68 """Analyzes the app's output and applies suppressions to the reports. 69 70 Analyze() searches the logs for leak reports and tries to apply 71 suppressions to them. Unsuppressed reports and other log messages are 72 dumped as is. 73 74 If |check_sanity| is True, the list of suppressed reports is searched for a 75 report starting with SANITY_TEST_SUPPRESSION. If there isn't one, Analyze 76 returns 2 regardless of the unsuppressed reports. 77 78 Args: 79 log_lines: An iterator over the app's log lines. 80 check_sanity: A flag that determines whether we should check the tool's 81 sanity. 82 Returns: 83 2, if the sanity check fails, 84 1, if unsuppressed reports remain in the output and the sanity check 85 passes, 86 0, if all the errors are suppressed and the sanity check passes. 87 """ 88 return_code = 0 89 # leak signature: [number of bytes, number of objects] 90 cur_leak_signature = None 91 cur_stack = [] 92 cur_report = [] 93 reported_hashes = {} 94 # Statistics grouped by suppression description: 95 # [hit count, bytes, objects]. 96 used_suppressions = {} 97 hooked_allocator_line_encountered = False 98 for line in log_lines: 99 line = line.rstrip() # remove the trailing \n 100 match = self.STACK_LINE_RE.match(line) 101 if match: 102 cur_stack.append(match.groups()[0]) 103 cur_report.append(line) 104 continue 105 else: 106 if cur_stack: 107 # Try to find the suppression that applies to the current leak stack. 108 description = '' 109 for supp in self._suppressions: 110 if supp.Match(cur_stack): 111 cur_stack = [] 112 description = supp.description 113 break 114 if cur_stack: 115 if not cur_leak_signature: 116 print 'Missing leak signature for the following stack: ' 117 for frame in cur_stack: 118 print ' ' + frame 119 print 'Aborting...' 120 return 3 121 122 # Drop boring callers from the stack to get less redundant info 123 # and fewer unique reports. 124 found_boring = False 125 for i in range(1, len(cur_stack)): 126 for j in self.BORING_CALLERS: 127 if re.match(j, cur_stack[i]): 128 cur_stack = cur_stack[:i] 129 cur_report = cur_report[:i] 130 found_boring = True 131 break 132 if found_boring: 133 break 134 135 error_hash = hash("".join(cur_stack)) & 0xffffffffffffffff 136 if error_hash not in reported_hashes: 137 reported_hashes[error_hash] = 1 138 # Print the report and set the return code to 1. 139 print ('Leak of %d bytes in %d objects allocated from:' 140 % tuple(cur_leak_signature)) 141 print '\n'.join(cur_report) 142 return_code = 1 143 # Generate the suppression iff the stack contains more than one 144 # frame (otherwise it's likely to be broken) 145 if len(cur_stack) > 1 or found_boring: 146 print '\nSuppression (error hash=#%016X#):\n{' % (error_hash) 147 print ' <insert_a_suppression_name_here>' 148 print ' Heapcheck:Leak' 149 for frame in cur_stack: 150 print ' fun:' + frame 151 print '}\n\n' 152 else: 153 print ('This stack may be broken due to omitted frame pointers.' 154 ' It is not recommended to suppress it.\n') 155 else: 156 # Update the suppressions histogram. 157 if description in used_suppressions: 158 hits, bytes, objects = used_suppressions[description] 159 hits += 1 160 bytes += cur_leak_signature[0] 161 objects += cur_leak_signature[1] 162 used_suppressions[description] = [hits, bytes, objects] 163 else: 164 used_suppressions[description] = [1] + cur_leak_signature 165 cur_stack = [] 166 cur_report = [] 167 cur_leak_signature = None 168 match = self.LEAK_REPORT_RE.match(line) 169 if match: 170 cur_leak_signature = map(int, match.groups()) 171 else: 172 match = self.HOOKED_ALLOCATOR_RE.match(line) 173 if match: 174 hooked_allocator_line_encountered = True 175 else: 176 print line 177 # Print the list of suppressions used. 178 is_sane = False 179 if used_suppressions: 180 print 181 print '-----------------------------------------------------' 182 print 'Suppressions used:' 183 print ' count bytes objects name' 184 histo = {} 185 for description in used_suppressions: 186 if description.startswith(HeapcheckWrapper.SANITY_TEST_SUPPRESSION): 187 is_sane = True 188 hits, bytes, objects = used_suppressions[description] 189 line = '%8d %8d %8d %s' % (hits, bytes, objects, description) 190 if hits in histo: 191 histo[hits].append(line) 192 else: 193 histo[hits] = [line] 194 keys = histo.keys() 195 keys.sort() 196 for count in keys: 197 for line in histo[count]: 198 print line 199 print '-----------------------------------------------------' 200 if hooked_allocator_line_encountered: 201 print ('WARNING: Workaround for http://crbug.com/132867 (tons of ' 202 '"Hooked allocator frame not found, returning empty trace") ' 203 'in effect.') 204 if check_sanity and not is_sane: 205 logging.error("Sanity check failed") 206 return 2 207 else: 208 return return_code 209 210 def RunTestsAndAnalyze(self, check_sanity): 211 exec_retcode = self.Execute() 212 log_file = file(self.TMP_FILE, 'r') 213 analyze_retcode = self.Analyze(log_file, check_sanity) 214 log_file.close() 215 216 if analyze_retcode: 217 logging.error("Analyze failed.") 218 return analyze_retcode 219 220 if exec_retcode: 221 logging.error("Test execution failed.") 222 return exec_retcode 223 else: 224 logging.info("Test execution completed successfully.") 225 226 return 0 227 228 def Main(self, args, check_sanity=False): 229 self._args = args 230 start = datetime.datetime.now() 231 retcode = -1 232 retcode = self.RunTestsAndAnalyze(check_sanity) 233 end = datetime.datetime.now() 234 seconds = (end - start).seconds 235 hours = seconds / 3600 236 seconds %= 3600 237 minutes = seconds / 60 238 seconds %= 60 239 logging.info('elapsed time: %02d:%02d:%02d', hours, minutes, seconds) 240 logging.info('For more information on the Heapcheck bot see ' 241 'http://dev.chromium.org/developers/how-tos/' 242 'using-the-heap-leak-checker') 243 return retcode 244 245 246 def RunTool(args, supp_files, module): 247 tool = HeapcheckWrapper(supp_files) 248 MODULES_TO_SANITY_CHECK = ["base"] 249 check_sanity = module in MODULES_TO_SANITY_CHECK 250 return tool.Main(args[1:], check_sanity) 251