diff --git a/emcc.py b/emcc.py index f9e2929f0557c..e4a670ac0a33f 100755 --- a/emcc.py +++ b/emcc.py @@ -2363,20 +2363,21 @@ def do_binaryen(final, target, asm_target, options, memfile, wasm_binary_target, if options.opt_level >= 2: # minify the JS optimizer.do_minify() # calculate how to minify - if optimizer.cleanup_shell or optimizer.minify_whitespace or options.use_closure_compiler: - misc_temp_files.note(final) + if optimizer.cleanup_shell or options.use_closure_compiler: if DEBUG: save_intermediate('preclean', 'js') + # in -Os and -Oz, run AJSDCE (aggressive JS DCE, performs multiple iterations) + passes = ['noPrintMetadata', 'JSDCE' if options.shrink_level == 0 else 'AJSDCE', 'last'] + if optimizer.minify_whitespace: + passes.append('minifyWhitespace') + misc_temp_files.note(final) + logging.debug('running cleanup on shell code: ' + ' '.join(passes)) + final = shared.Building.js_optimizer_no_asmjs(final, passes) + if DEBUG: save_intermediate('postclean', 'js') if options.use_closure_compiler: logging.debug('running closure on shell code') + misc_temp_files.note(final) final = shared.Building.closure_compiler(final, pretty=not optimizer.minify_whitespace) - else: - assert optimizer.cleanup_shell - logging.debug('running cleanup on shell code') - passes = ['noPrintMetadata', 'JSDCE', 'last'] - if optimizer.minify_whitespace: - passes.append('minifyWhitespace') - final = shared.Building.js_optimizer_no_asmjs(final, passes) - if DEBUG: save_intermediate('postclean', 'js') + if DEBUG: save_intermediate('postclosure', 'js') # replace placeholder strings with correct subresource locations if shared.Settings.SINGLE_FILE: f = open(final, 'r') diff --git a/tests/optimizer/AJSDCE-output.js b/tests/optimizer/AJSDCE-output.js new file mode 100644 index 0000000000000..b4804b9429bf2 --- /dev/null +++ b/tests/optimizer/AJSDCE-output.js @@ -0,0 +1,27 @@ + +var z = fleefl(); +var zz = fleefl(); +function g(a) { + return a + 1; +} +Module["g"] = g; +function h(a) { + return a + 1; +} +print(h(123)); +((function() { + var z = fleefl(); + var zz = fleefl(); + function g(a) { + return a + 1; + } + Module["g"] = g; + function hh(a) { + return a + 1; + } + print(hh(123)); +}))(); +function glue() { +} +glue(); + diff --git a/tests/optimizer/AJSDCE.js b/tests/optimizer/AJSDCE.js new file mode 100644 index 0000000000000..ca1c133929fee --- /dev/null +++ b/tests/optimizer/AJSDCE.js @@ -0,0 +1,61 @@ + +// all unused +var x; +var y = 1; +var z = fleefl(); +var xx, yy = 1, zz = fleefl(); // but zz must remain due to the side effects in the value +function f(x, y, z) { + // shadow the x,y,z + x = y; + y = z; +} + +// exported +function g(a) { + return a+1; +} +Module['g'] = g; + +// used +function h(a) { + var t; // unused + return a+1; +} +print(h(123)); + +// inner workings +(function() { + var x; + var y = 1; + var z = fleefl(); + var xx, yy = 1, zz = fleefl(); + function f(x, y, z) { + // shadow the x,y,z + x = y; + y = z; + } + + // exported + function g(a) { + return a+1; + } + Module['g'] = g; + + // used + function hh(a) { + var t; // unused + return a+1; + } + print(hh(123)); +})(); + +function glue() { + function lookup() { // 2 passes needed for this one + throw 1; + } + function removable() { // first remove this + lookup(); + } +} +glue(); + diff --git a/tests/optimizer/JSDCE-output.js b/tests/optimizer/JSDCE-output.js index efa7528d0975a..50194093bf33b 100644 --- a/tests/optimizer/JSDCE-output.js +++ b/tests/optimizer/JSDCE-output.js @@ -21,4 +21,10 @@ print(h(123)); } print(hh(123)); }))(); +function glue() { + function lookup() { + throw 1; + } +} +glue(); diff --git a/tests/optimizer/JSDCE.js b/tests/optimizer/JSDCE.js index d496992bfe052..ca1c133929fee 100644 --- a/tests/optimizer/JSDCE.js +++ b/tests/optimizer/JSDCE.js @@ -49,3 +49,13 @@ print(h(123)); print(hh(123)); })(); +function glue() { + function lookup() { // 2 passes needed for this one + throw 1; + } + function removable() { // first remove this + lookup(); + } +} +glue(); + diff --git a/tests/test_core.py b/tests/test_core.py index 9ace433518354..c8486349ab354 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -55,6 +55,13 @@ def is_split_memory(self): def is_wasm(self): return 'BINARYEN' in str(self.emcc_args) or self.is_wasm_backend() + # Use closure in some tests for some additional coverage + def maybe_closure(self): + if '-O2' in self.emcc_args or '-Os' in self.emcc_args: + self.emcc_args += ['--closure', '1'] + return True + return False + def do_run_in_out_file_test(self, *path, **kwargs): test_path = path_from_root(*path) @@ -882,8 +889,7 @@ def test_exceptions(self): Settings.EXCEPTION_DEBUG = 1 Settings.DISABLE_EXCEPTION_CATCHING = 0 - if '-O2' in self.emcc_args: - self.emcc_args += ['--closure', '1'] # Use closure here for some additional coverage + self.maybe_closure() src = ''' #include @@ -4089,8 +4095,7 @@ def test_langinfo(self): def test_files(self): self.banned_js_engines = [SPIDERMONKEY_ENGINE] # closure can generate variables called 'gc', which pick up js shell stuff - if '-O2' in self.emcc_args: - self.emcc_args += ['--closure', '1'] # Use closure here, to test we don't break FS stuff + if self.maybe_closure(): # Use closure here, to test we don't break FS stuff self.emcc_args = [x for x in self.emcc_args if x != '-g'] # ensure we test --closure 1 --memory-init-file 1 (-g would disable closure) elif '-O3' in self.emcc_args and not self.is_wasm(): print('closure 2') @@ -5095,8 +5100,7 @@ def test_sse1(self): orig_args = self.emcc_args for mode in [[], ['-s', 'SIMD=1']]: self.emcc_args = orig_args + mode + ['-msse'] - if '-O2' in self.emcc_args: - self.emcc_args += ['--closure', '1'] # Use closure here for some additional coverage + self.maybe_closure() self.do_run(open(path_from_root('tests', 'test_sse1.cpp'), 'r').read(), 'Success!') @@ -5118,8 +5122,7 @@ def test_sse1_full(self): orig_args = self.emcc_args for mode in [[], ['-s', 'SIMD=1']]: self.emcc_args = orig_args + mode + ['-I' + path_from_root('tests'), '-msse'] - if '-O2' in self.emcc_args: - self.emcc_args += ['--closure', '1'] # Use closure here for some additional coverage + self.maybe_closure() self.do_run(open(path_from_root('tests', 'test_sse1_full.cpp'), 'r').read(), self.ignore_nans(native_result), output_nicerizer=self.ignore_nans) @@ -5141,8 +5144,7 @@ def test_sse2_full(self): orig_args = self.emcc_args for mode in [[], ['-s', 'SIMD=1']]: self.emcc_args = orig_args + mode + ['-I' + path_from_root('tests'), '-msse2'] + args - if '-O2' in self.emcc_args: - self.emcc_args += ['--closure', '1'] # Use closure here for some additional coverage + self.maybe_closure() self.do_run(open(path_from_root('tests', 'test_sse2_full.cpp'), 'r').read(), self.ignore_nans(native_result), output_nicerizer=self.ignore_nans) @@ -5451,8 +5453,7 @@ def test_sqlite(self): force_c=True) def test_zlib(self): - if '-O2' in self.emcc_args and 'ASM_JS=0' not in self.emcc_args: # without asm, closure minifies Math.imul badly - self.emcc_args += ['--closure', '1'] # Use closure here for some additional coverage + self.maybe_closure() assert 'asm2g' in test_modes if self.run_name == 'asm2g': diff --git a/tests/test_other.py b/tests/test_other.py index e4135f11c82b1..0f7fc24d82cc9 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -1973,6 +1973,8 @@ def test_js_optimizer(self): ['JSDCE']), (path_from_root('tests', 'optimizer', 'JSDCE-hasOwnProperty.js'), open(path_from_root('tests', 'optimizer', 'JSDCE-hasOwnProperty-output.js')).read(), ['JSDCE']), + (path_from_root('tests', 'optimizer', 'AJSDCE.js'), open(path_from_root('tests', 'optimizer', 'AJSDCE-output.js')).read(), + ['AJSDCE']), ]: print(input, passes) diff --git a/tools/js-optimizer.js b/tools/js-optimizer.js index 8f12ea89e36f9..5454c4d0e9fcd 100644 --- a/tools/js-optimizer.js +++ b/tools/js-optimizer.js @@ -7807,112 +7807,127 @@ function eliminateDeadGlobals(ast) { // Removes obviously-unused code. Similar to closure compiler in its rules - // export e.g. by Module['..'] = theThing; , or use it somewhere, otherwise // it goes away. -function JSDCE(ast) { - var scopes = [{}]; // begin with empty toplevel scope - function DUMP() { - printErr('vvvvvvvvvvvvvv'); - for (var i = 0; i < scopes.length; i++) { - printErr(i + ' : ' + JSON.stringify(scopes[i])); - } - printErr('^^^^^^^^^^^^^^'); - } - function ensureData(scope, name) { - if (Object.prototype.hasOwnProperty.call(scope, name)) return scope[name]; - scope[name] = { - def: 0, - use: 0, - param: 0 // true for function params, which cannot be eliminated - }; - return scope[name]; - } - function cleanUp(ast, names) { +function JSDCE(ast, multipleIterations) { + function iteration() { + var removed = false; + var scopes = [{}]; // begin with empty toplevel scope + function DUMP() { + printErr('vvvvvvvvvvvvvv'); + for (var i = 0; i < scopes.length; i++) { + printErr(i + ' : ' + JSON.stringify(scopes[i])); + } + printErr('^^^^^^^^^^^^^^'); + } + function ensureData(scope, name) { + if (Object.prototype.hasOwnProperty.call(scope, name)) return scope[name]; + scope[name] = { + def: 0, + use: 0, + param: 0 // true for function params, which cannot be eliminated + }; + return scope[name]; + } + function cleanUp(ast, names) { + traverse(ast, function(node, type) { + if (type === 'defun' && Object.prototype.hasOwnProperty.call(names, node[1])) { + removed = true; + return emptyNode(); + } + if (type === 'defun' || type === 'function') return null; // do not enter other scopes + if (type === 'var') { + node[1] = node[1].filter(function(varItem, j) { + var curr = varItem[0]; + var value = varItem[1]; + var keep = !(curr in names) || (value && hasSideEffects(value)); + if (!keep) removed = true; + return keep; + }); + if (node[1].length === 0) return emptyNode(); + } + }); + return ast; + } + var isVarNameOrObjectKeys = []; + // isVarNameOrObjectKeys is a stack which saves the state the node is defining a variable or in an object literal. + // the second argument `type` passed into the callback function called by traverse() could be a variable name or object key name. + // You cannot distinguish the `type` is a real type or not without isVarNameOrObjectKeys. + // ex.) var name = true; // `type` can be 'name' + // var obj = { defun: true } // `type` can be 'defun' traverse(ast, function(node, type) { - if (type === 'defun' && Object.prototype.hasOwnProperty.call(names, node[1])) return emptyNode(); - if (type === 'defun' || type === 'function') return null; // do not enter other scopes + if (isVarNameOrObjectKeys[isVarNameOrObjectKeys.length - 1]) { // check parent node defines a variable or is an object literal + // `type` is a variable name or an object key name + isVarNameOrObjectKeys.push(false); // doesn't define a variable nor be an object literal + return; + } if (type === 'var') { - node[1] = node[1].filter(function(varItem, j) { - var curr = varItem[0]; - var value = varItem[1]; - return !(curr in names) || (value && hasSideEffects(value)); + node[1].forEach(function(varItem, j) { + var name = varItem[0]; + ensureData(scopes[scopes.length-1], name).def = 1; }); - if (node[1].length === 0) return emptyNode(); + isVarNameOrObjectKeys.push(true); // this `node` defines a varible + return; + } + if (type === 'object') { + isVarNameOrObjectKeys.push(true); // this `node` is an object literal + return; } - }); - return ast; - } - var isVarNameOrObjectKeys = []; - // isVarNameOrObjectKeys is a stack which saves the state the node is defining a variable or in an object literal. - // the second argument `type` passed into the callback function called by traverse() could be a variable name or object key name. - // You cannot distinguish the `type` is a real type or not without isVarNameOrObjectKeys. - // ex.) var name = true; // `type` can be 'name' - // var obj = { defun: true } // `type` can be 'defun' - traverse(ast, function(node, type) { - if (isVarNameOrObjectKeys[isVarNameOrObjectKeys.length - 1]) { // check parent node defines a variable or is an object literal - // `type` is a variable name or an object key name isVarNameOrObjectKeys.push(false); // doesn't define a variable nor be an object literal - return; - } - if (type === 'var') { - node[1].forEach(function(varItem, j) { - var name = varItem[0]; - ensureData(scopes[scopes.length-1], name).def = 1; - }); - isVarNameOrObjectKeys.push(true); // this `node` defines a varible - return; - } - if (type === 'object') { - isVarNameOrObjectKeys.push(true); // this `node` is an object literal - return; - } - isVarNameOrObjectKeys.push(false); // doesn't define a variable nor be an object literal - if (type === 'defun' || type === 'function') { - if (node[1]) ensureData(scopes[scopes.length-1], node[1]).def = 1; - var scope = {}; - node[2].forEach(function(param) { - ensureData(scope, param).def = 1; - scope[param].param = 1; - }); - scopes.push(scope); - return; - } - if (type === 'name') { - ensureData(scopes[scopes.length-1], node[1]).use = 1; - } - }, function(node, type) { - isVarNameOrObjectKeys.pop(); - if (isVarNameOrObjectKeys[isVarNameOrObjectKeys.length - 1]) return; // `type` is a variable name or an object key name - if (type === 'defun' || type === 'function') { - var scope = scopes.pop(); - var names = set(); - for (name in scope) { - var data = scope[name]; - if (data.use && !data.def) { - // this is used from a higher scope, propagate the use down - ensureData(scopes[scopes.length-1], name).use = 1; - continue; - } - if (data.def && !data.use && !data.param) { - // this is eliminateable! - names[name] = 0; + if (type === 'defun' || type === 'function') { + if (node[1]) ensureData(scopes[scopes.length-1], node[1]).def = 1; + var scope = {}; + node[2].forEach(function(param) { + ensureData(scope, param).def = 1; + scope[param].param = 1; + }); + scopes.push(scope); + return; + } + if (type === 'name') { + ensureData(scopes[scopes.length-1], node[1]).use = 1; + } + }, function(node, type) { + isVarNameOrObjectKeys.pop(); + if (isVarNameOrObjectKeys[isVarNameOrObjectKeys.length - 1]) return; // `type` is a variable name or an object key name + if (type === 'defun' || type === 'function') { + var scope = scopes.pop(); + var names = set(); + for (name in scope) { + var data = scope[name]; + if (data.use && !data.def) { + // this is used from a higher scope, propagate the use down + ensureData(scopes[scopes.length-1], name).use = 1; + continue; + } + if (data.def && !data.use && !data.param) { + // this is eliminateable! + names[name] = 0; + } } + cleanUp(node[3], names); + } + }); + // toplevel + var scope = scopes.pop(); + assert(scopes.length === 0); + + var names = set(); + for (var name in scope) { + var data = scope[name]; + if (data.def && !data.use) { + assert(!data.param); // can't be + // this is eliminateable! + names[name] = 0; } - cleanUp(node[3], names); - } - }); - // toplevel - var scope = scopes.pop(); - assert(scopes.length === 0); - - var names = set(); - for (var name in scope) { - var data = scope[name]; - if (data.def && !data.use) { - assert(!data.param); // can't be - // this is eliminateable! - names[name] = 0; } + cleanUp(ast, names); + return removed; } - cleanUp(ast, names); + while (iteration() && multipleIterations) { } +} + +// Aggressive JSDCE - multiple iterations +function AJSDCE(ast) { + JSDCE(ast, /* multipleIterations= */ true); } function removeFuncs(ast) { @@ -7961,6 +7976,7 @@ var passes = { dumpCallGraph: dumpCallGraph, asmLastOpts: asmLastOpts, JSDCE: JSDCE, + AJSDCE: AJSDCE, removeFuncs: removeFuncs, noop: function() {},