diff --git a/lib/source-map-consumer.js b/lib/source-map-consumer.js index e3716d57..ea32f43e 100644 --- a/lib/source-map-consumer.js +++ b/lib/source-map-consumer.js @@ -9,15 +9,6 @@ var util = require('./util'); var binarySearch = require('./binary-search'); var ArraySet = require('./array-set').ArraySet; var base64VLQ = require('./base64-vlq'); -var quickSort = require('./quick-sort').quickSort; -var mappingListModule = require('./mapping-list'); -var ML_FIELDS = mappingListModule.FIELDS_PER_MAPPING; -var ML_F_GEN_LINE = mappingListModule.F_GEN_LINE; -var ML_F_GEN_COL = mappingListModule.F_GEN_COL; -var ML_F_SRC_IDX = mappingListModule.F_SRC_IDX; -var ML_F_ORIG_LINE = mappingListModule.F_ORIG_LINE; -var ML_F_ORIG_COL = mappingListModule.F_ORIG_COL; -var ML_F_NAME_IDX = mappingListModule.F_NAME_IDX; function SourceMapConsumer(aSourceMap, aSourceMapURL) { var sourceMap = aSourceMap; @@ -39,35 +30,27 @@ SourceMapConsumer.fromSourceMap = function(aSourceMap, aSourceMapURL) { */ SourceMapConsumer.prototype._version = 3; -// `__generatedMappings` and `__originalMappings` are arrays that hold the -// parsed mapping coordinates from the source map's "mappings" attribute. They -// are lazily instantiated, accessed via the `_generatedMappings` and -// `_originalMappings` getters respectively, and we only parse the mappings -// and create these arrays once queried for a source location. We jump through -// these hoops because there can be many thousands of mappings, and parsing -// them is expensive, so we only want to do it if we must. +// Parsed mappings are stored in Int32Array "slabs" instead of arrays of JS +// objects. Each mapping occupies six i32 slots (see SLAB_FIELDS below) — the +// generator-side MappingList from PR #64 uses the same layout. Eliminating the +// per-mapping heap object removes ~350k allocations per parse of a typical +// bundle and the ~12% GC tax that came with them. // -// Each object in the arrays is of the form: +// The generated-position slab lives on `this._genBuf` (Int32Array) + +// `this._genCount` (used row count). The original-position slab is the same, +// on `this._origBuf` + `this._origCount`. Both are built lazily on first +// access through the `_generatedMappings` / `_originalMappings` getters, which +// materialize back-compat object arrays from the slab on demand for callers +// that still expect the old shape. // -// { -// generatedLine: The line number in the generated code, -// generatedColumn: The column number in the generated code, -// source: The path to the original source file that generated this -// chunk of code, -// originalLine: The line number in the original source that -// corresponds to this chunk of generated code, -// originalColumn: The column number in the original source that -// corresponds to this chunk of generated code, -// name: The name of the original symbol which generated this chunk of -// code. -// } -// -// All properties except for `generatedLine` and `generatedColumn` can be -// `null`. +// Slot semantics: +// GEN_LINE, GEN_COL — always set +// SRC_IDX — integer index into `_sources`, or -1 for "no source" +// ORIG_LINE, ORIG_COL — integers, or -1 for "no original line/column" +// NAME_IDX — integer index into `_names`, or -1 for "no name" // // `_generatedMappings` is ordered by the generated positions. -// -// `_originalMappings` is ordered by the original positions. +// `_originalMappings` is ordered by the original positions. // Warm-start cache fields for `originalPositionFor`. Declared on the prototype // so per-instance assignments don't grow the hidden class on first cache hit. @@ -87,37 +70,113 @@ SourceMapConsumer.prototype._sourceIndexCache = null; // unset slots. The array is sized from `_sources.size()` and never resized. SourceMapConsumer.prototype._gpfBySrc = null; +// Int32Array slab layout: 6 i32 slots per mapping, matching MappingList's +// generator-side field order (lib/mapping-list.js). Kept inline rather than +// imported because the consumer's hot paths read these constants in tight +// binary-search loops and the V8 inliner treats locally-declared constants +// best. +// Layout matches lib/mapping-list.js's MappingList slab exactly — same +// FIELDS_PER_MAPPING, same field order, same -1 sentinels — so fromSourceMap +// can copy the generator's slab into the consumer's with a single typed-array +// `set()` and no per-row decoding. +var SLAB_FIELDS = 6; +var SLAB_GEN_LINE = 0; +var SLAB_GEN_COL = 1; +var SLAB_SRC_IDX = 2; +var SLAB_ORIG_LINE = 3; +var SLAB_ORIG_COL = 4; +var SLAB_NAME_IDX = 5; +var SLAB_INITIAL_CAPACITY = 256; + +// Slab storage — see top-of-file comment for semantics. +SourceMapConsumer.prototype._genBuf = null; +SourceMapConsumer.prototype._genCount = 0; +SourceMapConsumer.prototype._origBuf = null; +SourceMapConsumer.prototype._origCount = 0; +// Parallel Int32Array sized to `_genCount`, allocated lazily by +// `computeColumnSpans`. Slot i holds the last-generated-column for the +// mapping at slab row i, or -1 if the column extends to the line end. +SourceMapConsumer.prototype._lastGenCols = null; +// Per-row map orig→gen, built alongside `_origBuf` in `_buildOriginalMappings`. +// Lets `generatedPositionFor` / `allGeneratedPositionsFor` look up the +// matching `_lastGenCols` slot when `computeColumnSpans` has been called. +SourceMapConsumer.prototype._origToGen = null; + +// Materialized-object-array caches. Lazy: only built if a caller goes through +// the `_generatedMappings` / `_originalMappings` getter (eachMapping, +// originalPositionFor, etc. all read the slab directly without touching these). SourceMapConsumer.prototype.__generatedMappings = null; +SourceMapConsumer.prototype.__originalMappings = null; + Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', { configurable: true, enumerable: true, get: function () { - if (!this.__generatedMappings) { + if (this._genBuf === null) { this._parseMappings(this._mappings, this.sourceRoot); } - + if (this.__generatedMappings === null) { + this.__generatedMappings = this._materializeSlabAsArray(this._genBuf, this._genCount); + } return this.__generatedMappings; } }); -SourceMapConsumer.prototype.__originalMappings = null; Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', { configurable: true, enumerable: true, get: function () { - if (!this.__originalMappings) { - // Ensure generatedMappings are parsed first (may also set __originalMappings for IndexedSourceMapConsumer) - var generatedMappings = this._generatedMappings; - // Build originalMappings lazily if not already set (BasicSourceMapConsumer) - if (!this.__originalMappings) { + if (this._origBuf === null) { + if (this._genBuf === null) { + this._parseMappings(this._mappings, this.sourceRoot); + } + if (this._origBuf === null) { this._buildOriginalMappings(); } } - + if (this.__originalMappings === null) { + this.__originalMappings = this._materializeSlabAsArray( + this._origBuf, this._origCount, this._origToGen); + } return this.__originalMappings; } }); +// Materialize a `(buf, count)` slab into an Array with the legacy +// object shape (source/originalLine/originalColumn/name as integer index or +// `null`, matching how `_parseMappings` used to populate +// `__generatedMappings`). When `computeColumnSpans` has populated +// `_lastGenCols`, each output mapping also gets a `lastGeneratedColumn` +// property; for the original-order slab, the lookup hops through `origToGen` +// (built alongside `_origBuf`) to find the matching gen row. +SourceMapConsumer.prototype._materializeSlabAsArray = + function SourceMapConsumer_materializeSlabAsArray(buf, count, origToGen) { + var lastCols = this._lastGenCols; + var out = new Array(count); + for (var i = 0; i < count; i++) { + var off = i * SLAB_FIELDS; + var srcIdx = buf[off + SLAB_SRC_IDX]; + var origLine = buf[off + SLAB_ORIG_LINE]; + var origCol = buf[off + SLAB_ORIG_COL]; + var nameIdx = buf[off + SLAB_NAME_IDX]; + var m = { + generatedLine: buf[off + SLAB_GEN_LINE], + generatedColumn: buf[off + SLAB_GEN_COL], + source: srcIdx === -1 ? null : srcIdx, + originalLine: origLine === -1 ? null : origLine, + originalColumn: origCol === -1 ? null : origCol, + name: nameIdx === -1 ? null : nameIdx + }; + if (lastCols !== null) { + var genIdx = origToGen !== undefined ? origToGen[i] : i; + var v = lastCols[genIdx]; + m.lastGeneratedColumn = v === -1 ? Infinity : v; + } + out[i] = m; + } + return out; + }; + SourceMapConsumer.prototype._charIsMappingSeparator = function SourceMapConsumer_charIsMappingSeparator(aStr, index) { var c = aStr.charAt(index); @@ -161,6 +220,11 @@ SourceMapConsumer.prototype.eachMapping = var context = aContext || null; var order = aOrder || SourceMapConsumer.GENERATED_ORDER; + // Go through the materialized-array getters rather than reading the slab + // directly: the first call materializes once and the result is cached on + // `__generatedMappings` / `__originalMappings`, so repeated eachMapping + // calls iterate the stable-hidden-class object array (the post-#70 + // shape) instead of paying the slab-decode price per row each time. var mappings; switch (order) { case SourceMapConsumer.GENERATED_ORDER: @@ -173,18 +237,8 @@ SourceMapConsumer.prototype.eachMapping = throw new Error("Unknown order of iteration."); } - // Skip the `Function.prototype.bind` allocation when no context was - // supplied — `aCallback || null` defaults to a null context, which is - // the same as calling the function unbound. var cb = context !== null ? aCallback.bind(context) : aCallback; - // Direct reads of the underlying `_array` slot match the slab-direct - // pattern in `_serializeMappings` / `fromSourceMap`. Skips the `at()` - // bounds-check + function call per named mapping. var nameArray = this._names._array; - // `_absoluteSources` is precomputed in both consumer constructors as - // `computeSourceURL(sourceRoot, _sources.at(i), sourceMapURL)`, so a - // direct index skips the per-call URL parse + resolve. Same memoization - // pattern used by `originalPositionFor` (PR #49). var absoluteSources = this._absoluteSources; for (var i = 0, n = mappings.length; i < n; i++) { @@ -227,68 +281,105 @@ SourceMapConsumer.prototype.eachMapping = SourceMapConsumer.prototype.allGeneratedPositionsFor = function SourceMapConsumer_allGeneratedPositionsFor(aArgs) { var line = util.getArg(aArgs, 'line'); - - // When there is no exact match, BasicSourceMapConsumer.prototype._findMapping - // returns the index of the closest mapping less than the needle. By - // setting needle.originalColumn to 0, we thus find the last mapping for - // the given line, provided such a mapping exists. - var needle = { - source: util.getArg(aArgs, 'source'), - originalLine: line, - // `column` is optional, defaulted to 0. Inline the optional read. - originalColumn: aArgs.column != null ? aArgs.column : 0 - }; - - needle.source = this._findSourceIndex(needle.source); - if (needle.source < 0) { + var source = this._findSourceIndex(util.getArg(aArgs, 'source')); + if (source < 0) { return []; } + var hasColumn = aArgs.column !== undefined; + // column defaults to 0 for the LUB needle so the search lands on the + // smallest mapping with originalLine >= line. + var needleColumn = hasColumn ? aArgs.column : 0; + + if (this._origBuf === null) { + if (this._genBuf === null) { + this._parseMappings(this._mappings, this.sourceRoot); + } + if (this._origBuf === null) { + this._buildOriginalMappings(); + } + } + var buf = this._origBuf; + var count = this._origCount; + var lastCols = this._lastGenCols; + var origToGen = this._origToGen; + + // Inline LUB binary search on (source, originalLine, originalColumn). + // Returns the smallest index with value >= needle, then rewinds through + // ties to match binarySearch.search's smallest-equal semantics. + var lo = -1; + var hi = count; + while (hi - lo > 1) { + var mid = (lo + hi) >>> 1; + var off = mid * SLAB_FIELDS; + var cmp; + if (buf[off + SLAB_SRC_IDX] !== source) cmp = buf[off + SLAB_SRC_IDX] - source; + else if (buf[off + SLAB_ORIG_LINE] !== line) cmp = buf[off + SLAB_ORIG_LINE] - line; + else cmp = buf[off + SLAB_ORIG_COL] - needleColumn; + if (cmp < 0) lo = mid; + else hi = mid; + } + if (hi >= count) return []; + while (hi > 0) { + var aOff = hi * SLAB_FIELDS; + var bOff = (hi - 1) * SLAB_FIELDS; + if (buf[aOff + SLAB_SRC_IDX] !== buf[bOff + SLAB_SRC_IDX] || + buf[aOff + SLAB_ORIG_LINE] !== buf[bOff + SLAB_ORIG_LINE] || + buf[aOff + SLAB_ORIG_COL] !== buf[bOff + SLAB_ORIG_COL]) { + break; + } + hi--; + } + var index = hi; - var mappings = []; + var firstOff = index * SLAB_FIELDS; + if (buf[firstOff + SLAB_SRC_IDX] !== source) return []; - var index = this._findMapping(needle, - this._originalMappings, - "originalLine", - "originalColumn", - util.compareByOriginalPositions, - binarySearch.LEAST_UPPER_BOUND); - if (index >= 0) { - var mapping = this._originalMappings[index]; - - if (aArgs.column === undefined) { - var originalLine = mapping.originalLine; - - // Iterate until either we run out of mappings, or we run into - // a mapping for a different line than the one we found. Since - // mappings are sorted, this is guaranteed to find all mappings for - // the line we found. - while (mapping && mapping.originalLine === originalLine) { - mappings.push({ - line: mapping.generatedLine, - column: mapping.generatedColumn, - lastColumn: mapping.lastGeneratedColumn != null ? mapping.lastGeneratedColumn : null - }); - - mapping = this._originalMappings[++index]; + var mappings = []; + if (!hasColumn) { + // Collect every mapping on the matching originalLine (within the + // source) — the LUB landed on the smallest such index, so consecutive + // rows are contiguous up to the line change. + var matchLine = buf[firstOff + SLAB_ORIG_LINE]; + var i = index; + while (i < count) { + var off2 = i * SLAB_FIELDS; + if (buf[off2 + SLAB_SRC_IDX] !== source || + buf[off2 + SLAB_ORIG_LINE] !== matchLine) { + break; } - } else { - var originalColumn = mapping.originalColumn; - - // Iterate until either we run out of mappings, or we run into - // a mapping for a different line than the one we were searching for. - // Since mappings are sorted, this is guaranteed to find all mappings for - // the line we are searching for. - while (mapping && - mapping.originalLine === line && - mapping.originalColumn == originalColumn) { - mappings.push({ - line: mapping.generatedLine, - column: mapping.generatedColumn, - lastColumn: mapping.lastGeneratedColumn != null ? mapping.lastGeneratedColumn : null - }); - - mapping = this._originalMappings[++index]; + var lastCol = null; + if (lastCols !== null) { + var v = lastCols[origToGen[i]]; + lastCol = v === -1 ? Infinity : v; } + mappings.push({ + line: buf[off2 + SLAB_GEN_LINE], + column: buf[off2 + SLAB_GEN_COL], + lastColumn: lastCol + }); + i++; + } + } else { + var matchCol = buf[firstOff + SLAB_ORIG_COL]; + var i = index; + while (i < count) { + var off2 = i * SLAB_FIELDS; + if (buf[off2 + SLAB_SRC_IDX] !== source || + buf[off2 + SLAB_ORIG_LINE] !== line || + buf[off2 + SLAB_ORIG_COL] != matchCol) { + break; + } + var lastCol = null; + if (lastCols !== null) { + var v = lastCols[origToGen[i]]; + lastCol = v === -1 ? Infinity : v; + } + mappings.push({ + line: buf[off2 + SLAB_GEN_LINE], + column: buf[off2 + SLAB_GEN_COL], + lastColumn: lastCol + }); + i++; } } @@ -460,11 +551,12 @@ BasicSourceMapConsumer.fromSourceMap = return util.computeSourceURL(smc.sourceRoot, s, aSourceMapURL); }); - // Read the generator's MappingList slab directly. The slab already - // stores source/name as integer indices into aSourceMap._sources / - // _names, and our smc._sources / smc._names were initialized from the - // same toArray() above — so the indices are identical and no `indexOf` - // is needed per mapping. + // Copy the generator's MappingList slab straight into a consumer slab. + // ML_FIELDS / SLAB_FIELDS share the same layout and the same sentinel + // (-1 for absent source/origLine/origCol/name), so a single typed-array + // `set()` covers the active rows. Source/name remain integer indices — + // smc._sources / smc._names were initialized from aSourceMap's toArray() + // above, so the indices are identical. var ml = aSourceMap._mappings; if (!ml._sorted) { ml._sort(); @@ -472,63 +564,13 @@ BasicSourceMapConsumer.fromSourceMap = } var mlBuf = ml._buf; var mlCount = ml._count; - var destGeneratedMappings = smc.__generatedMappings = new Array(mlCount); - // Bucket original-side mappings by source index — same pattern as - // _buildOriginalMappings and IndexedSourceMapConsumer._parseMappings — - // so the per-bucket sort can use compareByOriginalPositionsNoSource and - // skip the function-call strcmp(source, source) primary key. - var originalBuckets = []; - - for (var i = 0; i < mlCount; i++) { - var mlOff = i * ML_FIELDS; - var destMapping = new Mapping; - destMapping.generatedLine = mlBuf[mlOff + ML_F_GEN_LINE]; - destMapping.generatedColumn = mlBuf[mlOff + ML_F_GEN_COL]; - - var srcIdx = mlBuf[mlOff + ML_F_SRC_IDX]; - if (srcIdx !== -1) { - destMapping.source = srcIdx; - destMapping.originalLine = mlBuf[mlOff + ML_F_ORIG_LINE]; - destMapping.originalColumn = mlBuf[mlOff + ML_F_ORIG_COL]; - - var nameIdx = mlBuf[mlOff + ML_F_NAME_IDX]; - if (nameIdx !== -1) { - destMapping.name = nameIdx; - } - - while (originalBuckets.length <= srcIdx) { - originalBuckets.push(null); - } - if (originalBuckets[srcIdx] === null) { - originalBuckets[srcIdx] = []; - } - originalBuckets[srcIdx].push(destMapping); - } - - destGeneratedMappings[i] = destMapping; - } - - var nonNullBuckets = []; - var compareOriginal = util.compareByOriginalPositionsNoSource; - for (var b = 0; b < originalBuckets.length; b++) { - var perSource = originalBuckets[b]; - if (perSource != null) { - // Well-formed input usually emits per-source mappings already in - // original-position order — probe and skip the sort when it is. - var sorted = true; - for (var k = 1; k < perSource.length; k++) { - if (compareOriginal(perSource[k - 1], perSource[k]) > 0) { - sorted = false; - break; - } - } - if (!sorted) { - quickSort(perSource, compareOriginal); - } - nonNullBuckets.push(perSource); - } - } - smc.__originalMappings = [].concat(...nonNullBuckets); + var genBuf = new Int32Array(mlCount * SLAB_FIELDS); + genBuf.set(mlBuf.subarray(0, mlCount * SLAB_FIELDS)); + smc._genBuf = genBuf; + smc._genCount = mlCount; + // Build _origBuf via the shared bucketing path on the freshly-populated + // _genBuf. Mirrors _parseMappings → _buildOriginalMappings. + BasicSourceMapConsumer.prototype._buildOriginalMappings.call(smc); return smc; }; @@ -547,63 +589,87 @@ Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', { } }); -/** - * Provide the JIT with a nice shape / hidden class. - */ -function Mapping() { - this.generatedLine = 0; - this.generatedColumn = 0; - this.source = null; - this.originalLine = null; - this.originalColumn = null; - this.name = null; -} - /** * Parse the mappings in a string in to a data structure which we can easily * query (the ordered arrays in the `this.__generatedMappings` and * `this.__originalMappings` properties). */ -const compareGenerated = util.compareByGeneratedPositionsDeflatedNoLine; -function sortGenerated(array, start) { - let l = array.length; - let n = array.length - start; - if (n <= 1) { - return; +// Swap two slab rows by index. Each row is `SLAB_FIELDS` i32 slots, so a swap +// is 12 typed-array reads / 12 writes — still cheap inside a per-line sort +// because most maps come in already sorted and the swap path never runs. +function slabSwapRows(buf, i, j) { + var oi = i * SLAB_FIELDS; + var oj = j * SLAB_FIELDS; + for (var k = 0; k < SLAB_FIELDS; k++) { + var tmp = buf[oi + k]; + buf[oi + k] = buf[oj + k]; + buf[oj + k] = tmp; } +} - // Check if already sorted (common case for well-formed source maps) - let sorted = true; - for (let i = start + 1; i < l; i++) { - if (compareGenerated(array[i - 1], array[i]) > 0) { +// Sort slab rows in `[start, count)` by `generatedColumn` (line is constant +// within a per-line subarray, so column-only suffices). Mirrors the original +// object-array `sortGenerated` strategy: check-sorted first, then either +// insertion sort for small n or quicksort-by-row for large n. +function sortGeneratedSlab(buf, start, count) { + var n = count - start; + if (n <= 1) return; + + // Check-sorted fast path — well-formed source maps emit segments in + // increasing generatedColumn within each line, so this is overwhelmingly + // the common case and we exit without doing any swap work. + var sorted = true; + for (var i = start + 1; i < count; i++) { + if (buf[(i - 1) * SLAB_FIELDS + SLAB_GEN_COL] > + buf[i * SLAB_FIELDS + SLAB_GEN_COL]) { sorted = false; break; } } - if (sorted) { + if (sorted) return; + + if (n === 2) { + slabSwapRows(buf, start, start + 1); return; } - if (n == 2) { - // Already checked above, must be out of order - let a = array[start]; - array[start] = array[start + 1]; - array[start + 1] = a; - } else if (n < 20) { - for (let i = start; i < l; i++) { - for (let j = i; j > start; j--) { - let a = array[j - 1]; - let b = array[j]; - if (compareGenerated(a, b) <= 0) { + if (n < 20) { + for (var i = start + 1; i < count; i++) { + for (var j = i; j > start; j--) { + if (buf[(j - 1) * SLAB_FIELDS + SLAB_GEN_COL] <= + buf[j * SLAB_FIELDS + SLAB_GEN_COL]) { break; } - array[j - 1] = b; - array[j] = a; + slabSwapRows(buf, j - 1, j); } } - } else { - quickSort(array, compareGenerated, start); + return; + } + + // Large unsorted run — sort a permutation array by genCol, then permute + // the slab in one pass. O(n) extra memory but only triggered for the rare + // ill-formed map. + var perm = new Array(n); + for (var i = 0; i < n; i++) perm[i] = start + i; + perm.sort(function (a, b) { + return buf[a * SLAB_FIELDS + SLAB_GEN_COL] - + buf[b * SLAB_FIELDS + SLAB_GEN_COL]; + }); + var sortedBuf = new Int32Array(n * SLAB_FIELDS); + for (var k = 0; k < n; k++) { + var srcOff = perm[k] * SLAB_FIELDS; + var dstOff = k * SLAB_FIELDS; + for (var f = 0; f < SLAB_FIELDS; f++) { + sortedBuf[dstOff + f] = buf[srcOff + f]; + } + } + for (var k = 0; k < n; k++) { + var dstOff = (start + k) * SLAB_FIELDS; + var srcOff = k * SLAB_FIELDS; + for (var f = 0; f < SLAB_FIELDS; f++) { + buf[dstOff + f] = sortedBuf[srcOff + f]; + } } } // Lookup table for single-byte VLQ decode (no continuation bit) @@ -632,13 +698,19 @@ BasicSourceMapConsumer.prototype._parseMappings = var length = aStr.length; var index = 0; var temp = {}; - var generatedMappings = []; var value, charCode; // Reuse segment array to avoid allocations per mapping var segment = [0, 0, 0, 0, 0]; var segmentLength = 0; - let subarrayStart = 0; + // Estimate initial slab capacity from the mappings string length — + // typical VLQ segments are 5-10 chars apiece, so `length >> 3` is a + // safe lower bound that avoids most reallocations on real-world maps. + var capacity = Math.max(SLAB_INITIAL_CAPACITY, length >> 3); + var buf = new Int32Array(capacity * SLAB_FIELDS); + var count = 0; + var subarrayStart = 0; + while (index < length) { charCode = aStr.charCodeAt(index); if (charCode === 59) { // ';' @@ -646,8 +718,8 @@ BasicSourceMapConsumer.prototype._parseMappings = index++; previousGeneratedColumn = 0; - sortGenerated(generatedMappings, subarrayStart); - subarrayStart = generatedMappings.length; + sortGeneratedSlab(buf, subarrayStart, count); + subarrayStart = count; } else if (charCode === 44) { // ',' index++; @@ -681,20 +753,13 @@ BasicSourceMapConsumer.prototype._parseMappings = throw new Error('Found a source and line, but no column'); } - // Compute every mapping field into a local before allocating the - // literal. Building the object in one shot lets V8 settle on a - // single hidden class per mapping shape; the previous code wrote - // nulls into source/originalLine/originalColumn/name and then - // overwrote them with numbers, which trips per-field shape - // transitions and slows downstream reads (eachMapping iteration - // was the visible loser). var genCol = previousGeneratedColumn + segment[0]; previousGeneratedColumn = genCol; - var src = null, origLine = null, origCol = null, nameIdx = null; + var srcIdx = -1, origLine = -1, origCol = -1, nameIdx = -1; if (segmentLength > 1) { - src = previousSource + segment[1]; - previousSource = src; + srcIdx = previousSource + segment[1]; + previousSource = srcIdx; var origLine0 = previousOriginalLine + segment[2]; previousOriginalLine = origLine0; @@ -709,19 +774,29 @@ BasicSourceMapConsumer.prototype._parseMappings = } } - generatedMappings.push({ - generatedLine: generatedLine, - generatedColumn: genCol, - source: src, - originalLine: origLine, - originalColumn: origCol, - name: nameIdx - }); + if (count === capacity) { + // Grow: double capacity, copy slab. set() on a typed array is a + // single memmove under the hood — much faster than re-pushing + // 6×count i32s individually. + capacity *= 2; + var nb = new Int32Array(capacity * SLAB_FIELDS); + nb.set(buf); + buf = nb; + } + var off = count * SLAB_FIELDS; + buf[off + SLAB_GEN_LINE] = generatedLine; + buf[off + SLAB_GEN_COL] = genCol; + buf[off + SLAB_SRC_IDX] = srcIdx; + buf[off + SLAB_ORIG_LINE] = origLine; + buf[off + SLAB_ORIG_COL] = origCol; + buf[off + SLAB_NAME_IDX] = nameIdx; + count++; } } - sortGenerated(generatedMappings, subarrayStart); - this.__generatedMappings = generatedMappings; + sortGeneratedSlab(buf, subarrayStart, count); + this._genBuf = buf; + this._genCount = count; }; /** @@ -729,70 +804,108 @@ BasicSourceMapConsumer.prototype._parseMappings = */ BasicSourceMapConsumer.prototype._buildOriginalMappings = function SourceMapConsumer_buildOriginalMappings() { - var generatedMappings = this.__generatedMappings; - var originalMappings = []; - - for (var i = 0; i < generatedMappings.length; i++) { - var mapping = generatedMappings[i]; - if (typeof mapping.originalLine === 'number') { - var currentSource = mapping.source; - while (originalMappings.length <= currentSource) { - originalMappings.push(null); - } - if (originalMappings[currentSource] === null) { - originalMappings[currentSource] = []; - } - originalMappings[currentSource].push(mapping); + var genBuf = this._genBuf; + var genCount = this._genCount; + + // Pass 1: count rows that carry an original position. A `-1` SLAB_ORIG_LINE + // marks "no original" (mappings emitted with a generated column only); we + // skip them entirely. _parseMappings keeps SLAB_SRC_IDX / SLAB_ORIG_LINE + // / SLAB_ORIG_COL in lockstep, so a single check on origLine is enough. + var rowsWithSource = 0; + var maxSrcIdx = -1; + for (var i = 0; i < genCount; i++) { + var off = i * SLAB_FIELDS; + if (genBuf[off + SLAB_ORIG_LINE] !== -1) { + rowsWithSource++; + var s = genBuf[off + SLAB_SRC_IDX]; + if (s > maxSrcIdx) maxSrcIdx = s; } } - var nonNullOriginalMappings = []; - var compareOriginal = util.compareByOriginalPositionsNoSource; - for (var i = 0; i < originalMappings.length; i++) { - var perSource = originalMappings[i]; - if (perSource != null) { - // Well-formed source maps emit segments in original-position order - // within each source, so the per-source array is usually already - // sorted. Probe with one O(N) pass and skip the quickSort when it - // is. Same shape as the sortGenerated skip-sorted check. - var sorted = true; - for (var j = 1; j < perSource.length; j++) { - if (compareOriginal(perSource[j - 1], perSource[j]) > 0) { - sorted = false; - break; - } - } - if (!sorted) { - quickSort(perSource, compareOriginal); + var origBuf = new Int32Array(rowsWithSource * SLAB_FIELDS); + var origToGen = new Int32Array(rowsWithSource); + if (rowsWithSource === 0) { + this._origBuf = origBuf; + this._origCount = 0; + this._origToGen = origToGen; + return; + } + + // Pass 2: bucket gen-slab row indices by source. The buckets are kept as + // plain JS arrays of i32 indices — the per-source counts (a few hundred + // to a few thousand for typical bundles) make a flat per-source pool + // overkill vs. the simplicity of small arrays. + var buckets = new Array(maxSrcIdx + 1); + for (var i = 0; i < genCount; i++) { + var off = i * SLAB_FIELDS; + if (genBuf[off + SLAB_ORIG_LINE] !== -1) { + var s = genBuf[off + SLAB_SRC_IDX]; + var b = buckets[s]; + if (b === undefined) { + b = buckets[s] = []; } - nonNullOriginalMappings.push(perSource); + b.push(i); } } - this.__originalMappings = [].concat(...nonNullOriginalMappings); - }; -/** - * Find the mapping that best matches the hypothetical "needle" mapping that - * we are searching for in the given "haystack" of mappings. - */ -BasicSourceMapConsumer.prototype._findMapping = - function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName, - aColumnName, aComparator, aBias) { - // To return the position we are searching for, we must first find the - // mapping for the given position and then return the opposite position it - // points to. Because the mappings are sorted, we can use binary search to - // find the best mapping. - - if (aNeedle[aLineName] <= 0) { - throw new TypeError('Line must be greater than or equal to 1, got ' - + aNeedle[aLineName]); - } - if (aNeedle[aColumnName] < 0) { - throw new TypeError('Column must be greater than or equal to 0, got ' - + aNeedle[aColumnName]); + // Pass 3: for each non-null bucket, ensure sorted by (originalLine, + // originalColumn, generatedColumn, generatedLine, name) — matches + // compareByOriginalPositionsNoSource — then copy rows into the orig + // slab. Well-formed maps emit per-source in original-position order, so + // the check-sorted pass almost always wins and the comparator-based + // sort below never runs. + var dstRow = 0; + for (var s = 0; s <= maxSrcIdx; s++) { + var bucket = buckets[s]; + if (bucket === undefined) continue; + + var n = bucket.length; + var sorted = true; + for (var k = 1; k < n; k++) { + var aOff = bucket[k - 1] * SLAB_FIELDS; + var bOff = bucket[k] * SLAB_FIELDS; + var d = genBuf[aOff + SLAB_ORIG_LINE] - genBuf[bOff + SLAB_ORIG_LINE]; + if (d > 0) { sorted = false; break; } + if (d === 0 && + genBuf[aOff + SLAB_ORIG_COL] > genBuf[bOff + SLAB_ORIG_COL]) { + sorted = false; break; + } + } + + if (!sorted) { + bucket.sort(function (a, b) { + var aOff = a * SLAB_FIELDS; + var bOff = b * SLAB_FIELDS; + var d = genBuf[aOff + SLAB_ORIG_LINE] - genBuf[bOff + SLAB_ORIG_LINE]; + if (d !== 0) return d; + d = genBuf[aOff + SLAB_ORIG_COL] - genBuf[bOff + SLAB_ORIG_COL]; + if (d !== 0) return d; + d = genBuf[aOff + SLAB_GEN_COL] - genBuf[bOff + SLAB_GEN_COL]; + if (d !== 0) return d; + d = genBuf[aOff + SLAB_GEN_LINE] - genBuf[bOff + SLAB_GEN_LINE]; + if (d !== 0) return d; + return genBuf[aOff + SLAB_NAME_IDX] - genBuf[bOff + SLAB_NAME_IDX]; + }); + } + + for (var k = 0; k < n; k++) { + var genIdx = bucket[k]; + var srcOff = genIdx * SLAB_FIELDS; + var dstOff = dstRow * SLAB_FIELDS; + origBuf[dstOff + SLAB_GEN_LINE] = genBuf[srcOff + SLAB_GEN_LINE]; + origBuf[dstOff + SLAB_GEN_COL] = genBuf[srcOff + SLAB_GEN_COL]; + origBuf[dstOff + SLAB_SRC_IDX] = genBuf[srcOff + SLAB_SRC_IDX]; + origBuf[dstOff + SLAB_ORIG_LINE] = genBuf[srcOff + SLAB_ORIG_LINE]; + origBuf[dstOff + SLAB_ORIG_COL] = genBuf[srcOff + SLAB_ORIG_COL]; + origBuf[dstOff + SLAB_NAME_IDX] = genBuf[srcOff + SLAB_NAME_IDX]; + origToGen[dstRow] = genIdx; + dstRow++; + } } - return binarySearch.search(aNeedle, aMappings, aComparator, aBias); + this._origBuf = origBuf; + this._origCount = dstRow; + this._origToGen = origToGen; }; /** @@ -801,25 +914,30 @@ BasicSourceMapConsumer.prototype._findMapping = */ BasicSourceMapConsumer.prototype.computeColumnSpans = function SourceMapConsumer_computeColumnSpans() { - for (var index = 0; index < this._generatedMappings.length; ++index) { - var mapping = this._generatedMappings[index]; - - // Mappings do not contain a field for the last generated columnt. We - // can come up with an optimistic estimate, however, by assuming that - // mappings are contiguous (i.e. given two consecutive mappings, the - // first mapping ends where the second one starts). - if (index + 1 < this._generatedMappings.length) { - var nextMapping = this._generatedMappings[index + 1]; - - if (mapping.generatedLine === nextMapping.generatedLine) { - mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1; + if (this._genBuf === null) { + this._parseMappings(this._mappings, this.sourceRoot); + } + var buf = this._genBuf; + var n = this._genCount; + var lastCols = this._lastGenCols = new Int32Array(n); + // Sentinel -1 in the slab serializes back to `Infinity` at the callers + // that return `lastColumn` to user code (originalPositionFor / + // generatedPositionFor / allGeneratedPositionsFor / eachMapping). + for (var i = 0; i < n; i++) { + var off = i * SLAB_FIELDS; + if (i + 1 < n) { + var nextOff = (i + 1) * SLAB_FIELDS; + if (buf[off + SLAB_GEN_LINE] === buf[nextOff + SLAB_GEN_LINE]) { + lastCols[i] = buf[nextOff + SLAB_GEN_COL] - 1; continue; } } - - // The last mapping for each line spans the entire line. - mapping.lastGeneratedColumn = Infinity; + lastCols[i] = -1; } + // Invalidate any cached object-array view so a stale `_generatedMappings` + // getter doesn't return mappings without `lastGeneratedColumn`. + this.__generatedMappings = null; + this.__originalMappings = null; }; /** @@ -850,42 +968,46 @@ BasicSourceMapConsumer.prototype.originalPositionFor = function SourceMapConsumer_originalPositionFor(aArgs) { var needleLine = util.getArg(aArgs, 'line'); var needleColumn = util.getArg(aArgs, 'column'); - // `bias` is optional, defaulted to GLB. Inline the optional read — every - // trace call paid for the getArg function-call overhead. + // The legacy code path went through `_findMapping` which validated + // line/column at the entry. Slab-direct lookups bypass that, so + // we have to validate explicitly to keep the documented errors. + if (needleLine <= 0) { + throw new TypeError('Line must be greater than or equal to 1, got ' + needleLine); + } + if (needleColumn < 0) { + throw new TypeError('Column must be greater than or equal to 0, got ' + needleColumn); + } var bias = aArgs.bias != null ? aArgs.bias : SourceMapConsumer.GREATEST_LOWER_BOUND; - var mappings = this._generatedMappings; + + if (this._genBuf === null) { + this._parseMappings(this._mappings, this.sourceRoot); + } + var buf = this._genBuf; + var count = this._genCount; var index = -1; // Warm-start cache: ascending-column traces (the bundler walk pattern) - // repeatedly query the same line with growing columns. When the cache - // applies, run a bounded inline binary search on [cachedIndex, len) - // instead of the full haystack. binarySearch.search itself is left - // untouched so its V8 optimization profile is preserved on every other - // call site. Only used for GLB bias. + // repeatedly query the same line with growing columns. When applicable, + // run a bounded inline binary search on [cachedIndex, count). GLB only. if (bias === SourceMapConsumer.GREATEST_LOWER_BOUND && this._opfLine === needleLine && needleColumn >= this._opfColumn) { - // Cache invariant: mappings[_opfIndex] <= the previous needle on this - // line, so it is also <= the new needle (column has not decreased). - // GLB lies in [_opfIndex, len). Iterative bisect with inclusive lo. var lo = this._opfIndex; - var hi = mappings.length; + var hi = count; while (hi - lo > 1) { var mid = (lo + hi) >>> 1; - var m = mappings[mid]; - var cmp = m.generatedLine - needleLine; - if (cmp === 0) cmp = m.generatedColumn - needleColumn; + var off = mid * SLAB_FIELDS; + var cmp = buf[off + SLAB_GEN_LINE] - needleLine; + if (cmp === 0) cmp = buf[off + SLAB_GEN_COL] - needleColumn; if (cmp <= 0) lo = mid; else hi = mid; } - // Rewind through any tie cluster to match binarySearch's - // smallest-equal semantics. while (lo > 0) { - var cur = mappings[lo]; - var prev = mappings[lo - 1]; - if (prev.generatedLine !== cur.generatedLine || - prev.generatedColumn !== cur.generatedColumn) { + var aOff = lo * SLAB_FIELDS; + var bOff = (lo - 1) * SLAB_FIELDS; + if (buf[aOff + SLAB_GEN_LINE] !== buf[bOff + SLAB_GEN_LINE] || + buf[aOff + SLAB_GEN_COL] !== buf[bOff + SLAB_GEN_COL]) { break; } lo--; @@ -894,51 +1016,80 @@ BasicSourceMapConsumer.prototype.originalPositionFor = } if (index < 0) { - var needle = { - generatedLine: needleLine, - generatedColumn: needleColumn - }; - index = this._findMapping( - needle, - mappings, - "generatedLine", - "generatedColumn", - util.compareByGeneratedPositionsDeflated, - bias - ); + // Cold-path inline binary search on the slab. Same approach as the + // gpf inline cold path landed in PR #68: direct typed-array reads, + // no aCompare callback through binarySearch.search, no needle alloc. + if (bias === SourceMapConsumer.GREATEST_LOWER_BOUND) { + var lo = -1; + var hi = count; + while (hi - lo > 1) { + var mid = (lo + hi) >>> 1; + var off = mid * SLAB_FIELDS; + var cmp = buf[off + SLAB_GEN_LINE] - needleLine; + if (cmp === 0) cmp = buf[off + SLAB_GEN_COL] - needleColumn; + if (cmp <= 0) lo = mid; + else hi = mid; + } + if (lo >= 0) { + while (lo > 0) { + var aOff = lo * SLAB_FIELDS; + var bOff = (lo - 1) * SLAB_FIELDS; + if (buf[aOff + SLAB_GEN_LINE] !== buf[bOff + SLAB_GEN_LINE] || + buf[aOff + SLAB_GEN_COL] !== buf[bOff + SLAB_GEN_COL]) { + break; + } + lo--; + } + index = lo; + } + } else { + // LUB: smallest index whose value is >= needle. Rewind to smallest- + // equal to match binarySearch.search semantics. + var lo = -1; + var hi = count; + while (hi - lo > 1) { + var mid = (lo + hi) >>> 1; + var off = mid * SLAB_FIELDS; + var cmp = buf[off + SLAB_GEN_LINE] - needleLine; + if (cmp === 0) cmp = buf[off + SLAB_GEN_COL] - needleColumn; + if (cmp < 0) lo = mid; + else hi = mid; + } + if (hi < count) { + while (hi > 0) { + var aOff = hi * SLAB_FIELDS; + var bOff = (hi - 1) * SLAB_FIELDS; + if (buf[aOff + SLAB_GEN_LINE] !== buf[bOff + SLAB_GEN_LINE] || + buf[aOff + SLAB_GEN_COL] !== buf[bOff + SLAB_GEN_COL]) { + break; + } + hi--; + } + index = hi; + } + } } if (index >= 0) { - var mapping = mappings[index]; - - if (mapping.generatedLine === needleLine) { - // Update cache only when GLB found a mapping on the queried line — - // the case where a follow-up ascending query can short-circuit. + var off = index * SLAB_FIELDS; + if (buf[off + SLAB_GEN_LINE] === needleLine) { if (bias === SourceMapConsumer.GREATEST_LOWER_BOUND) { this._opfLine = needleLine; this._opfColumn = needleColumn; this._opfIndex = index; } - // _parseMappings always sets `source`, `name`, `originalLine`, and - // `originalColumn` — null when unset, integer index otherwise — so a - // direct property read is correct and skips the getArg `in` check. - var source = mapping.source; - if (source !== null) { - // _absoluteSources is precomputed at construction time as - // computeSourceURL(sourceRoot, _sources.at(i), sourceMapURL) for - // each i. Indexing into it skips the per-call URL parse + resolve. - source = this._absoluteSources[source]; - } - var name = mapping.name; - if (name !== null) { - name = this._names.at(name); - } + var srcIdx = buf[off + SLAB_SRC_IDX]; + var source = srcIdx === -1 ? null : this._absoluteSources[srcIdx]; + var nameIdx = buf[off + SLAB_NAME_IDX]; + var name = nameIdx === -1 ? null : this._names._array[nameIdx]; + var origLine = buf[off + SLAB_ORIG_LINE]; + var origCol = buf[off + SLAB_ORIG_COL]; return { source: source, - line: mapping.originalLine, - column: mapping.originalColumn, - name: name + line: origLine === -1 ? null : origLine, + column: origCol === -1 ? null : origCol, + name: name }; } } @@ -1053,17 +1204,29 @@ BasicSourceMapConsumer.prototype.generatedPositionFor = var needleLine = util.getArg(aArgs, 'line'); var needleColumn = util.getArg(aArgs, 'column'); - // `bias` is optional, defaulted to GLB. Inline the optional read. + if (needleLine <= 0) { + throw new TypeError('Line must be greater than or equal to 1, got ' + needleLine); + } + if (needleColumn < 0) { + throw new TypeError('Column must be greater than or equal to 0, got ' + needleColumn); + } var bias = aArgs.bias != null ? aArgs.bias : SourceMapConsumer.GREATEST_LOWER_BOUND; - var mappings = this._originalMappings; + + if (this._origBuf === null) { + if (this._genBuf === null) { + this._parseMappings(this._mappings, this.sourceRoot); + } + if (this._origBuf === null) { + this._buildOriginalMappings(); + } + } + var buf = this._origBuf; + var count = this._origCount; var index = -1; // Per-source warm-start cache: walks like `for (s of sources) gpf(s,L,C)` - // hit cache on every iteration after the first. On a hit we run a bounded - // inline binary search on [cachedIdx, len) instead of touching the full - // haystack. binarySearch.search itself is left untouched so its V8 - // optimization profile is preserved on every other call site. GLB only. + // hit cache on every iteration after the first. GLB only. if (bias === SourceMapConsumer.GREATEST_LOWER_BOUND) { var cache = this._gpfBySrc; if (cache === null) { @@ -1075,36 +1238,26 @@ BasicSourceMapConsumer.prototype.generatedPositionFor = if (cachedLine === needleLine && needleColumn >= cachedColumn) { if (needleColumn === cachedColumn) { - // Exact-match fast path: same (source, line, col) as the previous - // cached query, so the smallest-equal GLB index is unchanged. The - // common case for `for (s of sources) gpf(s, L, C)` walks where C - // is fixed. index = cache[slot + 2]; } else { - // Cache invariant: mappings[cachedIdx] is the smallest-equal GLB - // result of the previous query on this (source, line). Since the - // new needle is > the previous needle (column strictly greater), - // GLB lies in [cachedIdx, len). var lo = cache[slot + 2]; - var hi = mappings.length; + var hi = count; while (hi - lo > 1) { var mid = (lo + hi) >>> 1; - var m = mappings[mid]; + var off = mid * SLAB_FIELDS; var cmp; - if (m.source !== source) cmp = m.source - source; - else if (m.originalLine !== needleLine) cmp = m.originalLine - needleLine; - else cmp = m.originalColumn - needleColumn; + if (buf[off + SLAB_SRC_IDX] !== source) cmp = buf[off + SLAB_SRC_IDX] - source; + else if (buf[off + SLAB_ORIG_LINE] !== needleLine) cmp = buf[off + SLAB_ORIG_LINE] - needleLine; + else cmp = buf[off + SLAB_ORIG_COL] - needleColumn; if (cmp <= 0) lo = mid; else hi = mid; } - // Rewind through any tie cluster to match binarySearch's - // smallest-equal semantics. while (lo > 0) { - var cur = mappings[lo]; - var prev = mappings[lo - 1]; - if (prev.source !== cur.source || - prev.originalLine !== cur.originalLine || - prev.originalColumn !== cur.originalColumn) { + var aOff = lo * SLAB_FIELDS; + var bOff = (lo - 1) * SLAB_FIELDS; + if (buf[aOff + SLAB_SRC_IDX] !== buf[bOff + SLAB_SRC_IDX] || + buf[aOff + SLAB_ORIG_LINE] !== buf[bOff + SLAB_ORIG_LINE] || + buf[aOff + SLAB_ORIG_COL] !== buf[bOff + SLAB_ORIG_COL]) { break; } lo--; @@ -1116,32 +1269,25 @@ BasicSourceMapConsumer.prototype.generatedPositionFor = if (index < 0) { if (bias === SourceMapConsumer.GREATEST_LOWER_BOUND) { - // Cold-path inline GLB binary search. _originalMappings is sorted by - // (source, originalLine, originalColumn). Direct field access avoids - // the indirect aCompare callback through binarySearch.search and - // skips the per-call {source, originalLine, originalColumn} needle - // allocation that the _findMapping call site otherwise pays. var lo = -1; - var hi = mappings.length; + var hi = count; while (hi - lo > 1) { var mid = (lo + hi) >>> 1; - var m = mappings[mid]; + var off = mid * SLAB_FIELDS; var cmp; - if (m.source !== source) cmp = m.source - source; - else if (m.originalLine !== needleLine) cmp = m.originalLine - needleLine; - else cmp = m.originalColumn - needleColumn; + if (buf[off + SLAB_SRC_IDX] !== source) cmp = buf[off + SLAB_SRC_IDX] - source; + else if (buf[off + SLAB_ORIG_LINE] !== needleLine) cmp = buf[off + SLAB_ORIG_LINE] - needleLine; + else cmp = buf[off + SLAB_ORIG_COL] - needleColumn; if (cmp <= 0) lo = mid; else hi = mid; } if (lo >= 0) { - // Rewind through any tie cluster to match binarySearch.search's - // smallest-equal semantics. while (lo > 0) { - var cur = mappings[lo]; - var prev = mappings[lo - 1]; - if (prev.source !== cur.source || - prev.originalLine !== cur.originalLine || - prev.originalColumn !== cur.originalColumn) { + var aOff = lo * SLAB_FIELDS; + var bOff = (lo - 1) * SLAB_FIELDS; + if (buf[aOff + SLAB_SRC_IDX] !== buf[bOff + SLAB_SRC_IDX] || + buf[aOff + SLAB_ORIG_LINE] !== buf[bOff + SLAB_ORIG_LINE] || + buf[aOff + SLAB_ORIG_COL] !== buf[bOff + SLAB_ORIG_COL]) { break; } lo--; @@ -1149,45 +1295,66 @@ BasicSourceMapConsumer.prototype.generatedPositionFor = index = lo; } } else { - var needle = { - source: source, - originalLine: needleLine, - originalColumn: needleColumn - }; - index = this._findMapping( - needle, - mappings, - "originalLine", - "originalColumn", - util.compareByOriginalPositions, - bias - ); + // LUB cold path: smallest index with value >= needle, then rewind + // through ties for smallest-equal. + var lo = -1; + var hi = count; + while (hi - lo > 1) { + var mid = (lo + hi) >>> 1; + var off = mid * SLAB_FIELDS; + var cmp; + if (buf[off + SLAB_SRC_IDX] !== source) cmp = buf[off + SLAB_SRC_IDX] - source; + else if (buf[off + SLAB_ORIG_LINE] !== needleLine) cmp = buf[off + SLAB_ORIG_LINE] - needleLine; + else cmp = buf[off + SLAB_ORIG_COL] - needleColumn; + if (cmp < 0) lo = mid; + else hi = mid; + } + if (hi < count) { + while (hi > 0) { + var aOff = hi * SLAB_FIELDS; + var bOff = (hi - 1) * SLAB_FIELDS; + if (buf[aOff + SLAB_SRC_IDX] !== buf[bOff + SLAB_SRC_IDX] || + buf[aOff + SLAB_ORIG_LINE] !== buf[bOff + SLAB_ORIG_LINE] || + buf[aOff + SLAB_ORIG_COL] !== buf[bOff + SLAB_ORIG_COL]) { + break; + } + hi--; + } + index = hi; + } } } if (index >= 0) { - var mapping = mappings[index]; - - if (mapping.source === source) { - // Update cache only when GLB found a mapping on the queried - // (source, line) — the case where a follow-up ascending query can - // short-circuit. + var off = index * SLAB_FIELDS; + if (buf[off + SLAB_SRC_IDX] === source) { if (bias === SourceMapConsumer.GREATEST_LOWER_BOUND && - mapping.originalLine === needleLine) { + buf[off + SLAB_ORIG_LINE] === needleLine) { var s = source * 3; this._gpfBySrc[s] = needleLine; this._gpfBySrc[s + 1] = needleColumn; this._gpfBySrc[s + 2] = index; } - // generatedLine and generatedColumn are always set by _parseMappings; - // lastGeneratedColumn is added optionally by computeColumnSpans, so a - // direct read with a null fallback covers both the missing and - // explicitly-null cases the same way getArg(...,'',null) did. + // lastGeneratedColumn is stored in the parallel _lastGenCols slab if + // computeColumnSpans has been called; -1 sentinel means "extends to + // end of line". The legacy API returned `null` for either-not-set, so + // we mirror that. + var lastCol = null; + if (this._lastGenCols !== null) { + // _origBuf rows reference back to _genBuf positions only by + // (genLine, genCol). Look up the corresponding gen-slab index for + // this slot via a small per-row map built alongside _lastGenCols. + var lastIdx = this._origToGen !== null ? this._origToGen[index] : -1; + if (lastIdx >= 0) { + var v = this._lastGenCols[lastIdx]; + lastCol = v === -1 ? Infinity : v; + } + } return { - line: mapping.generatedLine, - column: mapping.generatedColumn, - lastColumn: mapping.lastGeneratedColumn != null ? mapping.lastGeneratedColumn : null + line: buf[off + SLAB_GEN_LINE], + column: buf[off + SLAB_GEN_COL], + lastColumn: lastCol }; } } @@ -1473,89 +1640,107 @@ IndexedSourceMapConsumer.prototype.generatedPositionFor = */ IndexedSourceMapConsumer.prototype._parseMappings = function IndexedSourceMapConsumer_parseMappings(aStr, aSourceRoot) { - this.__generatedMappings = []; - // Bucket original-side mappings by source index. Sorting per bucket with - // `compareByOriginalPositionsNoSource` avoids the `strcmp(source, source)` - // primary key on every comparison — same pattern as - // BasicSourceMapConsumer._buildOriginalMappings. - var originalBuckets = []; + // Pre-sum section mapping counts so we can size the merged _genBuf + // exactly. Triggers each section's lazy parse if it hasn't run yet. + var totalCount = 0; for (var i = 0; i < this._sections.length; i++) { - var section = this._sections[i]; - var sectionMappings = section.consumer._generatedMappings; - for (var j = 0; j < sectionMappings.length; j++) { - var mapping = sectionMappings[j]; - - var source = section.consumer._sources.at(mapping.source); - if(source !== null) { - source = util.computeSourceURL(section.consumer.sourceRoot, source, this._sourceMapURL); - } - source = this._sources.add(source); + var sc = this._sections[i].consumer; + if (sc._genBuf === null) { + sc._parseMappings(sc._mappings, sc.sourceRoot); + } + totalCount += sc._genCount; + } - var name = null; - if (mapping.name) { - name = this._names.add(section.consumer._names.at(mapping.name)); - } + var genBuf = new Int32Array(totalCount * SLAB_FIELDS); + var count = 0; + var sourcesSet = this._sources; + var namesSet = this._names; - // The mappings coming from the consumer for the section have - // generated positions relative to the start of the section, so we - // need to offset them to be relative to the start of the concatenated - // generated file. - var adjustedMapping = { - source: source, - generatedLine: mapping.generatedLine + - (section.generatedOffset.generatedLine - 1), - generatedColumn: mapping.generatedColumn + - (section.generatedOffset.generatedLine === mapping.generatedLine - ? section.generatedOffset.generatedColumn - 1 - : 0), - originalLine: mapping.originalLine, - originalColumn: mapping.originalColumn, - name: name - }; - - this.__generatedMappings.push(adjustedMapping); - if (typeof adjustedMapping.originalLine === 'number') { - while (originalBuckets.length <= source) { - originalBuckets.push(null); - } - if (originalBuckets[source] === null) { - originalBuckets[source] = []; - } - originalBuckets[source].push(adjustedMapping); + for (var i = 0; i < this._sections.length; i++) { + var section = this._sections[i]; + var sc = section.consumer; + var sectionGenLine = section.generatedOffset.generatedLine; + var lineOffset = sectionGenLine - 1; + var colOffset = section.generatedOffset.generatedColumn - 1; + var scBuf = sc._genBuf; + var scCount = sc._genCount; + // `sc._absoluteSources[i]` is the absolute URL the section's source at + // index i resolves to under the indexed map's `_sourceMapURL` + // (BasicSourceMapConsumer's constructor and IndexedSourceMapConsumer's + // _parseMappings both compute through `computeSourceURL` with the + // outer URL). Reading the precomputed value here saves a per-mapping + // URL parse + resolve, matching the eachMapping fast path. + var scAbsSources = sc._absoluteSources; + var scNameArray = sc._names._array; + + for (var j = 0; j < scCount; j++) { + var off = j * SLAB_FIELDS; + var scSrcIdx = scBuf[off + SLAB_SRC_IDX]; + var ourSrcIdx; + if (scSrcIdx === -1) { + ourSrcIdx = sourcesSet.add(null); + } else { + ourSrcIdx = sourcesSet.add(scAbsSources[scSrcIdx]); } + var scNameIdx = scBuf[off + SLAB_NAME_IDX]; + var ourNameIdx = scNameIdx === -1 + ? -1 + : namesSet.add(scNameArray[scNameIdx]); + + var scGenLine = scBuf[off + SLAB_GEN_LINE]; + var adjGenLine = scGenLine + lineOffset; + var adjGenCol = scBuf[off + SLAB_GEN_COL] + + (sectionGenLine === scGenLine ? colOffset : 0); + + var dstOff = count * SLAB_FIELDS; + genBuf[dstOff + SLAB_GEN_LINE] = adjGenLine; + genBuf[dstOff + SLAB_GEN_COL] = adjGenCol; + genBuf[dstOff + SLAB_SRC_IDX] = ourSrcIdx; + genBuf[dstOff + SLAB_ORIG_LINE] = scBuf[off + SLAB_ORIG_LINE]; + genBuf[dstOff + SLAB_ORIG_COL] = scBuf[off + SLAB_ORIG_COL]; + genBuf[dstOff + SLAB_NAME_IDX] = ourNameIdx; + count++; } } - // `_sources` is populated with already-absolute URLs (lines above resolve - // each section's source through `computeSourceURL`), so the absolute view - // is simply the ArraySet's toArray. Mirroring the field that - // BasicSourceMapConsumer sets in its constructor lets `eachMapping` index - // into `_absoluteSources` uniformly across consumer types. + // _sources holds absolute URLs after the section-merge above, so the + // absolute view is just the ArraySet's toArray — same shape as + // BasicSourceMapConsumer's constructor populates. this._absoluteSources = this._sources.toArray(); - quickSort(this.__generatedMappings, util.compareByGeneratedPositionsDeflated); - - var nonNullBuckets = []; - var compareOriginal = util.compareByOriginalPositionsNoSource; - for (var k = 0; k < originalBuckets.length; k++) { - var perSource = originalBuckets[k]; - if (perSource != null) { - // Well-formed input usually emits per-source mappings already in - // original-position order — probe and skip the sort when it is. - var sorted = true; - for (var m = 1; m < perSource.length; m++) { - if (compareOriginal(perSource[m - 1], perSource[m]) > 0) { - sorted = false; - break; - } - } - if (!sorted) { - quickSort(perSource, compareOriginal); - } - nonNullBuckets.push(perSource); + // Sort merged rows by (generatedLine, generatedColumn). Sections may + // overlap each other, so a single global sort is required (per-line + // sortGeneratedSlab won't do). Use a permutation array to avoid 6×count + // row swaps; quicksort over the perm is a few seconds even for vscode. + if (count > 1) { + var perm = new Array(count); + for (var p = 0; p < count; p++) perm[p] = p; + perm.sort(function (a, b) { + var aOff = a * SLAB_FIELDS; + var bOff = b * SLAB_FIELDS; + var d = genBuf[aOff + SLAB_GEN_LINE] - genBuf[bOff + SLAB_GEN_LINE]; + if (d !== 0) return d; + return genBuf[aOff + SLAB_GEN_COL] - genBuf[bOff + SLAB_GEN_COL]; + }); + var sorted = new Int32Array(count * SLAB_FIELDS); + for (var k = 0; k < count; k++) { + var srcOff = perm[k] * SLAB_FIELDS; + var dstOff = k * SLAB_FIELDS; + sorted[dstOff + SLAB_GEN_LINE] = genBuf[srcOff + SLAB_GEN_LINE]; + sorted[dstOff + SLAB_GEN_COL] = genBuf[srcOff + SLAB_GEN_COL]; + sorted[dstOff + SLAB_SRC_IDX] = genBuf[srcOff + SLAB_SRC_IDX]; + sorted[dstOff + SLAB_ORIG_LINE] = genBuf[srcOff + SLAB_ORIG_LINE]; + sorted[dstOff + SLAB_ORIG_COL] = genBuf[srcOff + SLAB_ORIG_COL]; + sorted[dstOff + SLAB_NAME_IDX] = genBuf[srcOff + SLAB_NAME_IDX]; } + genBuf = sorted; } - this.__originalMappings = [].concat(...nonNullBuckets); + + this._genBuf = genBuf; + this._genCount = count; + + // Build _origBuf from the now-populated _genBuf via the shared method. + BasicSourceMapConsumer.prototype._buildOriginalMappings.call(this); }; exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer;