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