Skip to content
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

Merged
merged 46 commits into from Jan 18, 2018
Merged

Conversation

fitzgen
Copy link
Contributor

@fitzgen fitzgen commented Jan 8, 2018

r? @tromey

Easier to graph / work with after benchmarking this way.
@paulirish
Copy link

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

@fitzgen
Copy link
Contributor Author

fitzgen commented Jan 16, 2018

Expect a detailed blog post in the coming days... ;)

```js
var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData);
consumer.destroy();
Copy link
Contributor

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.

Copy link
Contributor Author

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.

};
}

if (typeof print !== "function") {
print = console.log.bind(console);
print = function (x = "") {
console.log(`${x}`);
Copy link
Contributor

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.

Copy link
Contributor Author

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': ... }) " +
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

var sourceRoot = this.sourceRoot;
mappings.map(function (mapping) {
var source = null;
if(mapping.source !== null) {
Copy link
Contributor

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".

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));
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

@tromey tromey left a 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...

@fitzgen
Copy link
Contributor Author

fitzgen commented Jan 17, 2018

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.

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.

@tromey
Copy link
Contributor

tromey commented Jan 17, 2018

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.

@fitzgen
Copy link
Contributor Author

fitzgen commented Jan 17, 2018

Ok, I think this is just about ready to land. Waiting on OSX jobs in Travis CI.

@fitzgen
Copy link
Contributor Author

fitzgen commented Jan 18, 2018

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`.
Copy link

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/

Copy link
Contributor Author

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
Copy link

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

Copy link
Contributor Author

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': ... }) " +
Copy link

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?

Copy link
Contributor Author

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.

jasonLaster pushed a commit that referenced this pull request Oct 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants