2011-01-17 15:15:00 -08:00
#!/usr/bin/env python
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 , 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 " ' )
2011-04-03 18:39:36 -07:00
if ( err , exit ) != ( " " , 0 ) :
2011-01-17 15:15:00 -08:00
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 ( " %6d : %s " % ( 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 %s '
# 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 ,
2011-04-03 18:39:36 -07:00
type = " int " , dest = " regression " , default = None )
2011-01-17 15:15:00 -08:00
( OPTIONS , args ) = parser . parse_args ( )
2011-04-03 18:39:36 -07:00
if OPTIONS . regression != None :
2011-01-17 15:15:00 -08:00
# 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 ]
2011-04-03 18:39:36 -07:00
if OPTIONS . regression == None :
2011-01-17 15:15:00 -08:00
# 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 ' ) % ( 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 %d / %d in %s " % ( i , max , f )
2011-04-03 18:29:46 -07:00
else :
sys . stdout . write ( ' . ' ) # something short for tinderbox, no space or \n
2011-01-17 15:15:00 -08:00
command = ( command_template + ' -A %d ' ) % ( 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 :
2011-04-03 18:39:36 -07:00
if OPTIONS . regression != None :
2011-01-17 15:15:00 -08:00
# 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 \t allocation %d / %d in %s \n \t causes problems (detected using bug 624094) " % ( 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 \n out 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 ( )
2011-04-03 18:39:36 -07:00
if OPTIONS . regression == None :
2011-01-17 15:15:00 -08:00
count_lines ( )
2011-04-03 18:29:46 -07:00
print ' \n ' ,
2011-01-17 15:15:00 -08:00
# Do the actual regression check
2011-04-03 18:39:36 -07:00
if OPTIONS . regression != None :
2011-01-17 15:15:00 -08:00
expected_num_failures = OPTIONS . regression
if num_failures != expected_num_failures :
print " TEST-UNEXPECTED-FAIL | " ,
if num_failures > expected_num_failures :
print " More out-of-memory errors were found ( %s ) than expected ( %d ). 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. " % ( num_failures , expected_num_failures ) ,
else :
print " Congratulations, you have removed %d out-of-memory error(s) ( %d remain)! Please account for it by updating Makefile.in. " % ( expected_num_failures - num_failures , num_failures ) ,
sys . exit ( - 1 )
else :
print ' TEST-PASS | find_OOM_errors | Found the expected number of OOM errors ( %d ) ' % ( expected_num_failures )