diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/file_packager.py | 2 | ||||
-rw-r--r-- | tools/js_optimizer.py | 21 | ||||
-rw-r--r-- | tools/shared.py | 109 |
3 files changed, 90 insertions, 42 deletions
diff --git a/tools/file_packager.py b/tools/file_packager.py index 7e196efd..bfa8e2f0 100644 --- a/tools/file_packager.py +++ b/tools/file_packager.py @@ -228,7 +228,7 @@ if has_preloaded: curr = open(file_['localname'], 'rb').read() file_['data_end'] = start + len(curr) if AV_WORKAROUND: curr += '\x00' - print >> sys.stderr, 'bundling', file_['name'], file_['localname'], file_['data_start'], file_['data_end'] + #print >> sys.stderr, 'bundling', file_['name'], file_['localname'], file_['data_start'], file_['data_end'] start += len(curr) data.write(curr) data.close() diff --git a/tools/js_optimizer.py b/tools/js_optimizer.py index f2e610d0..52cae6e5 100644 --- a/tools/js_optimizer.py +++ b/tools/js_optimizer.py @@ -10,7 +10,9 @@ def path_from_root(*pathelems): JS_OPTIMIZER = path_from_root('tools', 'js-optimizer.js') -BEST_JS_PROCESS_SIZE = 1024*1024 +NUM_CHUNKS_PER_CORE = 1.5 +MIN_CHUNK_SIZE = 1024*1024 +MAX_CHUNK_SIZE = 20*1024*1024 WINDOWS = sys.platform.startswith('win') @@ -74,6 +76,8 @@ def run_on_js(filename, passes, js_engine, jcache): assert gen_end > gen_start pre = js[:gen_start] post = js[gen_end:] + if 'last' in passes: + post = post.replace(suffix, '') # no need to write out the metadata - nothing after us needs it js = js[gen_start:gen_end] else: pre = '' @@ -88,16 +92,21 @@ def run_on_js(filename, passes, js_engine, jcache): if i < len(parts)-1: func += '\n}\n' # last part needs no } m = func_sig.search(func) if m: - ident = m.group(1) + ident = m.group(2) else: if suffix: continue # ignore whitespace ident = 'anon_%d' % i + assert ident funcs.append((ident, func)) parts = None total_size = len(js) js = None - chunks = shared.JCache.chunkify(funcs, BEST_JS_PROCESS_SIZE, 'jsopt' if jcache else None) + cores = int(os.environ.get('EMCC_CORES') or multiprocessing.cpu_count()) + intended_num_chunks = int(round(cores * NUM_CHUNKS_PER_CORE)) + chunk_size = min(MAX_CHUNK_SIZE, max(MIN_CHUNK_SIZE, total_size / intended_num_chunks)) + + chunks = shared.JCache.chunkify(funcs, chunk_size, 'jsopt' if jcache else None) if jcache: # load chunks from cache where we can # TODO: ignore small chunks @@ -133,15 +142,15 @@ def run_on_js(filename, passes, js_engine, jcache): commands = map(lambda filename: [js_engine, JS_OPTIMIZER, filename, 'noPrintMetadata'] + passes, filenames) #print [' '.join(command) for command in commands] - cores = min(multiprocessing.cpu_count(), filenames) + cores = min(cores, filenames) if len(chunks) > 1 and cores >= 2: # We can parallelize - if DEBUG: print >> sys.stderr, 'splitting up js optimization into %d chunks, using %d cores (total: %.2f MB)' % (len(chunks), cores, total_size/(1024*1024.)) + if DEBUG: print >> sys.stderr, 'splitting up js optimization into %d chunks of size %d, using %d cores (total: %.2f MB)' % (len(chunks), chunk_size, cores, total_size/(1024*1024.)) pool = multiprocessing.Pool(processes=cores) filenames = pool.map(run_on_chunk, commands, chunksize=1) else: # We can't parallize, but still break into chunks to avoid uglify/node memory issues - if len(chunks) > 1 and DEBUG: print >> sys.stderr, 'splitting up js optimization into %d chunks' % (len(chunks)) + if len(chunks) > 1 and DEBUG: print >> sys.stderr, 'splitting up js optimization into %d chunks of size %d' % (len(chunks), chunk_size) filenames = [run_on_chunk(command) for command in commands] else: filenames = [] diff --git a/tools/shared.py b/tools/shared.py index 0282fbb1..6f97737e 100644 --- a/tools/shared.py +++ b/tools/shared.py @@ -295,6 +295,10 @@ CANONICAL_TEMP_DIR = os.path.join(TEMP_DIR, 'emscripten_temp') EMSCRIPTEN_TEMP_DIR = None DEBUG = os.environ.get('EMCC_DEBUG') +if DEBUG == "0": + DEBUG = None +DEBUG_CACHE = DEBUG and "cache" in DEBUG + if DEBUG: try: EMSCRIPTEN_TEMP_DIR = CANONICAL_TEMP_DIR @@ -554,7 +558,7 @@ class Settings: ret = [] for key, value in Settings.__dict__.iteritems(): if key == key.upper(): # this is a hack. all of our settings are ALL_CAPS, python internals are not - jsoned = json.dumps(value) + jsoned = json.dumps(value, sort_keys=True) ret += ['-s', key + '=' + jsoned] return ret @@ -563,6 +567,7 @@ class Settings: if opt_level >= 1: Settings.ASSERTIONS = 0 Settings.DISABLE_EXCEPTION_CATCHING = 1 + Settings.EMIT_GENERATED_FUNCTIONS = 1 if opt_level >= 2: Settings.RELOOP = 1 if opt_level >= 3: @@ -663,6 +668,9 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)''' % { 'winfix': '' if not WINDOWS e def make(args, stdout=None, stderr=None, env=None): if env is None: env = Building.get_building_env() + if not args: + print >> sys.stderr, 'Error: Executable to run not specified.' + sys.exit(1) #args += ['VERBOSE=1'] try: Popen(args, stdout=stdout, stderr=stderr, env=env).communicate() @@ -746,12 +754,16 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)''' % { 'winfix': '' if not WINDOWS e resolved_symbols = set() temp_dirs = [] files = map(os.path.abspath, files) + has_ar = False + for f in files: + has_ar = has_ar or Building.is_ar(f) for f in files: if not Building.is_ar(f): if Building.is_bitcode(f): - new_symbols = Building.llvm_nm(f) - resolved_symbols = resolved_symbols.union(new_symbols.defs) - unresolved_symbols = unresolved_symbols.union(new_symbols.undefs.difference(resolved_symbols)).difference(new_symbols.defs) + if has_ar: + new_symbols = Building.llvm_nm(f) + resolved_symbols = resolved_symbols.union(new_symbols.defs) + unresolved_symbols = unresolved_symbols.union(new_symbols.undefs.difference(resolved_symbols)).difference(new_symbols.defs) actual_files.append(f) else: # Extract object files from ar archives, and link according to gnu ld semantics @@ -804,7 +816,26 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)''' % { 'winfix': '' if not WINDOWS e # Finish link actual_files = unique_ordered(actual_files) # tolerate people trying to link a.so a.so etc. if DEBUG: print >>sys.stderr, 'emcc: llvm-linking:', actual_files - output = Popen([LLVM_LINK] + actual_files + ['-o', target], stdout=PIPE).communicate()[0] + + # check for too-long command line + link_cmd = [LLVM_LINK] + actual_files + ['-o', target] + # 8k is a bit of an arbitrary limit, but a reasonable one + # for max command line size before we use a respose file + response_file = None + if len(' '.join(link_cmd)) > 8192: + if DEBUG: print >>sys.stderr, 'using response file for llvm-link' + [response_fd, response_file] = mkstemp(suffix='.response', dir=TEMP_DIR) + response_fh = os.fdopen(response_fd, 'w') + for arg in actual_files: + response_fh.write(arg + "\n") + response_fh.close() + link_cmd = [LLVM_LINK, "@" + response_file, '-o', target] + + output = Popen(link_cmd, stdout=PIPE).communicate()[0] + + if response_file: + os.unlink(response_file) + assert os.path.exists(target) and (output is None or 'Could not open input file' not in output), 'Linking error: ' + output for temp_dir in temp_dirs: try_delete(temp_dir) @@ -826,6 +857,7 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)''' % { 'winfix': '' if not WINDOWS e def llvm_opt(filename, opts): if type(opts) is int: opts = Building.pick_llvm_opts(opts) + #opts += ['-debug-pass=Arguments'] if DEBUG: print >> sys.stderr, 'emcc: LLVM opts:', opts output = Popen([LLVM_OPT, filename] + opts + ['-o=' + filename + '.opt.bc'], stdout=PIPE).communicate()[0] assert os.path.exists(filename + '.opt.bc'), 'Failed to run llvm optimizations: ' + output @@ -862,8 +894,14 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)''' % { 'winfix': '' if not WINDOWS e assert os.path.exists(output_filename), 'Could not create bc file: ' + output return output_filename + nm_cache = {} # cache results of nm - it can be slow to run + @staticmethod def llvm_nm(filename, stdout=PIPE, stderr=None): + if filename in Building.nm_cache: + #if DEBUG: print >> sys.stderr, 'loading nm results for %s from cache' % filename + return Building.nm_cache[filename] + # LLVM binary ==> list of symbols output = Popen([LLVM_NM, filename], stdout=stdout, stderr=stderr).communicate()[0] class ret: @@ -884,6 +922,7 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)''' % { 'winfix': '' if not WINDOWS e ret.defs = set(ret.defs) ret.undefs = set(ret.undefs) ret.commons = set(ret.commons) + Building.nm_cache[filename] = ret return ret @staticmethod @@ -1084,24 +1123,6 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)''' % { 'winfix': '' if not WINDOWS e @staticmethod def is_bitcode(filename): - # checks if a file contains LLVM bitcode - # if the file doesn't exist or doesn't have valid symbols, it isn't bitcode - try: - defs = Building.llvm_nm(filename, stderr=PIPE) - # If no symbols found, it might just be an empty bitcode file, try to dis it - if len(defs.defs) + len(defs.undefs) + len(defs.commons) == 0: - # llvm-nm 3.0 has a bug when reading symbols from ar files - # so try to see if we're dealing with an ar file, in which - # case we should try to dis it. - if not Building.is_ar(filename): - test_ll = os.path.join(EMSCRIPTEN_TEMP_DIR, 'test.ll') - Building.llvm_dis(filename, test_ll) - assert os.path.exists(test_ll) - try_delete(test_ll) - except Exception, e: - if DEBUG: print >> sys.stderr, 'shared.Building.is_bitcode failed to test whether file \'%s\' is a llvm bitcode file! Failed on exception: %s' % (filename, e) - return False - # look for magic signature b = open(filename, 'r').read(4) if b[0] == 'B' and b[1] == 'C': @@ -1177,6 +1198,10 @@ class Cache: except: pass try_delete(RELOOPER) + try: + open(Cache.dirname + '__last_clear', 'w').write('last clear: ' + time.asctime() + '\n') + except: + print >> sys.stderr, 'failed to save last clear time' # Request a cached file. If it isn't in the cache, it will be created with # the given creator function @@ -1219,29 +1244,30 @@ class JCache: # Returns a cached value, if it exists. Make sure the full key matches @staticmethod def get(shortkey, keys): - #if DEBUG: print >> sys.stderr, 'jcache get?', shortkey + if DEBUG_CACHE: print >> sys.stderr, 'jcache get?', shortkey cachename = JCache.get_cachename(shortkey) if not os.path.exists(cachename): - #if DEBUG: print >> sys.stderr, 'jcache none at all' + if DEBUG_CACHE: print >> sys.stderr, 'jcache none at all' return data = cPickle.Unpickler(open(cachename, 'rb')).load() if len(data) != 2: - #if DEBUG: print >> sys.stderr, 'jcache error in get' + if DEBUG_CACHE: print >> sys.stderr, 'jcache error in get' return oldkeys = data[0] if len(oldkeys) != len(keys): - #if DEBUG: print >> sys.stderr, 'jcache collision (a)' + if DEBUG_CACHE: print >> sys.stderr, 'jcache collision (a)' return for i in range(len(oldkeys)): if oldkeys[i] != keys[i]: - #if DEBUG: print >> sys.stderr, 'jcache collision (b)' + if DEBUG_CACHE: print >> sys.stderr, 'jcache collision (b)' return - #if DEBUG: print >> sys.stderr, 'jcache win' + if DEBUG_CACHE: print >> sys.stderr, 'jcache win' return data[1] # Sets the cached value for a key (from get_key) @staticmethod def set(shortkey, keys, value): + if DEBUG_CACHE: print >> sys.stderr, 'save to cache', shortkey cachename = JCache.get_cachename(shortkey) cPickle.Pickler(open(cachename, 'wb')).dump([keys, value]) #if DEBUG: @@ -1265,28 +1291,36 @@ class JCache: if os.path.exists(chunking_file): try: previous_mapping = cPickle.Unpickler(open(chunking_file, 'rb')).load() # maps a function identifier to the chunk number it will be in - except: - pass + if DEBUG: print >> sys.stderr, 'jscache previous mapping of size %d loaded from %s' % (len(previous_mapping), chunking_file) + except Exception, e: + print >> sys.stderr, 'Failed to load and unpickle previous chunking file at %s: ' % chunking_file, e + else: + print >> sys.stderr, 'Previous chunking file not found at %s' % chunking_file chunks = [] if previous_mapping: # initialize with previous chunking news = [] for func in funcs: ident, data = func + assert ident, 'need names for jcache chunking' if not ident in previous_mapping: news.append(func) else: n = previous_mapping[ident] while n >= len(chunks): chunks.append([]) chunks[n].append(func) + if DEBUG: print >> sys.stderr, 'jscache not in previous chunking', len(news) # add news and adjust for new sizes spilled = news - for chunk in chunks: + for i in range(len(chunks)): + chunk = chunks[i] size = sum([len(func[1]) for func in chunk]) - while size > 1.5*chunk_size and len(chunk) > 0: + #if DEBUG: print >> sys.stderr, 'need spilling?', i, size, len(chunk), 'vs', chunk_size, 1.5*chunk_size + while size > 1.5*chunk_size and len(chunk) > 1: spill = chunk.pop() spilled.append(spill) size -= len(spill[1]) + #if DEBUG: print >> sys.stderr, 'jscache new + spilled', len(spilled) for chunk in chunks: size = sum([len(func[1]) for func in chunk]) while size < 0.66*chunk_size and len(spilled) > 0: @@ -1295,6 +1329,7 @@ class JCache: size += len(spill[1]) chunks = filter(lambda chunk: len(chunk) > 0, chunks) # might have empty ones, eliminate them funcs = spilled # we will allocate these into chunks as if they were normal inputs + #if DEBUG: print >> sys.stderr, 'leftover spills', len(spilled) # initialize reasonably, the rest of the funcs we need to split out curr = [] total_size = 0 @@ -1320,15 +1355,19 @@ class JCache: for i in range(len(chunks)): chunk = chunks[i] for ident, data in chunk: + assert ident not in new_mapping, 'cannot have duplicate names in jcache chunking' new_mapping[ident] = i cPickle.Pickler(open(chunking_file, 'wb')).dump(new_mapping) + if DEBUG: print >> sys.stderr, 'jscache mapping of size %d saved to %s' % (len(new_mapping), chunking_file) #if DEBUG: + # for i in range(len(chunks)): + # chunk = chunks[i] + # print >> sys.stderr, 'final chunk', i, len(chunk) + # print >> sys.stderr, 'new mapping:', new_mapping # if previous_mapping: # for ident in set(previous_mapping.keys() + new_mapping.keys()): # if previous_mapping.get(ident) != new_mapping.get(ident): # print >> sys.stderr, 'mapping inconsistency', ident, previous_mapping.get(ident), new_mapping.get(ident) - # for key, value in new_mapping.iteritems(): - # print >> sys.stderr, 'mapping:', key, value return [''.join([func[1] for func in chunk]) for chunk in chunks] # remove function names class JS: |