diff options
author | Jez Ng <me@jezng.com> | 2013-06-19 11:22:35 -0700 |
---|---|---|
committer | Jez Ng <me@jezng.com> | 2013-06-19 14:15:18 -0700 |
commit | d680f5e81b4bae6cbc46ba04b24c5809679d0bcc (patch) | |
tree | d96043802f514e2bc75a0e458a9aa655a6cb52de | |
parent | 050335fd91a09cde7ccea9f1fc0ecf536918b95a (diff) |
Implement source maps for optimized builds.
-rwxr-xr-x | emcc | 22 | ||||
-rwxr-xr-x | tests/runner.py | 14 | ||||
-rw-r--r-- | tools/eliminator/node_modules/uglify-js/lib/process.js | 34 | ||||
-rwxr-xr-x | tools/source-maps/sourcemapper.js | 106 |
4 files changed, 130 insertions, 46 deletions
@@ -1477,6 +1477,7 @@ try: open(final, 'w').write(src) if DEBUG: save_intermediate('bind') + # TODO: support source maps with js_transform # Apply a source code transformation, if requested if js_transform: shutil.copyfile(final, final + '.tr.js') @@ -1486,6 +1487,8 @@ try: execute(shlex.split(js_transform, posix=posix) + [os.path.abspath(final)]) if DEBUG: save_intermediate('transformed') + js_transform_tempfiles = [final] + # It is useful to run several js optimizer passes together, to save on unneeded unparsing/reparsing js_optimizer_queue = [] def flush_js_optimizer_queue(): @@ -1497,6 +1500,7 @@ try: logging.debug('applying js optimization passes: %s', js_optimizer_queue) final = shared.Building.js_optimizer(final, js_optimizer_queue, jcache, keep_llvm_debug and keep_js_debug) + js_transform_tempfiles.append(final) if DEBUG: save_intermediate('js_opts') else: for name in js_optimizer_queue: @@ -1506,6 +1510,7 @@ try: logging.debug('applying js optimization pass: %s', passes) final = shared.Building.js_optimizer(final, passes, jcache, keep_llvm_debug and keep_js_debug) + js_transform_tempfiles.append(final) save_intermediate(name) js_optimizer_queue = [] @@ -1514,7 +1519,8 @@ try: if DEBUG == '2': # Clean up the syntax a bit - final = shared.Building.js_optimizer(final, [], jcache) + final = shared.Building.js_optimizer(final, [], jcache, + keep_llvm_debug and keep_js_debug) if DEBUG: save_intermediate('pretty') def get_eliminate(): @@ -1532,6 +1538,8 @@ try: flush_js_optimizer_queue() logging.debug('running closure') + # no need to add this to js_transform_tempfiles, because closure and + # keep_js_debug are never simultaneously true final = shared.Building.closure_compiler(final) if DEBUG: save_intermediate('closure') @@ -1579,6 +1587,7 @@ try: src = re.sub('/\* memory initializer \*/ allocate\(([\d,\.concat\(\)\[\]\\n ]+)"i8", ALLOC_NONE, Runtime\.GLOBAL_BASE\)', repl, src, count=1) open(final + '.mem.js', 'w').write(src) final += '.mem.js' + js_transform_tempfiles[-1] = final # simple text substitution preserves comment line number mappings if DEBUG: if os.path.exists(memfile): save_intermediate('meminit') @@ -1586,9 +1595,12 @@ try: else: logging.debug('did not see memory initialization') - def generate_source_map(filename, map_file_base_name, offset=0): + def generate_source_map(map_file_base_name, offset=0): jsrun.run_js(shared.path_from_root('tools', 'source-maps', 'sourcemapper.js'), - shared.NODE_JS, [filename, os.getcwd(), map_file_base_name, str(offset)]) + shared.NODE_JS, js_transform_tempfiles + + ['--sourceRoot', os.getcwd(), + '--mapFileBaseName', map_file_base_name, + '--offset', str(offset)]) # If we were asked to also generate HTML, do that if final_suffix == 'html': @@ -1601,7 +1613,7 @@ try: re.DOTALL) if match is None: raise RuntimeError('Could not find script insertion point') - generate_source_map(final, target, match.group().count('\n')) + generate_source_map(target, match.group().count('\n')) html.write(shell.replace('{{{ SCRIPT_CODE }}}', open(final).read())) else: # Compress the main code @@ -1672,7 +1684,7 @@ try: from tools.split import split_javascript_file split_javascript_file(final, unsuffixed(target), split_js_file) else: - if keep_llvm_debug and keep_js_debug: generate_source_map(final, target) + if keep_llvm_debug and keep_js_debug: generate_source_map(target) # copy final JS to output shutil.move(final, target) diff --git a/tests/runner.py b/tests/runner.py index 63f5048a..1697294d 100755 --- a/tests/runner.py +++ b/tests/runner.py @@ -9585,21 +9585,21 @@ def process(filename): assert 'Assertion failed' in str(e), str(e) def test_source_map(self): + if Settings.USE_TYPED_ARRAYS != 2: return self.skip("doesn't pass without typed arrays") if '-g' not in Building.COMPILER_TEST_OPTS: Building.COMPILER_TEST_OPTS.append('-g') - if self.emcc_args is not None: - if '-O1' in self.emcc_args or '-O2' in self.emcc_args: return self.skip('optimizations remove LLVM debug info') src = ''' #include <stdio.h> #include <assert.h> - int foo() { - return 1; // line 6 + __attribute__((noinline)) int foo() { + printf("hi"); // line 6 + return 1; // line 7 } int main() { - int i = foo(); // line 10 - return 0; // line 11 + printf("%d", foo()); // line 11 + return 0; // line 12 } ''' @@ -9621,7 +9621,7 @@ def process(filename): self.assertIdentical(src_filename, m['source']) seen_lines.add(m['originalLine']) # ensure that all the 'meaningful' lines in the original code get mapped - assert seen_lines.issuperset([6, 10, 11]) + assert seen_lines.issuperset([6, 7, 11, 12]) self.build(src, dirname, src_filename, post_build=(None,post)) diff --git a/tools/eliminator/node_modules/uglify-js/lib/process.js b/tools/eliminator/node_modules/uglify-js/lib/process.js index 660001aa..39ccde37 100644 --- a/tools/eliminator/node_modules/uglify-js/lib/process.js +++ b/tools/eliminator/node_modules/uglify-js/lib/process.js @@ -65,6 +65,7 @@ function NodeWithLine(str, line) { NodeWithLine.prototype = new String(); NodeWithLine.prototype.toString = function() { return this.str; } +NodeWithLine.prototype.valueOf = function() { return this.str; } NodeWithLine.prototype.lineComment = function() { return " // @line " + this.line; } // XXX ugly hack @@ -473,7 +474,10 @@ function gen_code(ast, options) { a.push(m[2] + "e-" + (m[1].length + m[2].length), str.substr(str.indexOf("."))); } - return best_of(a); + var best = best_of(a); + if (options.debug && this[0].start) + return new NodeWithLine(best, this[0].start.line); + return best; }; var w = ast_walker(); @@ -499,7 +503,16 @@ function gen_code(ast, options) { }, "block": make_block, "var": function(defs) { - return "var " + add_commas(MAP(defs, make_1vardef)) + ";"; + var s = "var " + add_commas(MAP(defs, make_1vardef)) + ";"; + if (options.debug) { + // hack: we don't support mapping one optimized line to more than one + // generated line, so in case of multiple comma-separated var definitions, + // just take the first + if (defs[0][1] && defs[0][1][0] && defs[0][1][0].start) { + return s + (new NodeWithLine(s, defs[0][1][0].start.line)).lineComment(); + } + } + return s; }, "const": function(defs) { return "const " + add_commas(MAP(defs, make_1vardef)) + ";"; @@ -555,8 +568,8 @@ function gen_code(ast, options) { if (op && op !== true) op += "="; else op = "="; var s = add_spaces([ make(lvalue), op, parenthesize(rvalue, "seq") ]); - if (options.debug && lvalue[0].start) - return new NodeWithLine(s, lvalue[0].start.line); + if (options.debug && this[0].start) + return new NodeWithLine(s, this[0].start.line); return s; }, "dot": function(expr) { @@ -574,9 +587,12 @@ function gen_code(ast, options) { var f = make(func); if (needs_parens(func)) f = "(" + f + ")"; - return f + "(" + add_commas(MAP(args, function(expr){ + var str = f + "(" + add_commas(MAP(args, function(expr){ return parenthesize(expr, "seq"); })) + ")"; + if (options.debug && this[0].start) + return new NodeWithLine(str, this[0].start.line) + return str; }, "function": make_function, "defun": make_function, @@ -613,7 +629,7 @@ function gen_code(ast, options) { var out = [ "return" ]; var str = make(expr); if (expr != null) out.push(str); - return add_spaces(out) + ";" + str.lineComment(); + return add_spaces(out) + ";" + (str ? str.lineComment() : ''); }, "binary": function(operator, lvalue, rvalue) { var left = make(lvalue), right = make(rvalue); @@ -633,7 +649,11 @@ function gen_code(ast, options) { && rvalue[0] == "regexp" && /^script/i.test(rvalue[1])) { right = " " + right; } - return add_spaces([ left, operator, right ]); + var tok = this[0]; + var str = add_spaces([ left, operator, right ]); + if (options.debug && tok.start) + return new NodeWithLine(str, tok.start.line); + return str; }, "unary-prefix": function(operator, expr) { var val = make(expr); diff --git a/tools/source-maps/sourcemapper.js b/tools/source-maps/sourcemapper.js index c7021a0f..ba993aa1 100755 --- a/tools/source-maps/sourcemapper.js +++ b/tools/source-maps/sourcemapper.js @@ -2,6 +2,9 @@ "use strict"; +var fs = require('fs'); +var path = require('path'); + function countLines(s) { var count = 0; for (var i = 0, l = s.length; i < l; i ++) { @@ -63,21 +66,39 @@ function extractComments(source, commentHandler) { } } -function generateMap(fileName, sourceRoot, mapFileBaseName, generatedLineOffset) { - var fs = require('fs'); - var path = require('path'); +function getMappings(source) { + // generatedLineNumber -> { originalLineNumber, originalFileName } + var mappings = {}; + extractComments(source, function(content, generatedLineNumber) { + var matches = /@line (\d+)(?: "([^"]*)")?/.exec(content); + if (matches === null) return; + var originalFileName = matches[2]; + mappings[generatedLineNumber] = { + originalLineNumber: parseInt(matches[1], 10), + originalFileName: originalFileName + } + }); + return mappings; +} + +function generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset) { var SourceMapGenerator = require('source-map').SourceMapGenerator; var generator = new SourceMapGenerator({ file: mapFileBaseName }); - var generatedSource = fs.readFileSync(fileName, 'utf-8'); var seenFiles = Object.create(null); - extractComments(generatedSource, function(content, generatedLineNumber) { - var matches = /@line (\d+) "([^"]*)"/.exec(content); - if (matches === null) return; - var originalLineNumber = parseInt(matches[1], 10); - var originalFileName = matches[2]; + for (var generatedLineNumber in mappings) { + var generatedLineNumber = parseInt(generatedLineNumber, 10); + var mapping = mappings[generatedLineNumber]; + var originalFileName = mapping.originalFileName; + generator.addMapping({ + generated: { line: generatedLineNumber + generatedLineOffset, column: 0 }, + original: { line: mapping.originalLineNumber, column: 0 }, + source: originalFileName + }); + // we could call setSourceContent repeatedly, but readFileSync is slow, so + // avoid doing it unnecessarily if (!(originalFileName in seenFiles)) { seenFiles[originalFileName] = true; var rootedPath = originalFileName[0] === path.sep ? @@ -89,33 +110,64 @@ function generateMap(fileName, sourceRoot, mapFileBaseName, generatedLineOffset) " at " + rootedPath); } } + } - generator.addMapping({ - generated: { line: generatedLineNumber + generatedLineOffset, column: 0 }, - original: { line: originalLineNumber, column: 0 }, - source: originalFileName - }); - }); - - var mapFileName = mapFileBaseName + '.map'; - fs.writeFileSync(mapFileName, generator.toString()); + fs.writeFileSync(mapFileBaseName + '.map', generator.toString()); +} - var lastLine = generatedSource.slice(generatedSource.lastIndexOf('\n')); +function appendMappingURL(fileName, source, mapFileName) { + var lastLine = source.slice(source.lastIndexOf('\n')); if (!/sourceMappingURL/.test(lastLine)) fs.appendFileSync(fileName, '//@ sourceMappingURL=' + path.basename(mapFileName)); } +function parseArgs(args) { + var rv = { _: [] }; // unflagged args go into `_`; similar to the optimist library + for (var i = 0; i < args.length; i++) { + if (/^--/.test(args[i])) rv[args[i].slice(2)] = args[++i]; + else rv._.push(args[i]); + } + return rv; +} + if (require.main === module) { if (process.argv.length < 3) { - console.log('Usage: ./sourcemapper.js <filename> <source root (default: .)> ' + - '<map file basename (default: filename)>' + - '<generated line offset (default: 0)>'); + console.log('Usage: ./sourcemapper.js <original js> <optimized js file ...> \\\n' + + '\t--sourceRoot <default "."> \\\n' + + '\t--mapFileBaseName <default `filename`> \\\n' + + '\t--offset <default 0>'); process.exit(1); } else { - var sourceRoot = process.argv.length > 3 ? process.argv[3] : "."; - var mapFileBaseName = process.argv.length > 4 ? process.argv[4] : process.argv[2]; - var generatedLineOffset = process.argv.length > 5 ? - parseInt(process.argv[5], 10) : 0; - generateMap(process.argv[2], sourceRoot, mapFileBaseName, generatedLineOffset); + var opts = parseArgs(process.argv.slice(2)); + var fileName = opts._[0]; + var sourceRoot = opts.sourceRoot ? opts.sourceRoot : "."; + var mapFileBaseName = opts.mapFileBaseName ? opts.mapFileBaseName : fileName; + var generatedLineOffset = opts.offset ? parseInt(opts.offset, 10) : 0; + + var generatedSource = fs.readFileSync(fileName, 'utf-8'); + var source = generatedSource; + var mappings = getMappings(generatedSource); + for (var i = 1, l = opts._.length; i < l; i ++) { + var optimizedSource = fs.readFileSync(opts._[i], 'utf-8') + var optimizedMappings = getMappings(optimizedSource); + var newMappings = {}; + // uglify processes the code between EMSCRIPTEN_START_FUNCS and + // EMSCRIPTEN_END_FUNCS, so its line number maps are relative to those + // markers. we correct for that here. + var startFuncsLineNumber = countLines( + source.slice(0, source.indexOf('// EMSCRIPTEN_START_FUNCS'))) + 2; + for (var line in optimizedMappings) { + var originalLineNumber = optimizedMappings[line].originalLineNumber + startFuncsLineNumber; + if (originalLineNumber in mappings) { + newMappings[line] = mappings[originalLineNumber]; + } + } + mappings = newMappings; + source = optimizedSource; + } + + generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset); + appendMappingURL(opts._[opts._.length - 1], generatedSource, + opts.mapFileBaseName + '.map'); } } |