Skip to content

Commit 4a047e7

Browse files
authored
Merge pull request #306 from fitzgen/wasm
Use WebAssembly to speed up SourceMapConsumer
2 parents 264fcb4 + f878008 commit 4a047e7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2968
-26673
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
*.log
33
.idea
44
node_modules/*
5+
build/
6+
package-lock.json
7+
bench/*.svg

.travis.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ language: node_js
33
sudo: false
44

55
node_js:
6-
- "0.10"
7-
- "0.12"
8-
- "4"
9-
- "5"
10-
- "6"
6+
- "8"
7+
- "9"
118

129
cache:
1310
directories:

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Change Log
22

3+
## 0.7.0
4+
5+
* `SourceMapConsumer` now uses WebAssembly, and is **much** faster!
6+
7+
* **Breaking change:** `new SourceMapConsumer` now returns a `Promise` object
8+
that resolves to the newly constructed `SourceMapConsumer` instance, rather
9+
than returning the new instance immediately.
10+
11+
* **Breaking change:** when you're done using a `SourceMapConsumer` instance,
12+
you must call `SourceMapConsumer.prototype.destroy` on it. After calling
13+
`destroy`, you must not use the instance again.
14+
15+
* **Breaking change:** `SourceMapConsumer` used to be able to handle lines,
16+
columns numbers and source and name indices up to `2^53 - 1` (aka
17+
`Number.MAX_SAFE_INTEGER`). It can now only handle them up to `2^32 - 1`.
18+
19+
* **Breaking change:** The `source-map` library now uses modern ECMAScript-isms:
20+
`let`, arrow functions, `async`, etc. Use Babel to compile it down to
21+
ECMAScript 5 if you need to support older JavaScript environments.
22+
323
## 0.5.6
424

525
* Fix for regression when people were using numbers as names in source maps. See

CONTRIBUTING.md

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ very appreciated.
1616
- [Submitting Pull Requests](#submitting-pull-requests)
1717
- [Running Tests](#running-tests)
1818
- [Writing New Tests](#writing-new-tests)
19+
- [Updating the `lib/mappings.wasm` WebAssembly Module](#updating-the-libmappingswasm-webassembly-module)
1920

2021
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
2122

@@ -51,17 +52,6 @@ This will create the following files:
5152

5253
* `dist/source-map.js` - The plain browser build.
5354

54-
* `dist/source-map.min.js` - The minified browser build.
55-
56-
* `dist/source-map.min.js.map` - The source map for the minified browser build.
57-
58-
* `dist/source-map.debug.js` - The debug browser build.
59-
60-
* `dist/source-map.debug.js.map` - The source map for the debug browser build.
61-
62-
* `dist/test/*` - These are the test files built for running as xpcshell unit
63-
tests within mozilla-central.
64-
6555
## Submitting Pull Requests
6656

6757
Make sure that tests pass locally before creating a pull request.
@@ -122,3 +112,51 @@ can use as well:
122112
```js
123113
var util = require('./util');
124114
```
115+
116+
## Updating the `lib/mappings.wasm` WebAssembly Module
117+
118+
Ensure that you have the Rust toolchain installed:
119+
120+
```
121+
$ curl https://sh.rustup.rs -sSf | sh
122+
```
123+
124+
The `wasm32-unknown-unknown` target is nightly-only at the time of writing. Use
125+
`rustup` to ensure you have it installed:
126+
127+
```
128+
$ rustup toolchain install nightly
129+
$ rustup target add wasm32-unknown-unknown --toolchain nightly
130+
```
131+
132+
Next, clone the Rust source used to create `lib/mappings.wasm`:
133+
134+
```
135+
$ git clone https://github.com/fitzgen/source-map-mappings.git
136+
$ cd source-map-mappings/
137+
```
138+
139+
Make sure the crate's tests pass:
140+
141+
```
142+
$ cargo test
143+
```
144+
145+
Build Rust crate as a `.wasm` file:
146+
147+
```
148+
$ cd source-map-mappings-wasm-api/
149+
$ cargo build --release --target wasm32-unknown-unknown
150+
```
151+
152+
The resulting `wasm` file will be located at
153+
`source-map-mappings-c-api/target/wasm32-unknown-unknown/release/source_map_mappings.wasm`.
154+
155+
Finally, to minimize its size, run `wasm-gc` on it and output the minimified
156+
`.wasm` file into this library's `lib/mappings.wasm`:
157+
158+
```
159+
$ cargo install --git https://github.com/alexcrichton/wasm-gc # If you don't already have it.
160+
$ wasm-gc target/wasm32-unknown-unknown/release/source_map_mappings.wasm \\
161+
/path/to/mozilla/source-map/lib/mappings.wasm
162+
```

README.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ This is a library to generate and consume the source map format
3232
- [With SourceMapGenerator (low level API)](#with-sourcemapgenerator-low-level-api)
3333
- [API](#api)
3434
- [SourceMapConsumer](#sourcemapconsumer)
35+
- [SourceMapConsumer.initialize(options)](#sourcemapconsumerinitializeoptions)
3536
- [new SourceMapConsumer(rawSourceMap)](#new-sourcemapconsumerrawsourcemap)
37+
- [SourceMapConsumer.prototype.destroy()](#sourcemapconsumerprototypedestroy)
3638
- [SourceMapConsumer.prototype.computeColumnSpans()](#sourcemapconsumerprototypecomputecolumnspans)
3739
- [SourceMapConsumer.prototype.originalPositionFor(generatedPosition)](#sourcemapconsumerprototypeoriginalpositionforgeneratedposition)
3840
- [SourceMapConsumer.prototype.generatedPositionFor(originalPosition)](#sourcemapconsumerprototypegeneratedpositionfororiginalposition)
@@ -67,7 +69,7 @@ This is a library to generate and consume the source map format
6769
### Consuming a source map
6870

6971
```js
70-
var rawSourceMap = {
72+
const rawSourceMap = {
7173
version: 3,
7274
file: 'min.js',
7375
names: ['bar', 'baz', 'n'],
@@ -76,7 +78,7 @@ var rawSourceMap = {
7678
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
7779
};
7880

79-
var smc = new SourceMapConsumer(rawSourceMap);
81+
const smc = await new SourceMapConsumer(rawSourceMap);
8082

8183
console.log(smc.sources);
8284
// [ 'http://example.com/www/js/one.js',
@@ -101,6 +103,9 @@ console.log(smc.generatedPositionFor({
101103
smc.eachMapping(function (m) {
102104
// ...
103105
});
106+
107+
// Free the SourceMapConsumer's manually managed wasm data.
108+
smc.destroy();
104109
```
105110

106111
### Generating a source map
@@ -182,10 +187,27 @@ const sourceMap = require("devtools/toolkit/sourcemap/source-map.js");
182187

183188
### SourceMapConsumer
184189

185-
A SourceMapConsumer instance represents a parsed source map which we can query
190+
A `SourceMapConsumer` instance represents a parsed source map which we can query
186191
for information about the original file positions by giving it a file position
187192
in the generated source.
188193

194+
#### SourceMapConsumer.initialize(options)
195+
196+
When using `SourceMapConsumer` outside of node.js, for example on the Web, it
197+
needs to know from what URL to load `lib/mappings.wasm`. You must inform it by
198+
calling `initialize` before constructing any `SourceMapConsumer`s.
199+
200+
The options object has the following properties:
201+
202+
* `"lib/mappings.wasm"`: A `String` containing the URL of the
203+
`lib/mappings.wasm` file.
204+
205+
```js
206+
sourceMap.SourceMapConsumer.initialize({
207+
"lib/mappings.wasm": "https://example.com/source-map/lib/mappings.wasm"
208+
});
209+
```
210+
189211
#### new SourceMapConsumer(rawSourceMap)
190212

191213
The only parameter is the raw source map (either as a string which can be
@@ -207,8 +229,23 @@ following attributes:
207229

208230
* `file`: Optional. The generated filename this source map is associated with.
209231

232+
The promise of the constructed souce map consumer is returned.
233+
234+
When the `SourceMapConsumer` will no longer be used anymore, you must call its
235+
`destroy` method.
236+
237+
```js
238+
const consumer = await new sourceMap.SourceMapConsumer(rawSourceMapJsonData);
239+
doStuffWith(consumer);
240+
consumer.destroy();
241+
```
242+
243+
#### SourceMapConsumer.prototype.destroy()
244+
245+
Free this source map consumer's associated wasm data that is manually-managed.
246+
210247
```js
211-
var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData);
248+
consumer.destroy();
212249
```
213250

214251
#### SourceMapConsumer.prototype.computeColumnSpans()
@@ -239,7 +276,6 @@ consumer.allGeneratedPositionsFor({ line: 2, source: "foo.coffee" })
239276
// { line: 2,
240277
// column: 20,
241278
// lastColumn: Infinity } ]
242-
243279
```
244280

245281
#### SourceMapConsumer.prototype.originalPositionFor(generatedPosition)
@@ -579,9 +615,9 @@ Creates a SourceNode from generated code and a SourceMapConsumer.
579615
should be relative to.
580616

581617
```js
582-
var consumer = new SourceMapConsumer(fs.readFileSync("path/to/my-file.js.map", "utf8"));
583-
var node = SourceNode.fromStringWithSourceMap(fs.readFileSync("path/to/my-file.js"),
584-
consumer);
618+
const consumer = await new SourceMapConsumer(fs.readFileSync("path/to/my-file.js.map", "utf8"));
619+
onst node = SourceNode.fromStringWithSourceMap(fs.readFileSync("path/to/my-file.js"),
620+
consumer);
585621
```
586622

587623
#### SourceNode.prototype.add(chunk)

bench/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.csv

bench/README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
# Benchmarking
22

3-
This directory contains helpers for benchmarking (1) parsing, and (2) generating
4-
source maps.
3+
This directory contains helpers for benchmarking the `mozilla/source-map`
4+
library.
55

66
Ensure that you have built the library, as these benchmarks rely on
77
`dist/source-map.js`. See the main README.md for detais on building.
88

9-
## Running Within a Browser
9+
Run a local webserver from the root of the repository:
1010

11-
Open `bench.html` in a browser and click on the appropriate button.
11+
```
12+
$ cd source-map/
13+
$ python -m SimpleHTTPServer
14+
```
1215

13-
## Running with a JS Shell
16+
Open
17+
[http://localhost:8000/bench/bench.html](http://localhost:8000/bench/bench.html)
18+
in your browser.
1419

15-
Run `$JS_SHELL bench-shell-bindings.js`.
20+
Open `bench.html` in a browser and click on the appropriate button.

bench/angular-min-source-map.js

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bench/bench-dom-bindings.js

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,94 @@
1+
sourceMap.SourceMapConsumer.initialize({
2+
"lib/mappings.wasm": "../lib/mappings.wasm",
3+
});
4+
5+
function bindRange(labelId, updater) {
6+
const label = document.getElementById(labelId);
7+
const input = label.querySelector("input");
8+
9+
input.addEventListener("input", e => {
10+
e.preventDefault();
11+
updater(input.value);
12+
});
13+
14+
updater(input.value);
15+
}
16+
17+
bindRange("warm-up-iters", input => {
18+
const value = parseInt(input, 10);
19+
WARM_UP_ITERATIONS = value;
20+
});
21+
22+
bindRange("bench-iters", input => {
23+
const value = parseInt(input, 10);
24+
BENCH_ITERATIONS = value;
25+
});
26+
27+
const whichMap = document.getElementById("input-map");
28+
const multiplyBy = document.getElementById("multiply-size-by");
29+
30+
var testSourceMap = SCALA_JS_RUNTIME_SOURCE_MAP;
31+
32+
const updateTestSourceMap = () => {
33+
const origMap = window[whichMap.value];
34+
testSourceMap = JSON.parse(JSON.stringify(origMap));
35+
36+
const factor = parseInt(multiplyBy.value, 10);
37+
if (factor === 1) {
38+
return;
39+
}
40+
41+
const mappings = new Array(factor);
42+
mappings.fill(origMap.mappings);
43+
testSourceMap.mappings = mappings.join(";");
44+
45+
for (let i = 0; i < factor; i++) {
46+
testSourceMap.sources.splice(testSourceMap.sources.length, 0, ...origMap.sources);
47+
testSourceMap.names.splice(testSourceMap.names.length, 0, ...origMap.names);
48+
}
49+
};
50+
updateTestSourceMap();
51+
52+
whichMap.addEventListener("input", e => {
53+
e.preventDefault();
54+
updateTestSourceMap();
55+
});
56+
57+
multiplyBy.addEventListener("input", e => {
58+
e.preventDefault();
59+
updateTestSourceMap();
60+
});
61+
62+
var implAndBrowser = "<unknown>";
63+
64+
const implAndBrowserInput = document.getElementById("impl-and-browser");
65+
const updateImplAndBrowser = () => {
66+
implAndBrowser = implAndBrowserInput.value;
67+
};
68+
implAndBrowserInput.addEventListener("input", updateImplAndBrowser);
69+
updateImplAndBrowser();
70+
71+
172
// Run a benchmark when the given button is clicked and display results in the
273
// given element.
3-
function benchOnClick(button, results, bencher) {
4-
button.addEventListener("click", function (e) {
74+
function benchOnClick(button, results, benchName, bencher) {
75+
button.addEventListener("click", async function (e) {
576
e.preventDefault();
6-
var stats = bencher();
77+
78+
const buttons = [...document.querySelectorAll("button")];
79+
buttons.forEach(b => b.setAttribute("disabled", true));
80+
results.innerHTML = "";
81+
await new Promise(r => requestAnimationFrame(r));
82+
83+
var stats = await bencher();
84+
85+
buttons.forEach(b => b.removeAttribute("disabled"));
86+
87+
const csv = stats
88+
.xs
89+
.map(x => `"${implAndBrowser}",${testSourceMap.mappings.length},"${benchName}",${x}`)
90+
.join("\n");
91+
792
results.innerHTML = `
893
<table>
994
<thead>
@@ -23,14 +108,21 @@ function benchOnClick(button, results, bencher) {
23108
</tr>
24109
</tbody>
25110
</table>
111+
<pre style="overflow:scroll;max-height:100px; max-width:500px;outline:1px solid black">${csv}</pre>
26112
`;
27113
}, false);
28114
}
29115

30-
benchOnClick(document.getElementById("bench-consumer"),
31-
document.getElementById("consumer-results"),
32-
benchmarkParseSourceMap);
116+
for (let bench of Object.keys(benchmarks)) {
117+
const hr = document.createElement("hr");
118+
document.body.appendChild(hr);
33119

34-
benchOnClick(document.getElementById("bench-generator"),
35-
document.getElementById("generator-results"),
36-
benchmarkSerializeSourceMap);
120+
const button = document.createElement("button");
121+
button.innerHTML = `<h2>${bench}</h2>`;
122+
document.body.appendChild(button);
123+
124+
const results = document.createElement("div");
125+
document.body.appendChild(results);
126+
127+
benchOnClick(button, results, bench, benchmarks[bench]);
128+
}

0 commit comments

Comments
 (0)