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."""
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)
28 def __init__(self
, supp_files
):
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
)
38 def PutEnvAndLog(self
, env_name
, env_value
):
39 """Sets the env var |env_name| to |env_value| and writes to logging.info.
41 os
.putenv(env_name
, env_value
)
42 logging
.info('export %s=%s', env_name
, env_value
)
45 """Executes the app to be tested."""
46 logging
.info('starting execution...')
47 proc
= ['sh', path_utils
.ScriptDir() + '/heapcheck_std.sh']
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', '');
65 return common
.RunSubprocess(proc
, self
._timeout
)
67 def Analyze(self
, log_lines
, check_sanity
=False):
68 """Analyzes the app's output and applies suppressions to the reports.
70 Analyze() searches the logs for leak reports and tries to apply
71 suppressions to them. Unsuppressed reports and other log messages are
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.
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
83 2, if the sanity check fails,
84 1, if unsuppressed reports remain in the output and the sanity check
86 0, if all the errors are suppressed and the sanity check passes.
89 # leak signature: [number of bytes, number of objects]
90 cur_leak_signature
= None
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
)
102 cur_stack
.append(match
.groups()[0])
103 cur_report
.append(line
)
107 # Try to find the suppression that applies to the current leak stack.
109 for supp
in self
._suppressions
:
110 if supp
.Match(cur_stack
):
112 description
= supp
.description
115 if not cur_leak_signature
:
116 print 'Missing leak signature for the following stack: '
117 for frame
in cur_stack
:
122 # Drop boring callers from the stack to get less redundant info
123 # and fewer unique reports.
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
]
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
)
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
153 print ('This stack may be broken due to omitted frame pointers.'
154 ' It is not recommended to suppress it.\n')
156 # Update the suppressions histogram.
157 if description
in used_suppressions
:
158 hits
, bytes
, objects
= used_suppressions
[description
]
160 bytes
+= cur_leak_signature
[0]
161 objects
+= cur_leak_signature
[1]
162 used_suppressions
[description
] = [hits
, bytes
, objects
]
164 used_suppressions
[description
] = [1] + cur_leak_signature
167 cur_leak_signature
= None
168 match
= self
.LEAK_REPORT_RE
.match(line
)
170 cur_leak_signature
= map(int, match
.groups())
172 match
= self
.HOOKED_ALLOCATOR_RE
.match(line
)
174 hooked_allocator_line_encountered
= True
177 # Print the list of suppressions used.
179 if used_suppressions
:
181 print '-----------------------------------------------------'
182 print 'Suppressions used:'
183 print ' count bytes objects name'
185 for description
in used_suppressions
:
186 if description
.startswith(HeapcheckWrapper
.SANITY_TEST_SUPPRESSION
):
188 hits
, bytes
, objects
= used_suppressions
[description
]
189 line
= '%8d %8d %8d %s' % (hits
, bytes
, objects
, description
)
191 histo
[hits
].append(line
)
197 for line
in histo
[count
]:
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") '
204 if check_sanity
and not is_sane
:
205 logging
.error("Sanity check failed")
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
)
217 logging
.error("Analyze failed.")
218 return analyze_retcode
221 logging
.error("Test execution failed.")
224 logging
.info("Test execution completed successfully.")
228 def Main(self
, args
, check_sanity
=False):
230 start
= datetime
.datetime
.now()
232 retcode
= self
.RunTestsAndAnalyze(check_sanity
)
233 end
= datetime
.datetime
.now()
234 seconds
= (end
- start
).seconds
235 hours
= seconds
/ 3600
237 minutes
= 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')
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
)