forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			352 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python
 | |
| # This Source Code Form is subject to the terms of the Mozilla Public
 | |
| # License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | |
| from __future__ import print_function
 | |
| 
 | |
| usage = """%prog: A test for OOM conditions in the shell.
 | |
| 
 | |
| %prog finds segfaults and other errors caused by incorrect handling of
 | |
| allocation during OOM (out-of-memory) conditions.
 | |
| """
 | |
| 
 | |
| help = """Check for regressions only. This runs a set of files with a known
 | |
| number of OOM errors (specified by REGRESSION_COUNT), and exits with a non-zero
 | |
| result if more or less errors are found. See js/src/Makefile.in for invocation.
 | |
| """
 | |
| 
 | |
| 
 | |
| import hashlib
 | |
| import re
 | |
| import shlex
 | |
| import subprocess
 | |
| import sys
 | |
| import threading
 | |
| import time
 | |
| 
 | |
| from optparse import OptionParser
 | |
| 
 | |
| #####################################################################
 | |
| # Utility functions
 | |
| #####################################################################
 | |
| def run(args, stdin=None):
 | |
|   class ThreadWorker(threading.Thread):
 | |
|     def __init__(self, pipe):
 | |
|       super(ThreadWorker, self).__init__()
 | |
|       self.all = ""
 | |
|       self.pipe = pipe
 | |
|       self.setDaemon(True)
 | |
| 
 | |
|     def run(self):
 | |
|       while True:
 | |
|         line = self.pipe.readline()
 | |
|         if line == '': break
 | |
|         else:
 | |
|           self.all += line
 | |
| 
 | |
|   try:
 | |
|     if type(args) == str:
 | |
|       args = shlex.split(args)
 | |
| 
 | |
|     args = [str(a) for a in args] # convert to strs
 | |
| 
 | |
|     stdin_pipe = subprocess.PIPE if stdin else None
 | |
|     proc = subprocess.Popen(args, stdin=stdin_pipe, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 | |
|     if stdin_pipe:
 | |
|       proc.stdin.write(stdin)
 | |
|       proc.stdin.close()
 | |
| 
 | |
|     stdout_worker = ThreadWorker(proc.stdout)
 | |
|     stderr_worker = ThreadWorker(proc.stderr)
 | |
|     stdout_worker.start()
 | |
|     stderr_worker.start()
 | |
| 
 | |
|     proc.wait()
 | |
|     stdout_worker.join()
 | |
|     stderr_worker.join()
 | |
| 
 | |
|   except KeyboardInterrupt as e:
 | |
|     sys.exit(-1)
 | |
| 
 | |
|   stdout, stderr = stdout_worker.all, stderr_worker.all
 | |
|   result = (stdout, stderr, proc.returncode)
 | |
|   return result
 | |
| 
 | |
| def get_js_files():
 | |
|   (out, err, exit) = run('find ../jit-test/tests -name "*.js"')
 | |
|   if (err, exit) != ("", 0):
 | |
|     sys.exit("Wrong directory, run from an objdir")
 | |
|   return out.split()
 | |
| 
 | |
| 
 | |
| 
 | |
| #####################################################################
 | |
| # Blacklisting
 | |
| #####################################################################
 | |
| def in_blacklist(sig):
 | |
|   return sig in blacklist
 | |
| 
 | |
| def add_to_blacklist(sig):
 | |
|   blacklist[sig] = blacklist.get(sig, 0)
 | |
|   blacklist[sig] += 1
 | |
| 
 | |
| # How often is a particular lines important for this.
 | |
| def count_lines():
 | |
|   """Keep track of the amount of times individual lines occur, in order to
 | |
|      prioritize the errors which occur most frequently."""
 | |
|   counts = {}
 | |
|   for string,count in blacklist.items():
 | |
|     for line in string.split("\n"):
 | |
|       counts[line] = counts.get(line, 0) + count
 | |
| 
 | |
|   lines = []
 | |
|   for k,v in counts.items():
 | |
|     lines.append("{0:6}: {1}".format(v, k))
 | |
| 
 | |
|   lines.sort()
 | |
| 
 | |
|   countlog = file("../OOM_count_log", "w")
 | |
|   countlog.write("\n".join(lines))
 | |
|   countlog.flush()
 | |
|   countlog.close()
 | |
| 
 | |
| 
 | |
| #####################################################################
 | |
| # Output cleaning
 | |
| #####################################################################
 | |
| def clean_voutput(err):
 | |
|   # Skip what we can't reproduce
 | |
|   err = re.sub(r"^--\d+-- run: /usr/bin/dsymutil \"shell/js\"$", "", err, flags=re.MULTILINE)
 | |
|   err = re.sub(r"^==\d+==", "", err, flags=re.MULTILINE)
 | |
|   err = re.sub(r"^\*\*\d+\*\*", "", err, flags=re.MULTILINE)
 | |
|   err = re.sub(r"^\s+by 0x[0-9A-Fa-f]+: ", "by: ", err, flags=re.MULTILINE)
 | |
|   err = re.sub(r"^\s+at 0x[0-9A-Fa-f]+: ", "at: ", err, flags=re.MULTILINE)
 | |
|   err = re.sub(r"(^\s+Address 0x)[0-9A-Fa-f]+( is not stack'd)", r"\1\2", err, flags=re.MULTILINE)
 | |
|   err = re.sub(r"(^\s+Invalid write of size )\d+", r"\1x", err, flags=re.MULTILINE)
 | |
|   err = re.sub(r"(^\s+Invalid read of size )\d+", r"\1x", err, flags=re.MULTILINE)
 | |
|   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)
 | |
| 
 | |
|   # Skip the repeating bit due to the segfault
 | |
|   lines = []
 | |
|   for l in err.split('\n'):
 | |
|     if l == " Process terminating with default action of signal 11 (SIGSEGV)":
 | |
|       break
 | |
|     lines.append(l)
 | |
|   err = '\n'.join(lines)
 | |
| 
 | |
|   return err
 | |
| 
 | |
| def remove_failed_allocation_backtraces(err):
 | |
|   lines = []
 | |
| 
 | |
|   add = True
 | |
|   for l in err.split('\n'):
 | |
| 
 | |
|     # Set start and end conditions for including text
 | |
|     if l == " The site of the failed allocation is:":
 | |
|       add = False
 | |
|     elif l[:2] not in ['by: ', 'at:']:
 | |
|       add = True
 | |
| 
 | |
|     if add:
 | |
|       lines.append(l)
 | |
| 
 | |
| 
 | |
|   err = '\n'.join(lines)
 | |
| 
 | |
|   return err
 | |
| 
 | |
| 
 | |
| def clean_output(err):
 | |
|   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)
 | |
| 
 | |
|   return err
 | |
| 
 | |
| 
 | |
| #####################################################################
 | |
| # Consts, etc
 | |
| #####################################################################
 | |
| 
 | |
| command_template = 'shell/js' \
 | |
|                  + ' -m -j -p' \
 | |
|                  + ' -e "const platform=\'darwin\'; const libdir=\'../jit-test/lib/\';"' \
 | |
|                  + ' -f ../jit-test/lib/prolog.js' \
 | |
|                  + ' -f {0}'
 | |
| 
 | |
| 
 | |
| # Blacklists are things we don't want to see in our logs again (though we do
 | |
| # want to count them when they happen). Whitelists we do want to see in our
 | |
| # logs again, principally because the information we have isn't enough.
 | |
| 
 | |
| blacklist = {}
 | |
| add_to_blacklist(r"('', '', 1)") # 1 means OOM if the shell hasn't launched yet.
 | |
| add_to_blacklist(r"('', 'out of memory\n', 1)")
 | |
| 
 | |
| whitelist = set()
 | |
| whitelist.add(r"('', 'out of memory\n', -11)") # -11 means OOM
 | |
| whitelist.add(r"('', 'out of memory\nout of memory\n', -11)")
 | |
| 
 | |
| 
 | |
| 
 | |
| #####################################################################
 | |
| # Program
 | |
| #####################################################################
 | |
| 
 | |
| # Options
 | |
| parser = OptionParser(usage=usage)
 | |
| parser.add_option("-r", "--regression", action="store", metavar="REGRESSION_COUNT", help=help,
 | |
|                   type="int", dest="regression", default=None)
 | |
|                   
 | |
| (OPTIONS, args) = parser.parse_args()
 | |
| 
 | |
| 
 | |
| if OPTIONS.regression != None:
 | |
|   # TODO: This should be expanded as we get a better hang of the OOM problems.
 | |
|   # For now, we'll just check that the number of OOMs in one short file does not
 | |
|   # increase.
 | |
|   files = ["../jit-test/tests/arguments/args-createontrace.js"]
 | |
| else:
 | |
|   files = get_js_files()
 | |
| 
 | |
|   # Use a command-line arg to reduce the set of files
 | |
|   if len (args):
 | |
|     files = [f for f in files if f.find(args[0]) != -1]
 | |
| 
 | |
| 
 | |
| if OPTIONS.regression == None:
 | |
|   # Don't use a logfile, this is automated for tinderbox.
 | |
|   log = file("../OOM_log", "w")
 | |
| 
 | |
| 
 | |
| num_failures = 0
 | |
| for f in files:
 | |
| 
 | |
|   # Run it once to establish boundaries
 | |
|   command = (command_template + ' -O').format(f)
 | |
|   out, err, exit = run(command)
 | |
|   max = re.match(".*OOM max count: (\d+).*", out, flags=re.DOTALL).groups()[0]
 | |
|   max = int(max)
 | |
|   
 | |
|   # OOMs don't recover well for the first 20 allocations or so.
 | |
|   # TODO: revisit this.
 | |
|   for i in range(20, max): 
 | |
| 
 | |
|     if OPTIONS.regression == None:
 | |
|       print("Testing allocation {0}/{1} in {2}".format(i,max,f))
 | |
|     else:
 | |
|       sys.stdout.write('.') # something short for tinderbox, no space or \n
 | |
| 
 | |
|     command = (command_template + ' -A {0}').format(f, i)
 | |
|     out, err, exit = run(command)
 | |
| 
 | |
|     # Success (5 is SM's exit code for controlled errors)
 | |
|     if exit == 5 and err.find("out of memory") != -1:
 | |
|       continue
 | |
| 
 | |
|     # Failure
 | |
|     else:
 | |
| 
 | |
|       if OPTIONS.regression != None:
 | |
|         # Just count them
 | |
|         num_failures += 1
 | |
|         continue
 | |
| 
 | |
|       #########################################################################
 | |
|       # The regression tests ends above. The rest of this is for running  the
 | |
|       # script manually.
 | |
|       #########################################################################
 | |
| 
 | |
|       problem = str((out, err, exit))
 | |
|       if in_blacklist(problem) and problem not in whitelist:
 | |
|         add_to_blacklist(problem)
 | |
|         continue
 | |
| 
 | |
|       add_to_blacklist(problem)
 | |
| 
 | |
| 
 | |
|       # Get valgrind output for a good stack trace
 | |
|       vcommand = "valgrind --dsymutil=yes -q --log-file=OOM_valgrind_log_file " + command
 | |
|       run(vcommand)
 | |
|       vout = file("OOM_valgrind_log_file").read()
 | |
|       vout = clean_voutput(vout)
 | |
|       sans_alloc_sites = remove_failed_allocation_backtraces(vout)
 | |
| 
 | |
|       # Don't print duplicate information
 | |
|       if in_blacklist(sans_alloc_sites):
 | |
|         add_to_blacklist(sans_alloc_sites)
 | |
|         continue
 | |
| 
 | |
|       add_to_blacklist(sans_alloc_sites)
 | |
| 
 | |
|       log.write ("\n")
 | |
|       log.write ("\n")
 | |
|       log.write ("=========================================================================")
 | |
|       log.write ("\n")
 | |
|       log.write ("An allocation failure at\n\tallocation {0}/{1} in {2}\n\t"
 | |
|                  "causes problems (detected using bug 624094)"
 | |
|                  .format(i, max, f))
 | |
|       log.write ("\n")
 | |
|       log.write ("\n")
 | |
| 
 | |
|       log.write ("Command (from obj directory, using patch from bug 624094):\n  " + command)
 | |
|       log.write ("\n")
 | |
|       log.write ("\n")
 | |
|       log.write ("stdout, stderr, exitcode:\n  " + problem)
 | |
|       log.write ("\n")
 | |
|       log.write ("\n")
 | |
| 
 | |
|       double_free = err.find("pointer being freed was not allocated") != -1
 | |
|       oom_detected = err.find("out of memory") != -1
 | |
|       multiple_oom_detected = err.find("out of memory\nout of memory") != -1
 | |
|       segfault_detected = exit == -11
 | |
| 
 | |
|       log.write ("Diagnosis: ")
 | |
|       log.write ("\n")
 | |
|       if multiple_oom_detected:
 | |
|         log.write ("  - Multiple OOMs reported")
 | |
|         log.write ("\n")
 | |
|       if segfault_detected:
 | |
|         log.write ("  - segfault")
 | |
|         log.write ("\n")
 | |
|       if not oom_detected:
 | |
|         log.write ("  - No OOM checking")
 | |
|         log.write ("\n")
 | |
|       if double_free:
 | |
|         log.write ("  - Double free")
 | |
|         log.write ("\n")
 | |
| 
 | |
|       log.write ("\n")
 | |
| 
 | |
|       log.write ("Valgrind info:\n" + vout)
 | |
|       log.write ("\n")
 | |
|       log.write ("\n")
 | |
|       log.flush()
 | |
| 
 | |
|   if OPTIONS.regression == None:
 | |
|     count_lines()
 | |
| 
 | |
| print()
 | |
| 
 | |
| # Do the actual regression check
 | |
| if OPTIONS.regression != None:
 | |
|   expected_num_failures = OPTIONS.regression
 | |
| 
 | |
|   if num_failures != expected_num_failures:
 | |
| 
 | |
|     print("TEST-UNEXPECTED-FAIL |", end='')
 | |
|     if num_failures > expected_num_failures:
 | |
|       print("More out-of-memory errors were found ({0}) than expected ({1}). "
 | |
|             "This probably means an allocation site has been added without a "
 | |
|             "NULL-check. If this is unavoidable, you can account for it by "
 | |
|             "updating Makefile.in.".format(num_failures, expected_num_failures),
 | |
|             end='')
 | |
|     else:
 | |
|       print("Congratulations, you have removed {0} out-of-memory error(s) "
 | |
|             "({1} remain)! Please account for it by updating Makefile.in." 
 | |
|             .format(expected_num_failures - num_failures, num_failures),
 | |
|             end='')
 | |
|     sys.exit(-1)
 | |
|   else:
 | |
|     print('TEST-PASS | find_OOM_errors | Found the expected number of OOM '
 | |
|           'errors ({0})'.format(expected_num_failures))
 | |
| 
 | 
