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