Skip to content

Commit d680f5e

Browse files
committed
Implement source maps for optimized builds.
1 parent 050335f commit d680f5e

File tree

4 files changed

+130
-46
lines changed

4 files changed

+130
-46
lines changed

emcc

+17-5
Original file line numberDiff line numberDiff line change
@@ -1477,6 +1477,7 @@ try:
14771477
open(final, 'w').write(src)
14781478
if DEBUG: save_intermediate('bind')
14791479

1480+
# TODO: support source maps with js_transform
14801481
# Apply a source code transformation, if requested
14811482
if js_transform:
14821483
shutil.copyfile(final, final + '.tr.js')
@@ -1486,6 +1487,8 @@ try:
14861487
execute(shlex.split(js_transform, posix=posix) + [os.path.abspath(final)])
14871488
if DEBUG: save_intermediate('transformed')
14881489

1490+
js_transform_tempfiles = [final]
1491+
14891492
# It is useful to run several js optimizer passes together, to save on unneeded unparsing/reparsing
14901493
js_optimizer_queue = []
14911494
def flush_js_optimizer_queue():
@@ -1497,6 +1500,7 @@ try:
14971500
logging.debug('applying js optimization passes: %s', js_optimizer_queue)
14981501
final = shared.Building.js_optimizer(final, js_optimizer_queue, jcache,
14991502
keep_llvm_debug and keep_js_debug)
1503+
js_transform_tempfiles.append(final)
15001504
if DEBUG: save_intermediate('js_opts')
15011505
else:
15021506
for name in js_optimizer_queue:
@@ -1506,6 +1510,7 @@ try:
15061510
logging.debug('applying js optimization pass: %s', passes)
15071511
final = shared.Building.js_optimizer(final, passes, jcache,
15081512
keep_llvm_debug and keep_js_debug)
1513+
js_transform_tempfiles.append(final)
15091514
save_intermediate(name)
15101515
js_optimizer_queue = []
15111516

@@ -1514,7 +1519,8 @@ try:
15141519

15151520
if DEBUG == '2':
15161521
# Clean up the syntax a bit
1517-
final = shared.Building.js_optimizer(final, [], jcache)
1522+
final = shared.Building.js_optimizer(final, [], jcache,
1523+
keep_llvm_debug and keep_js_debug)
15181524
if DEBUG: save_intermediate('pretty')
15191525

15201526
def get_eliminate():
@@ -1532,6 +1538,8 @@ try:
15321538
flush_js_optimizer_queue()
15331539

15341540
logging.debug('running closure')
1541+
# no need to add this to js_transform_tempfiles, because closure and
1542+
# keep_js_debug are never simultaneously true
15351543
final = shared.Building.closure_compiler(final)
15361544
if DEBUG: save_intermediate('closure')
15371545

@@ -1579,16 +1587,20 @@ try:
15791587
src = re.sub('/\* memory initializer \*/ allocate\(([\d,\.concat\(\)\[\]\\n ]+)"i8", ALLOC_NONE, Runtime\.GLOBAL_BASE\)', repl, src, count=1)
15801588
open(final + '.mem.js', 'w').write(src)
15811589
final += '.mem.js'
1590+
js_transform_tempfiles[-1] = final # simple text substitution preserves comment line number mappings
15821591
if DEBUG:
15831592
if os.path.exists(memfile):
15841593
save_intermediate('meminit')
15851594
logging.debug('wrote memory initialization to %s' % memfile)
15861595
else:
15871596
logging.debug('did not see memory initialization')
15881597

1589-
def generate_source_map(filename, map_file_base_name, offset=0):
1598+
def generate_source_map(map_file_base_name, offset=0):
15901599
jsrun.run_js(shared.path_from_root('tools', 'source-maps', 'sourcemapper.js'),
1591-
shared.NODE_JS, [filename, os.getcwd(), map_file_base_name, str(offset)])
1600+
shared.NODE_JS, js_transform_tempfiles +
1601+
['--sourceRoot', os.getcwd(),
1602+
'--mapFileBaseName', map_file_base_name,
1603+
'--offset', str(offset)])
15921604

15931605
# If we were asked to also generate HTML, do that
15941606
if final_suffix == 'html':
@@ -1601,7 +1613,7 @@ try:
16011613
re.DOTALL)
16021614
if match is None:
16031615
raise RuntimeError('Could not find script insertion point')
1604-
generate_source_map(final, target, match.group().count('\n'))
1616+
generate_source_map(target, match.group().count('\n'))
16051617
html.write(shell.replace('{{{ SCRIPT_CODE }}}', open(final).read()))
16061618
else:
16071619
# Compress the main code
@@ -1672,7 +1684,7 @@ try:
16721684
from tools.split import split_javascript_file
16731685
split_javascript_file(final, unsuffixed(target), split_js_file)
16741686
else:
1675-
if keep_llvm_debug and keep_js_debug: generate_source_map(final, target)
1687+
if keep_llvm_debug and keep_js_debug: generate_source_map(target)
16761688

16771689
# copy final JS to output
16781690
shutil.move(final, target)

tests/runner.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -9585,21 +9585,21 @@ def process(filename):
95859585
assert 'Assertion failed' in str(e), str(e)
95869586

95879587
def test_source_map(self):
9588+
if Settings.USE_TYPED_ARRAYS != 2: return self.skip("doesn't pass without typed arrays")
95889589
if '-g' not in Building.COMPILER_TEST_OPTS: Building.COMPILER_TEST_OPTS.append('-g')
9589-
if self.emcc_args is not None:
9590-
if '-O1' in self.emcc_args or '-O2' in self.emcc_args: return self.skip('optimizations remove LLVM debug info')
95919590

95929591
src = '''
95939592
#include <stdio.h>
95949593
#include <assert.h>
95959594

9596-
int foo() {
9597-
return 1; // line 6
9595+
__attribute__((noinline)) int foo() {
9596+
printf("hi"); // line 6
9597+
return 1; // line 7
95989598
}
95999599

96009600
int main() {
9601-
int i = foo(); // line 10
9602-
return 0; // line 11
9601+
printf("%d", foo()); // line 11
9602+
return 0; // line 12
96039603
}
96049604
'''
96059605

@@ -9621,7 +9621,7 @@ def post(filename):
96219621
self.assertIdentical(src_filename, m['source'])
96229622
seen_lines.add(m['originalLine'])
96239623
# ensure that all the 'meaningful' lines in the original code get mapped
9624-
assert seen_lines.issuperset([6, 10, 11])
9624+
assert seen_lines.issuperset([6, 7, 11, 12])
96259625

96269626
self.build(src, dirname, src_filename, post_build=(None,post))
96279627

tools/eliminator/node_modules/uglify-js/lib/process.js

+27-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/source-maps/sourcemapper.js

+79-27
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
"use strict";
44

5+
var fs = require('fs');
6+
var path = require('path');
7+
58
function countLines(s) {
69
var count = 0;
710
for (var i = 0, l = s.length; i < l; i ++) {
@@ -63,21 +66,39 @@ function extractComments(source, commentHandler) {
6366
}
6467
}
6568

66-
function generateMap(fileName, sourceRoot, mapFileBaseName, generatedLineOffset) {
67-
var fs = require('fs');
68-
var path = require('path');
69+
function getMappings(source) {
70+
// generatedLineNumber -> { originalLineNumber, originalFileName }
71+
var mappings = {};
72+
extractComments(source, function(content, generatedLineNumber) {
73+
var matches = /@line (\d+)(?: "([^"]*)")?/.exec(content);
74+
if (matches === null) return;
75+
var originalFileName = matches[2];
76+
mappings[generatedLineNumber] = {
77+
originalLineNumber: parseInt(matches[1], 10),
78+
originalFileName: originalFileName
79+
}
80+
});
81+
return mappings;
82+
}
83+
84+
function generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset) {
6985
var SourceMapGenerator = require('source-map').SourceMapGenerator;
7086

7187
var generator = new SourceMapGenerator({ file: mapFileBaseName });
72-
var generatedSource = fs.readFileSync(fileName, 'utf-8');
7388
var seenFiles = Object.create(null);
7489

75-
extractComments(generatedSource, function(content, generatedLineNumber) {
76-
var matches = /@line (\d+) "([^"]*)"/.exec(content);
77-
if (matches === null) return;
78-
var originalLineNumber = parseInt(matches[1], 10);
79-
var originalFileName = matches[2];
90+
for (var generatedLineNumber in mappings) {
91+
var generatedLineNumber = parseInt(generatedLineNumber, 10);
92+
var mapping = mappings[generatedLineNumber];
93+
var originalFileName = mapping.originalFileName;
94+
generator.addMapping({
95+
generated: { line: generatedLineNumber + generatedLineOffset, column: 0 },
96+
original: { line: mapping.originalLineNumber, column: 0 },
97+
source: originalFileName
98+
});
8099

100+
// we could call setSourceContent repeatedly, but readFileSync is slow, so
101+
// avoid doing it unnecessarily
81102
if (!(originalFileName in seenFiles)) {
82103
seenFiles[originalFileName] = true;
83104
var rootedPath = originalFileName[0] === path.sep ?
@@ -89,33 +110,64 @@ function generateMap(fileName, sourceRoot, mapFileBaseName, generatedLineOffset)
89110
" at " + rootedPath);
90111
}
91112
}
113+
}
92114

93-
generator.addMapping({
94-
generated: { line: generatedLineNumber + generatedLineOffset, column: 0 },
95-
original: { line: originalLineNumber, column: 0 },
96-
source: originalFileName
97-
});
98-
});
99-
100-
var mapFileName = mapFileBaseName + '.map';
101-
fs.writeFileSync(mapFileName, generator.toString());
115+
fs.writeFileSync(mapFileBaseName + '.map', generator.toString());
116+
}
102117

103-
var lastLine = generatedSource.slice(generatedSource.lastIndexOf('\n'));
118+
function appendMappingURL(fileName, source, mapFileName) {
119+
var lastLine = source.slice(source.lastIndexOf('\n'));
104120
if (!/sourceMappingURL/.test(lastLine))
105121
fs.appendFileSync(fileName, '//@ sourceMappingURL=' + path.basename(mapFileName));
106122
}
107123

124+
function parseArgs(args) {
125+
var rv = { _: [] }; // unflagged args go into `_`; similar to the optimist library
126+
for (var i = 0; i < args.length; i++) {
127+
if (/^--/.test(args[i])) rv[args[i].slice(2)] = args[++i];
128+
else rv._.push(args[i]);
129+
}
130+
return rv;
131+
}
132+
108133
if (require.main === module) {
109134
if (process.argv.length < 3) {
110-
console.log('Usage: ./sourcemapper.js <filename> <source root (default: .)> ' +
111-
'<map file basename (default: filename)>' +
112-
'<generated line offset (default: 0)>');
135+
console.log('Usage: ./sourcemapper.js <original js> <optimized js file ...> \\\n' +
136+
'\t--sourceRoot <default "."> \\\n' +
137+
'\t--mapFileBaseName <default `filename`> \\\n' +
138+
'\t--offset <default 0>');
113139
process.exit(1);
114140
} else {
115-
var sourceRoot = process.argv.length > 3 ? process.argv[3] : ".";
116-
var mapFileBaseName = process.argv.length > 4 ? process.argv[4] : process.argv[2];
117-
var generatedLineOffset = process.argv.length > 5 ?
118-
parseInt(process.argv[5], 10) : 0;
119-
generateMap(process.argv[2], sourceRoot, mapFileBaseName, generatedLineOffset);
141+
var opts = parseArgs(process.argv.slice(2));
142+
var fileName = opts._[0];
143+
var sourceRoot = opts.sourceRoot ? opts.sourceRoot : ".";
144+
var mapFileBaseName = opts.mapFileBaseName ? opts.mapFileBaseName : fileName;
145+
var generatedLineOffset = opts.offset ? parseInt(opts.offset, 10) : 0;
146+
147+
var generatedSource = fs.readFileSync(fileName, 'utf-8');
148+
var source = generatedSource;
149+
var mappings = getMappings(generatedSource);
150+
for (var i = 1, l = opts._.length; i < l; i ++) {
151+
var optimizedSource = fs.readFileSync(opts._[i], 'utf-8')
152+
var optimizedMappings = getMappings(optimizedSource);
153+
var newMappings = {};
154+
// uglify processes the code between EMSCRIPTEN_START_FUNCS and
155+
// EMSCRIPTEN_END_FUNCS, so its line number maps are relative to those
156+
// markers. we correct for that here.
157+
var startFuncsLineNumber = countLines(
158+
source.slice(0, source.indexOf('// EMSCRIPTEN_START_FUNCS'))) + 2;
159+
for (var line in optimizedMappings) {
160+
var originalLineNumber = optimizedMappings[line].originalLineNumber + startFuncsLineNumber;
161+
if (originalLineNumber in mappings) {
162+
newMappings[line] = mappings[originalLineNumber];
163+
}
164+
}
165+
mappings = newMappings;
166+
source = optimizedSource;
167+
}
168+
169+
generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset);
170+
appendMappingURL(opts._[opts._.length - 1], generatedSource,
171+
opts.mapFileBaseName + '.map');
120172
}
121173
}

0 commit comments

Comments
 (0)