New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use WebAssembly to speed up SourceMapConsumer #306
Conversation
…parse in benchmark
Easier to graph / work with after benchmarking this way.
This is awesome. I just tried it out in https://github.com/danvk/source-map-explorer and my particular usecase went from 20.5s to 8.2s. Nicely done. cc @danvk |
Expect a detailed blog post in the coming days... ;) |
```js | ||
var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData); | ||
consumer.destroy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bummer but I guess there's no good way around it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have ideas to improve this in follow ups:
Either add an async RAII-ish function like:
SourceMapConsumer.with = async function (rawSourceMap, f) {
const consumer = await new SourceMapConsumer(rawSourceMap);
try {
await f(consumer);
} finally {
consumer.destroy();
}
};
Or alternatively give every SourceMapConsumer
its own wasm module instance, make the Mappings
a global/implicit in the wasm heap, and let the GC manage lifetimes. Requires further investigation.
bench/bench-shell-bindings.js
Outdated
}; | ||
} | ||
|
||
if (typeof print !== "function") { | ||
print = console.log.bind(console); | ||
print = function (x = "") { | ||
console.log(`${x}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not important but I thought console.log
was self-bound already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it differed across engines at one point in time, unsure if it still does or not.
In fact, the shell version of the benchmarks doesn't work anymore (added too many options that are only exposed in the Webpage), so I should just remove it.
module.exports = function readWasm() { | ||
if (typeof mappingsWasmUrl !== "string") { | ||
throw new Error("You must provide the URL of lib/mappings.wasm by calling " + | ||
"SourceMapConsumer.initialize({ 'lib/mappings.wasm': ... }) " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be crazy to just have the wasm be a giant string constant in some .js
file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would bloat the code size, which I took quite a bit of effort in keeping small.
// The offset fields are 0-based, but we use 1-based indices when | ||
// encoding/decoding from VLQ. | ||
generatedLine: offsetLine + 1, | ||
generatedColumn: offsetColumn + 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not introduced here, but elsewhere it says that columns are 0-based (what I recall as well), so I wonder if this is a latent bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps? I had to do this to get the tests passing with the wasm. I think we can investigate further in a follow up.
for (var j = 0; j < sectionMappings.length; j++) { | ||
var mapping = sectionMappings[j]; | ||
|
||
var source = section.consumer._sources.at(mapping.source); | ||
source = util.computeSourceURL(section.consumer.sourceRoot, source, this._sourceMapURL); | ||
var source = util.computeSourceURL(section.consumer.sourceRoot, source, this._sourceMapURL); | ||
this._sources.add(source); | ||
source = this._sources.indexOf(source); | ||
|
||
var name = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't examine more context but I suspect this line should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The var name = null;
line? The name
variable is assigned to and used a little bit down this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks.
lib/source-map-consumer.js
Outdated
var sourceRoot = this.sourceRoot; | ||
mappings.map(function (mapping) { | ||
var source = null; | ||
if(mapping.source !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trivia: space after the "if".
test/test-source-map-generator.js
Outdated
exports['test .fromSourceMap'] = function (assert) { | ||
var map = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(util.testMap)); | ||
exports['test .fromSourceMap'] = async function (assert) { | ||
var map = SourceMapGenerator.fromSourceMap(await new SourceMapConsumer(util.testMap)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose it's not a big deal to leak the consumers in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I thought I caught them all, did I miss some?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see where these consumers were destroyed. But maybe fromSourceMap
does and I missed it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is big enough and covers enough different things that it is going to suffer from the other side of bikeshedding -- that is, I tried to read it all but I'm not sure I can really do it justice.
I do wonder if it's possible to remove the need for a separate wasm URL and thus (IIUC) the need for the constructor to return a promise.
Otherwise I'm inclined to push ahead. Perhaps I ought to go address #291 and #305...
Compiling wasm is still done off-thread, and returns a promise, so even if we already had the blob, the constructor would still return a promise. |
Thanks. Never mind then. |
Ok, I think this is just about ready to land. Waiting on OSX jobs in Travis CI. |
FYI, https://hacks.mozilla.org/2018/01/oxidizing-source-maps-with-rust-and-webassembly/ is live with details on the experience. Going to go ahead and merge this PR! |
``` | ||
|
||
The resulting `wasm` file will be located at | ||
`source-map-mappings-c-api/target/wasm32-unknown-unknown/release/source_map_mappings.wasm`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/c-api/wasm-api/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch!
Open `bench.html` in a browser and click on the appropriate button. | ||
``` | ||
$ cd source-map/ | ||
$ python -m SimpleHTTPServer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better python2 -m SimpleHTTPServer
or python3 -m http.server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks
module.exports = function readWasm() { | ||
if (typeof mappingsWasmUrl !== "string") { | ||
throw new Error("You must provide the URL of lib/mappings.wasm by calling " + | ||
"SourceMapConsumer.initialize({ 'lib/mappings.wasm': ... }) " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initialize()
accepts just string though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public SourceMapConsumer.initialize
function takes an object, the interal initialize
function in lib/read-wasm.js
just takes a string.
This is a leftover from #306.
r? @tromey