aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/js-optimizer.js175
-rw-r--r--tools/js_optimizer.py152
-rw-r--r--tools/test-js-optimizer-asm-regs-min-output.js36
-rw-r--r--tools/test-js-optimizer-asm-regs-min.js37
-rw-r--r--tools/test-js-optimizer-asm-regs-output.js4
-rw-r--r--tools/test-js-optimizer-asm-regs.js6
6 files changed, 362 insertions, 48 deletions
diff --git a/tools/js-optimizer.js b/tools/js-optimizer.js
index 9c744fa3..834c99e3 100644
--- a/tools/js-optimizer.js
+++ b/tools/js-optimizer.js
@@ -143,6 +143,8 @@ var FALSE_NODE = ['unary-prefix', '!', ['num', 1]];
var GENERATED_FUNCTIONS_MARKER = '// EMSCRIPTEN_GENERATED_FUNCTIONS';
var generatedFunctions = false; // whether we have received only generated functions
+var minifierInfo = null;
+
function srcToAst(src) {
return uglify.parser.parse(src);
}
@@ -218,12 +220,15 @@ function traverseGenerated(ast, pre, post, stack) {
function traverseGeneratedFunctions(ast, callback) {
assert(generatedFunctions);
- traverse(ast, function(node) {
- if (node[0] == 'defun') {
- callback(node);
- return null;
+ if (ast[0] == 'toplevel') {
+ var stats = ast[1];
+ for (var i = 0; i < stats.length; i++) {
+ var curr = stats[i];
+ if (curr[0] == 'defun') callback(curr);
}
- });
+ } else if (ast[0] == 'defun') {
+ callback(ast);
+ }
}
// Walk the ast in a simple way, with an understanding of which JS variables are defined)
@@ -535,9 +540,31 @@ function simplifyExpressionsPre(ast) {
});
}
+ function addFinalReturns(ast) {
+ traverseGeneratedFunctions(ast, function(fun) {
+ var returnType = null;
+ traverse(fun, function(node, type) {
+ if (type == 'return' && node[1]) {
+ returnType = detectAsmCoercion(node[1]);
+ }
+ });
+ // Add a final return if one is missing.
+ if (returnType !== null) {
+ var stats = getStatements(fun);
+ var last = stats[stats.length-1];
+ if (last[0] != 'return') {
+ var returnValue = ['num', 0];
+ if (returnType == ASM_DOUBLE) returnValue = ['unary-prefix', '+', returnValue];
+ stats.push(['return', returnValue]);
+ }
+ }
+ });
+ }
+
simplifyBitops(ast);
joinAdditions(ast);
// simplifyZeroComp(ast); TODO: investigate performance
+ if (asm) addFinalReturns(ast);
}
// In typed arrays mode 2, we can have
@@ -1300,7 +1327,9 @@ function normalizeAsm(func) {
var node = stats[i];
if (node[0] != 'stat' || node[1][0] != 'assign' || node[1][2][0] != 'name') break;
node = node[1];
- data.params[node[2][1]] = detectAsmCoercion(node[3]);
+ var name = node[2][1];
+ if (func[2] && func[2].indexOf(name) < 0) break; // not an assign into a parameter, but a global
+ data.params[name] = detectAsmCoercion(node[3]);
stats[i] = emptyNode();
i++;
}
@@ -1393,7 +1422,10 @@ function denormalizeAsm(func, data) {
//printErr('denormalized \n\n' + astToSrc(func) + '\n\n');
}
-// Very simple 'registerization', coalescing of variables into a smaller number.
+// Very simple 'registerization', coalescing of variables into a smaller number,
+// as part of minification. Globals-level minification began in a previous pass,
+// we receive minifierInfo which tells us how to rename globals. (Only in asm.js.)
+//
// We do not optimize when there are switches, so this pass only makes sense with
// relooping.
// TODO: Consider how this fits in with the rest of the optimization toolchain. Do
@@ -1420,7 +1452,6 @@ function registerize(ast) {
// We also mark local variables - i.e., having a var definition
var localVars = {};
var hasSwitch = false; // we cannot optimize variables if there is a switch
- var returnType = null; // for asm
traverse(fun, function(node, type) {
if (type == 'var') {
node[1].forEach(function(defined) { localVars[defined[0]] = 1 });
@@ -1432,11 +1463,74 @@ function registerize(ast) {
}
} else if (type == 'switch') {
hasSwitch = true;
- } else if (asm && type == 'return' && node[1]) {
- returnType = detectAsmCoercion(node[1]);
}
});
vacuum(fun);
+ if (minifierInfo) {
+ assert(asm);
+ var usedGlobals = {};
+ var nextLocal = 0;
+ // Minify globals using the mapping we were given
+ traverse(fun, function(node, type) {
+ if (type == 'name') {
+ var name = node[1];
+ var minified = minifierInfo.globals[name];
+ if (minified) {
+ assert(!localVars[name], name); // locals must not shadow globals, or else we don't know which is which
+ if (localVars[minified]) {
+ // trying to minify a global into a name used locally. rename all the locals
+ var newName = '$_newLocal_' + (nextLocal++);
+ assert(!localVars[newName]);
+ if (params[minified]) {
+ params[newName] = 1;
+ delete params[minified];
+ }
+ localVars[newName] = 1;
+ delete localVars[minified];
+ asmData.vars[newName] = asmData.vars[minified];
+ delete asmData.vars[minified];
+ asmData.params[newName] = asmData.params[minified];
+ delete asmData.params[minified];
+ traverse(fun, function(node, type) {
+ if (type == 'name' && node[1] == minified) {
+ node[1] = newName;
+ }
+ });
+ if (fun[2]) {
+ for (var i = 0; i < fun[2].length; i++) {
+ if (fun[2][i] == minified) fun[2][i] = newName;
+ }
+ }
+ }
+ node[1] = minified;
+ usedGlobals[minified] = 1;
+ }
+ }
+ });
+ assert(fun[1] in minifierInfo.globals, fun[1]);
+ fun[1] = minifierInfo.globals[fun[1]];
+ assert(fun[1]);
+ var nextRegName = 0;
+ }
+ var regTypes = {};
+ function getNewRegName(num, name) {
+ if (!asm) return 'r' + num;
+ var type = asmData.vars[name];
+ if (!minifierInfo) {
+ var ret = (type ? 'd' : 'i') + num;
+ regTypes[ret] = type;
+ return ret;
+ }
+ // find the next free minified name that is not used by a global that shows up in this function
+ while (nextRegName < minifierInfo.names.length) {
+ var ret = minifierInfo.names[nextRegName++];
+ if (!usedGlobals[ret]) {
+ regTypes[ret] = type;
+ return ret;
+ }
+ }
+ assert('ran out of names');
+ }
// Find the # of uses of each variable.
// While doing so, check if all a variable's uses are dominated in a simple
// way by a simple assign, if so, then we can assign its register to it
@@ -1521,7 +1615,7 @@ function registerize(ast) {
saved++;
} else {
reg = nextReg++;
- fullNames[reg] = (asm ? (asmData.vars[name] ? 'd' : 'i') : 'r') + reg; // TODO: even smaller names
+ fullNames[reg] = getNewRegName(reg, name);
if (params[name]) paramRegs[reg] = 1;
}
varRegs[name] = reg;
@@ -1565,7 +1659,7 @@ function registerize(ast) {
if (loopRegs[loops]) {
if (asm) {
loopRegs[loops].forEach(function(loopReg) {
- freeRegsClasses[fullNames[loopReg][0] == 'i' ? ASM_INT : ASM_DOUBLE].push(loopReg);
+ freeRegsClasses[regTypes[fullNames[loopReg]]].push(loopReg);
});
} else {
freeRegsClasses = freeRegsClasses.concat(loopRegs[loops]);
@@ -1601,7 +1695,7 @@ function registerize(ast) {
};
for (var i = 1; i < nextReg; i++) {
var reg = fullNames[i];
- var type = reg[0] == 'i' ? ASM_INT : ASM_DOUBLE
+ var type = regTypes[reg];
if (!paramRegs[i]) {
finalAsmData.vars[reg] = type;
} else {
@@ -1610,17 +1704,6 @@ function registerize(ast) {
}
}
denormalizeAsm(fun, finalAsmData);
- // Add a final return if one is missing. This is not strictly a register operation, but
- // this pass traverses the entire AST anyhow so adding it here is efficient.
- if (returnType !== null) {
- var stats = getStatements(fun);
- var last = stats[stats.length-1];
- if (last[0] != 'return') {
- var returnValue = ['num', 0];
- if (returnType == ASM_DOUBLE) returnValue = ['unary-prefix', '+', returnValue];
- stats.push(['return', returnValue]);
- }
- }
}
});
}
@@ -2155,6 +2238,43 @@ function eliminateMemSafe(ast) {
eliminate(ast, true);
}
+function minifyGlobals(ast) {
+ var minified = {};
+ var next = 0;
+ var first = true; // do not minify initial 'var asm ='
+ // find the globals
+ traverse(ast, function(node, type) {
+ if (type == 'var') {
+ if (first) {
+ first = false;
+ return;
+ }
+ var vars = node[1];
+ for (var i = 0; i < vars.length; i++) {
+ var name = vars[i][0];
+ assert(next < minifierInfo.names.length);
+ vars[i][0] = minified[name] = minifierInfo.names[next++];
+ }
+ }
+ });
+ // add all globals in function chunks, i.e. not here but passed to us
+ for (var i = 0; i < minifierInfo.globals.length; i++) {
+ name = minifierInfo.globals[i];
+ assert(next < minifierInfo.names.length);
+ minified[name] = minifierInfo.names[next++];
+ }
+ // apply minification
+ traverse(ast, function(node, type) {
+ if (type == 'name') {
+ var name = node[1];
+ if (name in minified) {
+ node[1] = minified[name];
+ }
+ }
+ });
+ suffix = '// MINIFY_INFO:' + JSON.stringify(minified);
+}
+
// Change +5 to DOT$ZERO(5). We then textually change 5 to 5.0 (uglify's ast cannot differentiate between 5 and 5.0 directly)
function prepDotZero(ast) {
traverse(ast, function(node, type) {
@@ -2200,6 +2320,7 @@ var passes = {
registerize: registerize,
eliminate: eliminate,
eliminateMemSafe: eliminateMemSafe,
+ minifyGlobals: minifyGlobals,
compress: function() { compress = true },
noPrintMetadata: function() { printMetadata = false },
asm: function() { asm = true },
@@ -2208,10 +2329,15 @@ var passes = {
// Main
+var suffix = '';
+
var src = read(arguments_[0]);
var ast = srcToAst(src);
//printErr(JSON.stringify(ast)); throw 1;
generatedFunctions = src.indexOf(GENERATED_FUNCTIONS_MARKER) >= 0;
+var minifierInfoStart = src.indexOf('// MINIFY_INFO:')
+if (minifierInfoStart > 0) minifierInfo = JSON.parse(src.substr(minifierInfoStart + 15));
+//printErr(JSON.stringify(minifierInfo));
arguments_.slice(1).forEach(function(arg) {
passes[arg](ast);
@@ -2231,4 +2357,5 @@ do {
} while (js != old);
print(js);
print('\n');
+print(suffix);
diff --git a/tools/js_optimizer.py b/tools/js_optimizer.py
index 13e6e4f6..60093bca 100644
--- a/tools/js_optimizer.py
+++ b/tools/js_optimizer.py
@@ -1,5 +1,5 @@
-import os, sys, subprocess, multiprocessing, re
+import os, sys, subprocess, multiprocessing, re, string, json
import shared
configuration = shared.configuration
@@ -19,6 +19,78 @@ WINDOWS = sys.platform.startswith('win')
DEBUG = os.environ.get('EMCC_DEBUG')
+func_sig = re.compile('( *)function ([_\w$]+)\(')
+
+class Minifier:
+ '''
+ asm.js minification support. We calculate possible names and minification of
+ globals here, then pass that into the parallel js-optimizer.js runners which
+ during registerize perform minification of locals.
+ '''
+
+ def __init__(self, js, js_engine):
+ self.js = js
+ self.js_engine = js_engine
+
+ # Create list of valid short names
+
+ MAX_NAMES = 6000#0
+ INVALID_2 = set(['do', 'if', 'in'])
+ INVALID_3 = set(['for', 'new', 'try', 'var', 'env'])
+
+ self.names = []
+ init_possibles = string.ascii_letters + '_$'
+ later_possibles = init_possibles + string.digits
+ for a in init_possibles:
+ if len(self.names) >= MAX_NAMES: break
+ self.names.append(a)
+ for a in init_possibles:
+ for b in later_possibles:
+ if len(self.names) >= MAX_NAMES: break
+ curr = a + b
+ if curr not in INVALID_2: self.names.append(curr)
+ for a in init_possibles:
+ for b in later_possibles:
+ for c in later_possibles:
+ if len(self.names) >= MAX_NAMES: break
+ curr = a + b + c
+ if curr not in INVALID_3: self.names.append(curr)
+ #print >> sys.stderr, self.names
+
+ def minify_shell(self, shell, compress):
+ #print >> sys.stderr, "MINIFY SHELL 1111111111", shell, "\n222222222222222"
+ # Run through js-optimizer.js to find and minify the global symbols
+ # We send it the globals, which it parses at the proper time. JS decides how
+ # to minify all global names, we receive a dictionary back, which is then
+ # used by the function processors
+
+ shell = shell.replace('0.0', '13371337') # avoid uglify doing 0.0 => 0
+
+ # Find all globals in the JS functions code
+ self.globs = [m.group(2) for m in func_sig.finditer(self.js)]
+
+ temp_file = temp_files.get('.minifyglobals.js').name
+ f = open(temp_file, 'w')
+ f.write(shell)
+ f.write('\n')
+ self
+ f.write('// MINIFY_INFO:' + self.serialize())
+ f.close()
+
+ output = subprocess.Popen(self.js_engine + [JS_OPTIMIZER, temp_file, 'minifyGlobals', 'noPrintMetadata'] + (['compress'] if compress else []), stdout=subprocess.PIPE).communicate()[0]
+ assert len(output) > 0 and not output.startswith('Assertion failed'), 'Error in js optimizer: ' + output
+ #print >> sys.stderr, "minified SHELL 3333333333333333", output, "\n44444444444444444444"
+ code, metadata = output.split('// MINIFY_INFO:')
+ self.globs = json.loads(metadata)
+ return code.replace('13371337', '0.0')
+
+
+ def serialize(self):
+ return json.dumps({
+ 'names': self.names,
+ 'globals': self.globs
+ })
+
def run_on_chunk(command):
filename = command[2] # XXX hackish
#print >> sys.stderr, 'running js optimizer command', ' '.join(command), '""""', open(filename).read()
@@ -46,10 +118,25 @@ def run_on_js(filename, passes, js_engine, jcache):
suffix_start = js.find(suffix_marker)
suffix = ''
if suffix_start >= 0:
- suffix = js[suffix_start:js.find('\n', suffix_start)] + '\n'
+ suffix_end = js.find('\n', suffix_start)
+ suffix = js[suffix_start:suffix_end] + '\n'
# if there is metadata, we will run only on the generated functions. If there isn't, we will run on everything.
generated = set(eval(suffix[len(suffix_marker)+1:]))
+ # Find markers
+ start_funcs_marker = '// EMSCRIPTEN_START_FUNCS\n'
+ end_funcs_marker = '// EMSCRIPTEN_END_FUNCS\n'
+ start_funcs = js.find(start_funcs_marker)
+ end_funcs = js.rfind(end_funcs_marker)
+ assert (start_funcs >= 0) == (end_funcs >= 0) == (not not suffix)
+ asm_registerize = 'asm' in passes and 'registerize' in passes
+ if asm_registerize:
+ start_asm_marker = '// EMSCRIPTEN_START_ASM\n'
+ end_asm_marker = '// EMSCRIPTEN_END_ASM\n'
+ start_asm = js.find(start_asm_marker)
+ end_asm = js.rfind(end_asm_marker)
+ assert (start_asm >= 0) == (end_asm >= 0)
+
if not suffix and jcache:
# JCache cannot be used without metadata, since it might reorder stuff, and that's dangerous since only generated can be reordered
# This means jcache does not work after closure compiler runs, for example. But you won't get much benefit from jcache with closure
@@ -57,29 +144,45 @@ def run_on_js(filename, passes, js_engine, jcache):
if DEBUG: print >>sys.stderr, 'js optimizer: no metadata, so disabling jcache'
jcache = False
- # If we process only generated code, find that and save the rest on the side
- func_sig = re.compile('( *)function (_[\w$]+)\(')
if suffix:
- pos = 0
- gen_start = 0
- gen_end = 0
- while 1:
- m = func_sig.search(js, pos)
- if not m: break
- pos = m.end()
- indent = m.group(1)
- ident = m.group(2)
- if ident in generated:
- if not gen_start:
- gen_start = m.start()
- assert gen_start
- gen_end = js.find('\n%s}\n' % indent, m.end()) + (3 + len(indent))
- assert gen_end > gen_start
- pre = js[:gen_start]
- post = js[gen_end:]
+ if not asm_registerize:
+ pre = js[:start_funcs + len(start_funcs_marker)]
+ post = js[end_funcs + len(end_funcs_marker):]
+ js = js[start_funcs + len(start_funcs_marker):end_funcs]
+ if 'asm' not in passes: # can have Module[..] and inlining prevention code, push those to post
+ class Finals:
+ buf = []
+ def process(line):
+ if len(line) > 0 and not line.startswith((' ', 'function', '}')):
+ Finals.buf.append(line)
+ return False
+ return True
+ js = '\n'.join(filter(process, js.split('\n')))
+ post = '\n'.join(Finals.buf) + '\n' + post
+ post = end_funcs_marker + post
+ else:
+ # We need to split out the asm shell as well, for minification
+ pre = js[:start_asm + len(start_asm_marker)]
+ post = js[end_asm:]
+ asm_shell = js[start_asm + len(start_asm_marker):start_funcs + len(start_funcs_marker)] + '''
+EMSCRIPTEN_FUNCS();
+''' + js[end_funcs + len(end_funcs_marker):end_asm + len(end_asm_marker)]
+ js = js[start_funcs + len(start_funcs_marker):end_funcs]
+
+ minifier = Minifier(js, js_engine)
+ asm_shell_pre, asm_shell_post = minifier.minify_shell(asm_shell, 'compress' in passes).split('EMSCRIPTEN_FUNCS();');
+ asm_shell_post = asm_shell_post.replace('});', '})');
+ pre += asm_shell_pre + '\n' + start_funcs_marker
+ post = end_funcs_marker + asm_shell_post + post
+
+ minify_info = minifier.serialize()
+ #if DEBUG: print >> sys.stderr, 'minify info:', minify_info
+ # remove suffix if no longer needed
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]
+ suffix_start = post.find(suffix_marker)
+ suffix_end = post.find('\n', suffix_start)
+ post = post[:suffix_start] + post[suffix_end:]
+
else:
pre = ''
post = ''
@@ -132,6 +235,9 @@ def run_on_js(filename, passes, js_engine, jcache):
f = open(temp_file, 'w')
f.write(chunk)
f.write(suffix_marker)
+ if asm_registerize:
+ f.write('\n')
+ f.write('// MINIFY_INFO:' + minify_info)
f.close()
return temp_file
filenames = [write_chunk(chunks[i], i) for i in range(len(chunks))]
diff --git a/tools/test-js-optimizer-asm-regs-min-output.js b/tools/test-js-optimizer-asm-regs-min-output.js
new file mode 100644
index 00000000..b8088022
--- /dev/null
+++ b/tools/test-js-optimizer-asm-regs-min-output.js
@@ -0,0 +1,36 @@
+function cl(b) {
+ b = b | 0;
+ var c = 0;
+ c = b * b;
+ a(c);
+ i1(b);
+}
+function cl(b) {
+ b = b | 0;
+ var c = 0;
+ c = b * b;
+ a(c);
+ i1(b);
+}
+function cl(b) {
+ b = b | 0;
+ var c = 0;
+ c = b * b;
+ a(c);
+ i1(b);
+}
+function cl(b) {
+ b = b | 0;
+ var c = 0;
+ c = b * b;
+ a(c);
+ i1(b);
+}
+function cl(b) {
+ b = b | 0;
+ var c = 0;
+ c = b * b;
+ a(c);
+ i1(b);
+}
+
diff --git a/tools/test-js-optimizer-asm-regs-min.js b/tools/test-js-optimizer-asm-regs-min.js
new file mode 100644
index 00000000..c126946d
--- /dev/null
+++ b/tools/test-js-optimizer-asm-regs-min.js
@@ -0,0 +1,37 @@
+function collideLocal(x) {
+ x = x | 0;
+ var a = 0;
+ a = x*x;
+ aGlobal(a); // aGlobal needs to be minified into a, but a is used!
+ bGlobal(x);
+}
+function collideLocal(x) {
+ x = x | 0;
+ var i1 = 0;
+ i1 = x*x;
+ aGlobal(i1);
+ bGlobal(x); // bGlobal needs to be minified into i1, but i1 is used!
+}
+function collideLocal(a) {
+ a = a | 0;
+ var x = 0;
+ x = a*a;
+ aGlobal(x); // aGlobal needs to be minified into a, but a is used by a param!
+ bGlobal(a);
+}
+function collideLocal(i1) {
+ i1 = i1 | 0;
+ var x = 0;
+ x = i1*i1;
+ aGlobal(x);
+ bGlobal(i1); // bGlobal needs to be minified into i1, but i1 is used by a param!
+}
+function collideLocal(i1) {
+ i1 = i1 | 0;
+ var a = 0;
+ a = i1*i1;
+ aGlobal(a); // multiple collisions, a and i1
+ bGlobal(i1);
+}
+// EMSCRIPTEN_GENERATED_FUNCTIONS
+// MINIFY_INFO: { "names": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "i1", "cl"], "globals": { "aGlobal": "a", "bGlobal": "i1", "collideLocal": "cl" } }
diff --git a/tools/test-js-optimizer-asm-regs-output.js b/tools/test-js-optimizer-asm-regs-output.js
index 99bccd2e..8c0bd970 100644
--- a/tools/test-js-optimizer-asm-regs-output.js
+++ b/tools/test-js-optimizer-asm-regs-output.js
@@ -38,4 +38,8 @@ function retf() {
}
return +0;
}
+function stackRestore(i1) {
+ i1 = i1 | 0;
+ STACKTOP = i1;
+}
diff --git a/tools/test-js-optimizer-asm-regs.js b/tools/test-js-optimizer-asm-regs.js
index 0afced29..a8b637ce 100644
--- a/tools/test-js-optimizer-asm-regs.js
+++ b/tools/test-js-optimizer-asm-regs.js
@@ -41,5 +41,9 @@ function retf() {
}
// missing final return, need it as a float
}
-// EMSCRIPTEN_GENERATED_FUNCTIONS: ["asm", "_doit", "rett", "ret2t", "retf"]
+function stackRestore(top) {
+ top = top|0;
+ STACKTOP = top;
+}
+// EMSCRIPTEN_GENERATED_FUNCTIONS: ["asm", "_doit", "rett", "ret2t", "retf", "stackRestore"]