123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- #!/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))
|