Permalink
Browse files

High-perf source map builder

Summary:
Adds a high performance source map builder that has certain restrictions compared to the `'source-map'` package:

- mappings have to be in the order of the generated source
- source files have to be started/ended separately on the generator. That means building up mappings is optimized for blocks of mappings that all belong to the same source file (or no file)

The implementation avoids allocation of complex value, i.e. strings and objects as much as possible by preallocating a buffer and using numeric character values throughout. The buffer is converted to a string only at the end.

This implementation is ~5✕ faster than using `'source-map'`.

Reviewed By: jeanlauliac

Differential Revision: D4392260

fbshipit-source-id: 406381302d951b919243a2b15e8bb75981e9f979
  • Loading branch information...
davidaurelio authored and facebook-github-bot committed Jan 9, 2017
1 parent e2a5bc1 commit 7ca5316562c3223fd634dd90ee82b672be4198e9
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @flow
+ */
+
+'use strict';
+
+const encode = require('./encode');
+
+const MAX_SEGMENT_LENGTH = 7;
+const ONE_MEG = 1024 * 1024;
+const COMMA = 0x2c;
+const SEMICOLON = 0x3b;
+
+/**
+ * Efficient builder for base64 VLQ mappings strings.
+ *
+ * This class uses a buffer that is preallocated with one megabyte and is
+ * reallocated dynamically as needed, doubling its size.
+ *
+ * Encoding never creates any complex value types (strings, objects), and only
+ * writes character values to the buffer.
+ *
+ * For details about source map terminology and specification, check
+ * https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
+ */
+class B64Builder {
+ buffer: Buffer;
+ pos: number;
+ hasSegment: boolean;
+
+ constructor() {
+ this.buffer = new Buffer(ONE_MEG);
+ this.pos = 0;
+ this.hasSegment = false;
+ }
+
+ /**
+ * Adds `n` markers for generated lines to the mappings.
+ */
+ markLines(n: number) {
+ this.hasSegment = false;
+ if (this.pos + n >= this.buffer.length) {
+ this._realloc();
+ }
+ while (n--) {
+ this.buffer[this.pos++] = SEMICOLON;
+ }
+ return this;
+ }
+
+ /**
+ * Starts a segment at the specified column offset in the current line.
+ */
+ startSegment(column: number) {
+ if (this.hasSegment) {
+ this._writeByte(COMMA);
+ } else {
+ this.hasSegment = true;
+ }
+
+ this.append(column);
+ return this;
+ }
+
+ /**
+ * Appends a single number to the mappings.
+ */
+ append(value: number) {
+ if (this.pos + MAX_SEGMENT_LENGTH >= this.buffer.length) {
+ this._realloc();
+ }
+
+ this.pos = encode(value, this.buffer, this.pos);
+ return this;
+ }
+
+ /**
+ * Returns the string representation of the mappings.
+ */
+ toString() {
+ return this.buffer.toString('ascii', 0, this.pos);
+ }
+
+ _writeByte(byte) {
+ if (this.pos === this.buffer.length) {
+ this._realloc();
+ }
+ this.buffer[this.pos++] = byte;
+ }
+
+ _realloc() {
+ const {buffer} = this;
+ this.buffer = new Buffer(buffer.length * 2);
+ buffer.copy(this.buffer);
+ }
+}
+
+module.exports = B64Builder;
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @flow
+ */
+
+'use strict';
+
+const B64Builder = require('./B64Builder');
+
+import type {SourceMap} from 'babel-core';
+
+/**
+ * Generates a source map from raw mappings.
+ *
+ * Raw mappings are a set of 2, 4, or five elements:
+ *
+ * - line and column number in the generated source
+ * - line and column number in the original source
+ * - symbol name in the original source
+ *
+ * Mappings have to be passed in the order appearance in the generated source.
+ */
+class Generator {
+ builder: B64Builder;
+ last: {|
+ generatedColumn: number,
+ generatedLine: number,
+ name: number,
+ source: number,
+ sourceColumn: number,
+ sourceLine: number,
+ |};
+ names: IndexedSet;
+ source: number;
+ sources: Array<string>;
+ sourcesContent: Array<string>;
+
+ constructor() {
+ this.builder = new B64Builder();
+ this.last = {
+ generatedColumn: 0,
+ generatedLine: 1, // lines are passed in 1-indexed
+ name: 0,
+ source: 0,
+ sourceColumn: 0,
+ sourceLine: 1,
+ };
+ this.names = new IndexedSet();
+ this.source = -1;
+ this.sources = [];
+ this.sourcesContent = [];
+ }
+
+ /**
+ * Mark the beginning of a new source file.
+ */
+ startFile(file: string, code: string) {
+ this.source = this.sources.push(file) - 1;
+ this.sourcesContent.push(code);
+ }
+
+ /**
+ * Mark the end of the current source file
+ */
+ endFile() {
+ this.source = -1;
+ }
+
+ /**
+ * Add a mapping that contains the first 2, 4, or all of the following values:
+ *
+ * 1. line offset in the generated source
+ * 2. column offset in the generated source
+ * 3. line offset in the original source
+ * 4. column offset in the original source
+ * 5. name of the symbol in the original source.
+ */
+ addMapping(
+ generatedLine: number,
+ generatedColumn: number,
+ sourceLine?: number,
+ sourceColumn?: number,
+ name?: string,
+ ): void {
+ var {last} = this;
+ if (this.source === -1 ||
+ generatedLine === last.generatedLine &&
+ generatedColumn < last.generatedColumn ||
+ generatedLine < last.generatedLine) {
+ const msg = this.source === -1
+ ? 'Cannot add mapping before starting a file with `addFile()`'
+ : 'Mapping is for a position preceding an earlier mapping';
+ throw new Error(msg);
+ }
+
+ if (generatedLine > last.generatedLine) {
+ this.builder.markLines(generatedLine - last.generatedLine);
+ last.generatedLine = generatedLine;
+ last.generatedColumn = 0;
+ }
+
+ this.builder.startSegment(generatedColumn - last.generatedColumn);
+ last.generatedColumn = generatedColumn;
+
+ if (sourceLine != null) {
+ if (sourceColumn == null) {
+ throw new Error(
+ 'Received mapping with source line, but without source column');
+ }
+
+ this.builder
+ .append(this.source - last.source)
+ .append(sourceLine - last.sourceLine)
+ .append(sourceColumn - last.sourceColumn);
+
+ last.source = this.source;
+ last.sourceColumn = sourceColumn;
+ last.sourceLine = sourceLine;
+
+ if (name != null) {
+ const nameIndex = this.names.indexFor(name);
+ this.builder.append(nameIndex - last.name);
+ last.name = nameIndex;
+ }
+ }
+ }
+
+ /**
+ * Return the source map as object.
+ */
+ toMap(file?: string): SourceMap {
+ return {
+ version: 3,
+ file,
+ sources: this.sources.slice(),
+ sourcesContent: this.sourcesContent.slice(),
+ names: this.names.items(),
+ mappings: this.builder.toString(),
+ };
+ }
+
+ /**
+ * Return the source map as string.
+ *
+ * This is ~2.5x faster than calling `JSON.stringify(generator.toMap())`
+ */
+ toString(file?: string): string {
+ return ('{' +
+ '"version":3,' +
+ (file ? `"file":${JSON.stringify(file)},` : '') +
+ `"sources":${JSON.stringify(this.sources)},` +
+ `"sourcesContent":${JSON.stringify(this.sourcesContent)},` +
+ `"names":${JSON.stringify(this.names.items())},` +
+ `"mappings":"${this.builder.toString()}"` +
+ '}');
+ }
+}
+
+class IndexedSet {
+ map: Map<string, number>;
+ nextIndex: number;
+
+ constructor() {
+ this.map = new Map();
+ this.nextIndex = 0;
+ }
+
+ indexFor(x: string) {
+ let index = this.map.get(x);
+ if (index == null) {
+ index = this.nextIndex++;
+ this.map.set(x, index);
+ }
+ return index;
+ }
+
+ items() {
+ return Array.from(this.map.keys());
+ }
+}
+
+module.exports = Generator;
Oops, something went wrong.

0 comments on commit 7ca5316

Please sign in to comment.