Home | History | Annotate | Download | only in heapcheck
      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