Skip to content

Commit 58420d1

Browse files
fix: improve performance in many places (#199)
1 parent 2081a31 commit 58420d1

8 files changed

Lines changed: 237 additions & 151 deletions

lib/OriginalSource.js

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
const Source = require("./Source");
99
const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
1010
const getGeneratedSourceInfo = require("./helpers/getGeneratedSourceInfo");
11-
const splitIntoLines = require("./helpers/splitIntoLines");
1211
const splitIntoPotentialTokens = require("./helpers/splitIntoPotentialTokens");
1312
const {
1413
isDualStringBufferCachingEnabled,
@@ -50,6 +49,11 @@ class OriginalSource extends Source {
5049
* @type {string}
5150
*/
5251
this._name = name;
52+
/**
53+
* @private
54+
* @type {undefined | number}
55+
*/
56+
this._cachedSize = undefined;
5357
}
5458

5559
getName() {
@@ -83,6 +87,23 @@ class OriginalSource extends Source {
8387
return this._valueAsBuffer;
8488
}
8589

90+
/**
91+
* Override Source.prototype.size (= `this.buffer().length`) to avoid
92+
* allocating a Buffer just to read the byte length, and memoize the
93+
* result so repeated calls don't re-scan the string.
94+
* @returns {number} byte length
95+
*/
96+
size() {
97+
if (this._cachedSize !== undefined) return this._cachedSize;
98+
if (this._valueAsBuffer !== undefined) {
99+
return (this._cachedSize = this._valueAsBuffer.length);
100+
}
101+
return (this._cachedSize = Buffer.byteLength(
102+
/** @type {string} */ (this._value),
103+
"utf8",
104+
));
105+
}
106+
86107
/**
87108
* @param {MapOptions=} options map options
88109
* @returns {RawSourceMap | null} map
@@ -165,27 +186,36 @@ class OriginalSource extends Source {
165186
}
166187
return result;
167188
}
168-
// Without column info, but also without final source
169-
// we need to split source by lines
189+
// Without column info, but also without final source.
190+
// We only get here when (options.columns === false && !finalSource),
191+
// so the source field is always undefined and the chunk arg is always
192+
// the line text. Single-pass scan over newlines avoids the
193+
// splitIntoLines array allocation.
194+
const value = this._value;
195+
const len = value.length;
196+
if (len === 0) {
197+
return { generatedLine: 1, generatedColumn: 0, source: undefined };
198+
}
170199
let line = 1;
171-
const matches = splitIntoLines(this._value);
172-
/** @type {string | undefined} */
173-
let match;
174-
for (match of matches) {
175-
onChunk(finalSource ? undefined : match, line, 0, 0, line, 0, -1);
200+
let i = 0;
201+
while (i < len) {
202+
const n = value.indexOf("\n", i);
203+
if (n === -1) {
204+
const lastLine = i === 0 ? value : value.slice(i);
205+
onChunk(lastLine, line, 0, 0, line, 0, -1);
206+
return {
207+
generatedLine: line,
208+
generatedColumn: lastLine.length,
209+
source: undefined,
210+
};
211+
}
212+
const chunk = n === i ? "\n" : value.slice(i, n + 1);
213+
onChunk(chunk, line, 0, 0, line, 0, -1);
176214
line++;
215+
i = n + 1;
177216
}
178-
return matches.length === 0 || /** @type {string} */ (match).endsWith("\n")
179-
? {
180-
generatedLine: matches.length + 1,
181-
generatedColumn: 0,
182-
source: finalSource ? this._value : undefined,
183-
}
184-
: {
185-
generatedLine: matches.length,
186-
generatedColumn: /** @type {string} */ (match).length,
187-
source: finalSource ? this._value : undefined,
188-
};
217+
// Source ended with a newline.
218+
return { generatedLine: line, generatedColumn: 0, source: undefined };
189219
}
190220

191221
/**

lib/RawSource.js

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,41 @@ class RawSource extends Source {
3030
constructor(value, convertToString = false) {
3131
super();
3232
const isBuffer = Buffer.isBuffer(value);
33-
if (!isBuffer && typeof value !== "string") {
33+
if (isBuffer) {
34+
/**
35+
* @private
36+
* @type {boolean}
37+
*/
38+
this._valueIsBuffer = !convertToString;
39+
/**
40+
* @private
41+
* @type {undefined | string | Buffer}
42+
*/
43+
this._value = convertToString ? undefined : value;
44+
/**
45+
* @private
46+
* @type {undefined | Buffer}
47+
*/
48+
this._valueAsBuffer = value;
49+
/**
50+
* @private
51+
* @type {undefined | string}
52+
*/
53+
this._valueAsString = undefined;
54+
} else if (typeof value === "string") {
55+
const interned = internString(value);
56+
this._valueIsBuffer = false;
57+
this._value = interned;
58+
this._valueAsBuffer = undefined;
59+
this._valueAsString = interned;
60+
} else {
3461
throw new TypeError("argument 'value' must be either string or Buffer");
3562
}
3663
/**
3764
* @private
38-
* @type {boolean}
65+
* @type {undefined | number}
3966
*/
40-
this._valueIsBuffer = !convertToString && isBuffer;
41-
const internedString =
42-
typeof value === "string" ? internString(value) : undefined;
43-
/**
44-
* @private
45-
* @type {undefined | string | Buffer}
46-
*/
47-
this._value =
48-
convertToString && isBuffer
49-
? undefined
50-
: typeof value === "string"
51-
? internedString
52-
: value;
53-
/**
54-
* @private
55-
* @type {undefined | Buffer}
56-
*/
57-
this._valueAsBuffer = isBuffer ? value : undefined;
58-
/**
59-
* @private
60-
* @type {undefined | string}
61-
*/
62-
this._valueAsString = isBuffer ? undefined : internedString;
67+
this._cachedSize = undefined;
6368
}
6469

6570
isBuffer() {
@@ -93,6 +98,23 @@ class RawSource extends Source {
9398
return this._valueAsBuffer;
9499
}
95100

101+
/**
102+
* Override Source.prototype.size (= `this.buffer().length`) to avoid
103+
* allocating a Buffer just to read the byte length, and memoize the
104+
* result so repeated calls don't re-scan the string.
105+
* @returns {number} byte length
106+
*/
107+
size() {
108+
if (this._cachedSize !== undefined) return this._cachedSize;
109+
if (this._valueAsBuffer !== undefined) {
110+
return (this._cachedSize = this._valueAsBuffer.length);
111+
}
112+
return (this._cachedSize = Buffer.byteLength(
113+
/** @type {string} */ (this._valueAsString),
114+
"utf8",
115+
));
116+
}
117+
96118
/**
97119
* @param {MapOptions=} options map options
98120
* @returns {RawSourceMap | null} map

lib/ReplaceSource.js

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ const hasStableSort =
3131
// This is larger than max string length
3232
const MAX_SOURCE_POSITION = 0x20000000;
3333

34+
/**
35+
* Stable comparator hoisted to module scope so each `_sortReplacements()`
36+
* call doesn't allocate a fresh closure.
37+
* @param {Replacement} a a
38+
* @param {Replacement} b b
39+
* @returns {number} order
40+
*/
41+
const compareStable = (a, b) => {
42+
const diff1 = a.start - b.start;
43+
if (diff1 !== 0) return diff1;
44+
const diff2 = a.end - b.end;
45+
if (diff2 !== 0) return diff2;
46+
return 0;
47+
};
48+
49+
/**
50+
* Index-stabilising comparator for v8 < 7.0 (pre-stable Array.prototype.sort).
51+
* @param {Replacement} a a
52+
* @param {Replacement} b b
53+
* @returns {number} order
54+
*/
55+
const compareUnstableFallback = (a, b) => {
56+
const diff1 = a.start - b.start;
57+
if (diff1 !== 0) return diff1;
58+
const diff2 = a.end - b.end;
59+
if (diff2 !== 0) return diff2;
60+
return /** @type {number} */ (a.index) - /** @type {number} */ (b.index);
61+
};
62+
3463
class Replacement {
3564
/**
3665
* @param {number} start start
@@ -124,7 +153,7 @@ class ReplaceSource extends Source {
124153
if (this._replacements.length === 0) {
125154
return this._source.source();
126155
}
127-
let current = this._source.source();
156+
const current = /** @type {string} */ (this._source.source());
128157
let pos = 0;
129158
const result = [];
130159

@@ -133,19 +162,19 @@ class ReplaceSource extends Source {
133162
const start = Math.floor(replacement.start);
134163
const end = Math.floor(replacement.end + 1);
135164
if (pos < start) {
136-
const offset = start - pos;
137-
result.push(current.slice(0, offset));
138-
current = current.slice(offset);
165+
// slice directly from the original string rather than repeatedly
166+
// producing smaller intermediate strings, which avoids O(n) copies.
167+
result.push(current.slice(pos, start));
139168
pos = start;
140169
}
141170
result.push(replacement.content);
142171
if (pos < end) {
143-
const offset = end - pos;
144-
current = current.slice(offset);
145172
pos = end;
146173
}
147174
}
148-
result.push(current);
175+
if (pos < current.length) {
176+
result.push(pos === 0 ? current : current.slice(pos));
177+
}
149178
return result.join("");
150179
}
151180

@@ -178,24 +207,10 @@ class ReplaceSource extends Source {
178207
_sortReplacements() {
179208
if (this._isSorted) return;
180209
if (hasStableSort) {
181-
this._replacements.sort((a, b) => {
182-
const diff1 = a.start - b.start;
183-
if (diff1 !== 0) return diff1;
184-
const diff2 = a.end - b.end;
185-
if (diff2 !== 0) return diff2;
186-
return 0;
187-
});
210+
this._replacements.sort(compareStable);
188211
} else {
189212
for (const [i, repl] of this._replacements.entries()) repl.index = i;
190-
this._replacements.sort((a, b) => {
191-
const diff1 = a.start - b.start;
192-
if (diff1 !== 0) return diff1;
193-
const diff2 = a.end - b.end;
194-
if (diff2 !== 0) return diff2;
195-
return (
196-
/** @type {number} */ (a.index) - /** @type {number} */ (b.index)
197-
);
198-
});
213+
this._replacements.sort(compareUnstableFallback);
199214
}
200215
this._isSorted = true;
201216
}
@@ -545,10 +560,14 @@ class ReplaceSource extends Source {
545560
hash.update("ReplaceSource");
546561
this._source.updateHash(hash);
547562
hash.update(this._name || "");
563+
// Feed each replacement as multiple updates instead of building one
564+
// combined template literal per replacement. The resulting digest is
565+
// identical (hash.update is additive over bytes), but we avoid
566+
// allocating a new string per replacement.
548567
for (const repl of this._replacements) {
549-
hash.update(
550-
`${repl.start}${repl.end}${repl.content}${repl.name ? repl.name : ""}`,
551-
);
568+
hash.update(`${repl.start}${repl.end}`);
569+
hash.update(repl.content);
570+
if (repl.name) hash.update(repl.name);
552571
}
553572
}
554573
}

lib/SourceMapSource.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ class SourceMapSource extends Source {
113113
: undefined;
114114

115115
this._removeOriginalSource = removeOriginalSource;
116+
/**
117+
* @private
118+
* @type {undefined | number}
119+
*/
120+
this._cachedSize = undefined;
116121
}
117122

118123
/**
@@ -143,6 +148,23 @@ class SourceMapSource extends Source {
143148
return this._valueAsBuffer;
144149
}
145150

151+
/**
152+
* Override Source.prototype.size (= `this.buffer().length`) to avoid
153+
* allocating a Buffer just to read the byte length, and memoize the
154+
* result so repeated calls don't re-scan the string.
155+
* @returns {number} byte length
156+
*/
157+
size() {
158+
if (this._cachedSize !== undefined) return this._cachedSize;
159+
if (this._valueAsBuffer !== undefined) {
160+
return (this._cachedSize = this._valueAsBuffer.length);
161+
}
162+
return (this._cachedSize = Buffer.byteLength(
163+
/** @type {string} */ (this._valueAsString),
164+
"utf8",
165+
));
166+
}
167+
146168
/**
147169
* @returns {SourceValue} source
148170
*/

0 commit comments

Comments
 (0)