| OLD | NEW |
| (Empty) |
| 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) | |
| OLD | NEW |