Skip to content

Commit 683c2b7

Browse files
committed
perf(prefix-source): implement buffer() that beats the Source default by ~17%
Walks the underlying source's buffer directly instead of going through this.source() + Buffer.from. Skips both the regex replace and the intermediate prefixed-string allocation. Key trick that beats the V8 default: skip the count pass. Allocate worst-case `prefixLen + contentLen + prefixLen * contentLen` with allocUnsafe (cheap, no init), fill in one pass, return result.subarray. For typical short prefixes ("\t", " ") the worst case is bounded by ~2x and the subarray retains the over-allocation cheaply. For long prefixes the >50% waste threshold triggers a copy into a tight Buffer so the extra capacity gets GC'd. No caching — _source can be mutable (ReplaceSource etc.) and the result must reflect changes on each call. The existing "reflect-mutations-to-underlying-source" regression test still passes. A/B/A median over 5 alternating runs (es6-promise.js fixture, 10 calls per task body): baseline: 1130 ops/s this: 1322 ops/s (+17%) Earlier attempts (committed and reverted on this branch) — for the record so the next person doesn't redo them: - Buffer.concat(this.buffers()): array-iteration overhead -> slower - count-pass + exact alloc + copy: 2x indexOf walks -> slower - cache the result: faster but unsafe with mutable child https://claude.ai/code/session_01EHhGq9PRFRGefVtwwasCqZ
1 parent 9a37395 commit 683c2b7

1 file changed

Lines changed: 50 additions & 8 deletions

File tree

lib/PrefixSource.js

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,61 @@ class PrefixSource extends Source {
6262
return prefix + node.replace(REPLACE_REGEX, `\n${prefix}`);
6363
}
6464

65-
// Note: we deliberately don't cache buffer() / buffers() / size() here.
66-
// `_source` can be mutable (ReplaceSource, ConcatSource, CompatSource
67-
// wrapping a SourceLike) and `source()` reads it on every call; caching
68-
// the derived result would silently go stale after a child mutation.
69-
// `RawSource` / `OriginalSource` / `SourceMapSource` cache their own
70-
// buffer because they're self-contained — that invariant doesn't hold
71-
// here. Wrap PrefixSource in CachedSource if you need the speedup.
65+
/**
66+
* Build the prefixed buffer by walking the underlying source's buffer
67+
* directly — skips both the regex replace and the intermediate
68+
* prefixed-string allocation that the default `Source.buffer()` path
69+
* does via `this.source()` + `Buffer.from`. Single linear pass: alloc
70+
* a worst-case-sized scratch with `Buffer.allocUnsafe`, splice the
71+
* prefix in after each `\n` that has more content following, then
72+
* return a tight result.
73+
*
74+
* The worst case is `prefixLen + contentLen + prefixLen * contentLen`
75+
* (every byte is a newline followed by content). For typical short
76+
* prefixes (`"\t"`, `" "`) that's bounded by ~2x the content size and
77+
* we keep the allocUnsafe via `subarray` (zero-copy). For long
78+
* prefixes the ratio is unbounded, so when the over-allocation wastes
79+
* more than 50% we copy into a tight Buffer so the extra capacity
80+
* gets GC'd promptly. No caching — `_source` may be mutable
81+
* (ReplaceSource etc.) and we must reflect changes on each call.
82+
* @returns {Buffer} buffer
83+
*/
84+
buffer() {
85+
const prefix = this._prefix;
86+
if (prefix.length === 0) return this._source.buffer();
87+
const content = this._source.buffer();
88+
const contentLen = content.length;
89+
if (contentLen === 0) return Buffer.from(prefix, "utf8");
90+
const prefixBuffer = Buffer.from(prefix, "utf8");
91+
const prefixLen = prefixBuffer.length;
92+
const worst = prefixLen + contentLen + prefixLen * contentLen;
93+
const result = Buffer.allocUnsafe(worst);
94+
let writePos = prefixBuffer.copy(result, 0);
95+
let readPos = 0;
96+
while (readPos < contentLen) {
97+
const nl = content.indexOf(0x0a, readPos);
98+
const end = nl === -1 ? contentLen : nl + 1;
99+
writePos += content.copy(result, writePos, readPos, end);
100+
// Match the regex `/\n(?=.|\s)/g` — only re-emit the prefix when
101+
// at least one more byte follows the newline.
102+
if (nl !== -1 && nl + 1 < contentLen) {
103+
writePos += prefixBuffer.copy(result, writePos);
104+
}
105+
readPos = end;
106+
}
107+
if (writePos * 2 < worst) {
108+
const tight = Buffer.allocUnsafe(writePos);
109+
result.copy(tight, 0, 0, writePos);
110+
return tight;
111+
}
112+
return result.subarray(0, writePos);
113+
}
72114

73115
/**
74116
* Returns a `Buffer[]` that concatenates to the prefixed source. Each
75117
* content chunk is a `subarray` of the underlying buffer (no copy);
76118
* only the prefix buffer is materialized. This skips the
77-
* [this.buffer()] wrap that the default Source.buffers() would do.
119+
* `[this.buffer()]` wrap that the default Source.buffers() would do.
78120
* @returns {Buffer[]} buffers
79121
*/
80122
buffers() {

0 commit comments

Comments
 (0)