Bumping manifests a=b2g-bump
[gecko.git] / config / find_OOM_errors.py
blob16065119b78a9516594c5d771403bbe539a19035
1 #!/usr/bin/env python
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 from __future__ import print_function
7 usage = """%prog: A test for OOM conditions in the shell.
9 %prog finds segfaults and other errors caused by incorrect handling of
10 allocation during OOM (out-of-memory) conditions.
11 """
13 help = """Check for regressions only. This runs a set of files with a known
14 number of OOM errors (specified by REGRESSION_COUNT), and exits with a non-zero
15 result if more or less errors are found. See js/src/Makefile.in for invocation.
16 """
19 import hashlib
20 import re
21 import shlex
22 import subprocess
23 import sys
24 import threading
25 import time
27 from optparse import OptionParser
29 #####################################################################
30 # Utility functions
31 #####################################################################
32 def run(args, stdin=None):
33 class ThreadWorker(threading.Thread):
34 def __init__(self, pipe):
35 super(ThreadWorker, self).__init__()
36 self.all = ""
37 self.pipe = pipe
38 self.setDaemon(True)
40 def run(self):
41 while True:
42 line = self.pipe.readline()
43 if line == '': break
44 else:
45 self.all += line
47 try:
48 if type(args) == str:
49 args = shlex.split(args)
51 args = [str(a) for a in args] # convert to strs
53 stdin_pipe = subprocess.PIPE if stdin else None
54 proc = subprocess.Popen(args, stdin=stdin_pipe, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
55 if stdin_pipe:
56 proc.stdin.write(stdin)
57 proc.stdin.close()
59 stdout_worker = ThreadWorker(proc.stdout)
60 stderr_worker = ThreadWorker(proc.stderr)
61 stdout_worker.start()
62 stderr_worker.start()
64 proc.wait()
65 stdout_worker.join()
66 stderr_worker.join()
68 except KeyboardInterrupt as e:
69 sys.exit(-1)
71 stdout, stderr = stdout_worker.all, stderr_worker.all
72 result = (stdout, stderr, proc.returncode)
73 return result
75 def get_js_files():
76 (out, err, exit) = run('find ../jit-test/tests -name "*.js"')
77 if (err, exit) != ("", 0):
78 sys.exit("Wrong directory, run from an objdir")
79 return out.split()
83 #####################################################################
84 # Blacklisting
85 #####################################################################
86 def in_blacklist(sig):
87 return sig in blacklist
89 def add_to_blacklist(sig):
90 blacklist[sig] = blacklist.get(sig, 0)
91 blacklist[sig] += 1
93 # How often is a particular lines important for this.
94 def count_lines():
95 """Keep track of the amount of times individual lines occur, in order to
96 prioritize the errors which occur most frequently."""
97 counts = {}
98 for string,count in blacklist.items():
99 for line in string.split("\n"):
100 counts[line] = counts.get(line, 0) + count
102 lines = []
103 for k,v in counts.items():
104 lines.append("{0:6}: {1}".format(v, k))
106 lines.sort()
108 countlog = file("../OOM_count_log", "w")
109 countlog.write("\n".join(lines))
110 countlog.flush()
111 countlog.close()
114 #####################################################################
115 # Output cleaning
116 #####################################################################
117 def clean_voutput(err):
118 # Skip what we can't reproduce
119 err = re.sub(r"^--\d+-- run: /usr/bin/dsymutil \"shell/js\"$", "", err, flags=re.MULTILINE)
120 err = re.sub(r"^==\d+==", "", err, flags=re.MULTILINE)
121 err = re.sub(r"^\*\*\d+\*\*", "", err, flags=re.MULTILINE)
122 err = re.sub(r"^\s+by 0x[0-9A-Fa-f]+: ", "by: ", err, flags=re.MULTILINE)
123 err = re.sub(r"^\s+at 0x[0-9A-Fa-f]+: ", "at: ", err, flags=re.MULTILINE)
124 err = re.sub(r"(^\s+Address 0x)[0-9A-Fa-f]+( is not stack'd)", r"\1\2", err, flags=re.MULTILINE)
125 err = re.sub(r"(^\s+Invalid write of size )\d+", r"\1x", err, flags=re.MULTILINE)
126 err = re.sub(r"(^\s+Invalid read of size )\d+", r"\1x", err, flags=re.MULTILINE)
127 err = re.sub(r"(^\s+Address 0x)[0-9A-Fa-f]+( is )\d+( bytes inside a block of size )[0-9,]+( free'd)", r"\1\2\3\4", err, flags=re.MULTILINE)
129 # Skip the repeating bit due to the segfault
130 lines = []
131 for l in err.split('\n'):
132 if l == " Process terminating with default action of signal 11 (SIGSEGV)":
133 break
134 lines.append(l)
135 err = '\n'.join(lines)
137 return err
139 def remove_failed_allocation_backtraces(err):
140 lines = []
142 add = True
143 for l in err.split('\n'):
145 # Set start and end conditions for including text
146 if l == " The site of the failed allocation is:":
147 add = False
148 elif l[:2] not in ['by: ', 'at:']:
149 add = True
151 if add:
152 lines.append(l)
155 err = '\n'.join(lines)
157 return err
160 def clean_output(err):
161 err = re.sub(r"^js\(\d+,0x[0-9a-f]+\) malloc: \*\*\* error for object 0x[0-9a-f]+: pointer being freed was not allocated\n\*\*\* set a breakppoint in malloc_error_break to debug\n$", "pointer being freed was not allocated", err, flags=re.MULTILINE)
163 return err
166 #####################################################################
167 # Consts, etc
168 #####################################################################
170 command_template = 'shell/js' \
171 + ' -m -j -p' \
172 + ' -e "const platform=\'darwin\'; const libdir=\'../jit-test/lib/\';"' \
173 + ' -f ../jit-test/lib/prolog.js' \
174 + ' -f {0}'
177 # Blacklists are things we don't want to see in our logs again (though we do
178 # want to count them when they happen). Whitelists we do want to see in our
179 # logs again, principally because the information we have isn't enough.
181 blacklist = {}
182 add_to_blacklist(r"('', '', 1)") # 1 means OOM if the shell hasn't launched yet.
183 add_to_blacklist(r"('', 'out of memory\n', 1)")
185 whitelist = set()
186 whitelist.add(r"('', 'out of memory\n', -11)") # -11 means OOM
187 whitelist.add(r"('', 'out of memory\nout of memory\n', -11)")
191 #####################################################################
192 # Program
193 #####################################################################
195 # Options
196 parser = OptionParser(usage=usage)
197 parser.add_option("-r", "--regression", action="store", metavar="REGRESSION_COUNT", help=help,
198 type="int", dest="regression", default=None)
200 (OPTIONS, args) = parser.parse_args()
203 if OPTIONS.regression != None:
204 # TODO: This should be expanded as we get a better hang of the OOM problems.
205 # For now, we'll just check that the number of OOMs in one short file does not
206 # increase.
207 files = ["../jit-test/tests/arguments/args-createontrace.js"]
208 else:
209 files = get_js_files()
211 # Use a command-line arg to reduce the set of files
212 if len (args):
213 files = [f for f in files if f.find(args[0]) != -1]
216 if OPTIONS.regression == None:
217 # Don't use a logfile, this is automated for tinderbox.
218 log = file("../OOM_log", "w")
221 num_failures = 0
222 for f in files:
224 # Run it once to establish boundaries
225 command = (command_template + ' -O').format(f)
226 out, err, exit = run(command)
227 max = re.match(".*OOM max count: (\d+).*", out, flags=re.DOTALL).groups()[0]
228 max = int(max)
230 # OOMs don't recover well for the first 20 allocations or so.
231 # TODO: revisit this.
232 for i in range(20, max):
234 if OPTIONS.regression == None:
235 print("Testing allocation {0}/{1} in {2}".format(i,max,f))
236 else:
237 sys.stdout.write('.') # something short for tinderbox, no space or \n
239 command = (command_template + ' -A {0}').format(f, i)
240 out, err, exit = run(command)
242 # Success (5 is SM's exit code for controlled errors)
243 if exit == 5 and err.find("out of memory") != -1:
244 continue
246 # Failure
247 else:
249 if OPTIONS.regression != None:
250 # Just count them
251 num_failures += 1
252 continue
254 #########################################################################
255 # The regression tests ends above. The rest of this is for running the
256 # script manually.
257 #########################################################################
259 problem = str((out, err, exit))
260 if in_blacklist(problem) and problem not in whitelist:
261 add_to_blacklist(problem)
262 continue
264 add_to_blacklist(problem)
267 # Get valgrind output for a good stack trace
268 vcommand = "valgrind --dsymutil=yes -q --log-file=OOM_valgrind_log_file " + command
269 run(vcommand)
270 vout = file("OOM_valgrind_log_file").read()
271 vout = clean_voutput(vout)
272 sans_alloc_sites = remove_failed_allocation_backtraces(vout)
274 # Don't print duplicate information
275 if in_blacklist(sans_alloc_sites):
276 add_to_blacklist(sans_alloc_sites)
277 continue
279 add_to_blacklist(sans_alloc_sites)
281 log.write ("\n")
282 log.write ("\n")
283 log.write ("=========================================================================")
284 log.write ("\n")
285 log.write ("An allocation failure at\n\tallocation {0}/{1} in {2}\n\t"
286 "causes problems (detected using bug 624094)"
287 .format(i, max, f))
288 log.write ("\n")
289 log.write ("\n")
291 log.write ("Command (from obj directory, using patch from bug 624094):\n " + command)
292 log.write ("\n")
293 log.write ("\n")
294 log.write ("stdout, stderr, exitcode:\n " + problem)
295 log.write ("\n")
296 log.write ("\n")
298 double_free = err.find("pointer being freed was not allocated") != -1
299 oom_detected = err.find("out of memory") != -1
300 multiple_oom_detected = err.find("out of memory\nout of memory") != -1
301 segfault_detected = exit == -11
303 log.write ("Diagnosis: ")
304 log.write ("\n")
305 if multiple_oom_detected:
306 log.write (" - Multiple OOMs reported")
307 log.write ("\n")
308 if segfault_detected:
309 log.write (" - segfault")
310 log.write ("\n")
311 if not oom_detected:
312 log.write (" - No OOM checking")
313 log.write ("\n")
314 if double_free:
315 log.write (" - Double free")
316 log.write ("\n")
318 log.write ("\n")
320 log.write ("Valgrind info:\n" + vout)
321 log.write ("\n")
322 log.write ("\n")
323 log.flush()
325 if OPTIONS.regression == None:
326 count_lines()
328 print()
330 # Do the actual regression check
331 if OPTIONS.regression != None:
332 expected_num_failures = OPTIONS.regression
334 if num_failures != expected_num_failures:
336 print("TEST-UNEXPECTED-FAIL |", end='')
337 if num_failures > expected_num_failures:
338 print("More out-of-memory errors were found ({0}) than expected ({1}). "
339 "This probably means an allocation site has been added without a "
340 "NULL-check. If this is unavoidable, you can account for it by "
341 "updating Makefile.in.".format(num_failures, expected_num_failures),
342 end='')
343 else:
344 print("Congratulations, you have removed {0} out-of-memory error(s) "
345 "({1} remain)! Please account for it by updating Makefile.in."
346 .format(expected_num_failures - num_failures, num_failures),
347 end='')
348 sys.exit(-1)
349 else:
350 print('TEST-PASS | find_OOM_errors | Found the expected number of OOM '
351 'errors ({0})'.format(expected_num_failures))