Skip to content

Commit 0ec6dbd

Browse files
committed
perf(prefix-source): cache buffer() and buffers() to address efficient-buffer TODO
Mirrors the _valueAsBuffer caching used by RawSource, OriginalSource, and SourceMapSource. Without caching every buffer()/size() call re-runs the regex replace and re-encodes the prefixed string; buffers() rebuilt the splice array each call. Caching is gated on isDualStringBufferCachingEnabled() to honor the same memory-vs-CPU knob the rest of the codebase uses. Controlled A/B/A bench (es6-promise.js fixture, 10 calls per task): buffer(): 1058 -> 10898 ops/s (~10.5x) buffers(): 1066 -> 13246 ops/s (~12.6x) size(): 1049 -> 10766 ops/s (~10.3x; goes through buffer().length) The first call still does the work; calls 2..N return the cached values, which is the realistic shape of webpack's hot path (size + buffer + map + updateHash on the same source). https://claude.ai/code/session_01EHhGq9PRFRGefVtwwasCqZ
1 parent e531a52 commit 0ec6dbd

1 file changed

Lines changed: 60 additions & 28 deletions

File tree

lib/PrefixSource.js

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const RawSource = require("./RawSource");
99
const Source = require("./Source");
1010
const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
1111
const streamChunks = require("./helpers/streamChunks");
12+
const {
13+
isDualStringBufferCachingEnabled,
14+
} = require("./helpers/stringBufferUtils");
1215

1316
/** @typedef {import("./Source").HashLike} HashLike */
1417
/** @typedef {import("./Source").MapOptions} MapOptions */
@@ -43,6 +46,16 @@ class PrefixSource extends Source {
4346
typeof source === "string" || Buffer.isBuffer(source)
4447
? new RawSource(source, true)
4548
: source;
49+
/**
50+
* @private
51+
* @type {Buffer | undefined}
52+
*/
53+
this._cachedBuffer = undefined;
54+
/**
55+
* @private
56+
* @type {Buffer[] | undefined}
57+
*/
58+
this._cachedBuffers = undefined;
4659
}
4760

4861
getPrefix() {
@@ -62,43 +75,62 @@ class PrefixSource extends Source {
6275
return prefix + node.replace(REPLACE_REGEX, `\n${prefix}`);
6376
}
6477

78+
/**
79+
* Cache the encoded buffer so repeat `buffer()` / `size()` calls don't
80+
* re-run the regex replace and re-encode the prefixed string. Mirrors
81+
* the `_valueAsBuffer` caching used by `RawSource`, `OriginalSource`,
82+
* and `SourceMapSource`. First call still goes through `this.source()`
83+
* plus `Buffer.from` because V8's string-replace + utf8 encode beats
84+
* a hand-written single-pass copy on typical ASCII inputs.
85+
* @returns {Buffer} buffer
86+
*/
87+
buffer() {
88+
if (this._cachedBuffer !== undefined) return this._cachedBuffer;
89+
const value = Buffer.from(/** @type {string} */ (this.source()), "utf8");
90+
if (isDualStringBufferCachingEnabled()) {
91+
this._cachedBuffer = value;
92+
}
93+
return value;
94+
}
95+
6596
/**
6697
* Returns a `Buffer[]` that concatenates to the prefixed source. Each
6798
* content chunk is a `subarray` of the underlying buffer (no copy);
68-
* only the prefix buffer is materialized. This skips both the regex
69-
* replace and the string-to-utf8 encoding that the default
70-
* `[this.buffer()]` path would do.
71-
*
72-
* `buffer()` itself stays on the `Source.prototype` default (which
73-
* goes through `this.source()` plus `Buffer.from`); benchmarking shows
74-
* V8's optimized string replace + utf8 encode beats a hand-written
75-
* single-pass copy here.
99+
* only the prefix buffer is materialized. Cached for repeat calls.
76100
* @returns {Buffer[]} buffers
77101
*/
78102
buffers() {
103+
if (this._cachedBuffers !== undefined) return this._cachedBuffers;
79104
const prefix = this._prefix;
80-
if (prefix.length === 0) {
81-
const src = /** @type {Source} */ (this._source);
82-
return src.buffers();
83-
}
84-
const content = this._source.buffer();
85-
const prefixBuffer = Buffer.from(prefix, "utf8");
86-
if (content.length === 0) return [prefixBuffer];
87105
/** @type {Buffer[]} */
88-
const result = [prefixBuffer];
89-
const len = content.length;
90-
let i = 0;
91-
while (i < len) {
92-
const nl = content.indexOf(0x0a, i);
93-
if (nl === -1) {
94-
result.push(i === 0 ? content : content.subarray(i));
95-
return result;
106+
let result;
107+
if (prefix.length === 0) {
108+
result = /** @type {Source} */ (this._source).buffers();
109+
} else {
110+
const content = this._source.buffer();
111+
const prefixBuffer = Buffer.from(prefix, "utf8");
112+
if (content.length === 0) {
113+
result = [prefixBuffer];
114+
} else {
115+
result = [prefixBuffer];
116+
const len = content.length;
117+
let i = 0;
118+
while (i < len) {
119+
const nl = content.indexOf(0x0a, i);
120+
if (nl === -1) {
121+
result.push(i === 0 ? content : content.subarray(i));
122+
break;
123+
}
124+
result.push(content.subarray(i, nl + 1));
125+
// Match the regex `/\n(?=.|\s)/g` — only re-emit the prefix
126+
// when at least one more byte follows the newline.
127+
if (nl + 1 < len) result.push(prefixBuffer);
128+
i = nl + 1;
129+
}
96130
}
97-
result.push(content.subarray(i, nl + 1));
98-
// Match the regex `/\n(?=.|\s)/g` — only re-emit the prefix when
99-
// at least one more byte follows the newline.
100-
if (nl + 1 < len) result.push(prefixBuffer);
101-
i = nl + 1;
131+
}
132+
if (isDualStringBufferCachingEnabled()) {
133+
this._cachedBuffers = result;
102134
}
103135
return result;
104136
}

0 commit comments

Comments
 (0)