106 lines
4.1 KiB
JavaScript
106 lines
4.1 KiB
JavaScript
/**
|
|
* Growable little-endian byte buffer shared by the op-stream and command-buffer
|
|
* encoders — the two hottest write paths in the package.
|
|
*
|
|
* Subclasses write single bytes directly through the protected `buf`/`n`
|
|
* fields after an `ensure` for the whole record (one bounds check per record,
|
|
* not per byte, and no per-byte call in unoptimized tiers — CodSpeed's
|
|
* Simulation mode runs too few iterations for V8 to inline method calls).
|
|
* Only the non-trivial logic lives here: growth, u32 writes, and UTF-8
|
|
* string encoding.
|
|
*/
|
|
const encoder = new TextEncoder();
|
|
const EMPTY = new Uint8Array(0);
|
|
/** Below this length the inline char-code copy beats `encodeInto`'s call
|
|
* overhead; above it the native bulk path wins (and skips the ASCII scan).
|
|
* Measured crossover ~16 bytes (Node 24: 8B 15 vs 41 ns, 16B 37 vs 43,
|
|
* 32B 73 vs 52). */
|
|
const INLINE_STR_MAX = 16;
|
|
export class ByteWriter {
|
|
// Backing allocates on first write: visitor passes construct buffers that
|
|
// often never receive a byte (read-only plugins).
|
|
buf = EMPTY;
|
|
n = 0;
|
|
#initialSize;
|
|
constructor(initialSize) {
|
|
this.#initialSize = initialSize;
|
|
}
|
|
/** Number of bytes written so far. */
|
|
get length() {
|
|
return this.n;
|
|
}
|
|
/** Reset for reuse; the grown buffer is retained so steady state is alloc-free. */
|
|
reset() {
|
|
this.n = 0;
|
|
}
|
|
/** View of the bytes written so far (no copy; valid until the next write or reset). */
|
|
take() {
|
|
return this.n === 0 ? EMPTY : this.buf.subarray(0, this.n);
|
|
}
|
|
/** Release the backing (handed-out views stay intact); next write re-allocates. */
|
|
release() {
|
|
this.buf = EMPTY;
|
|
this.n = 0;
|
|
}
|
|
/** Grow (doubling) so `extra` more bytes fit; required before unchecked writes. */
|
|
ensure(extra) {
|
|
if (this.n + extra <= this.buf.length)
|
|
return;
|
|
let size = Math.max(this.#initialSize, this.buf.length * 2);
|
|
while (this.n + extra > size)
|
|
size *= 2;
|
|
const grown = new Uint8Array(size);
|
|
grown.set(this.buf);
|
|
this.buf = grown;
|
|
}
|
|
/** Write a u32 (LE) at the cursor; caller must have `ensure`d 4 bytes. */
|
|
writeU32(v) {
|
|
const buf = this.buf;
|
|
let n = this.n;
|
|
buf[n++] = v & 255;
|
|
buf[n++] = (v >> 8) & 255;
|
|
buf[n++] = (v >> 16) & 255;
|
|
buf[n++] = (v >>> 24) & 255;
|
|
this.n = n;
|
|
}
|
|
/** u32 byte length + UTF-8 bytes (self-ensuring). */
|
|
utf8WithU32Len(s) {
|
|
const len = s.length;
|
|
this.ensure(4 + len * 3); // worst-case UTF-8 is 3 bytes per UTF-16 unit
|
|
// Short strings: inline char copy (cheaper than a native call), guarded by a
|
|
// quick ASCII scan. Anything longer — or non-ASCII — goes to encodeInto.
|
|
if (len <= INLINE_STR_MAX) {
|
|
let ascii = true;
|
|
for (let i = 0; i < len; i++) {
|
|
if (s.charCodeAt(i) > 127) {
|
|
ascii = false;
|
|
break;
|
|
}
|
|
}
|
|
if (ascii) {
|
|
// Inline stores, not writeU32: a per-string method call in the
|
|
// unoptimized tier is exactly what this class's header rules out.
|
|
const buf = this.buf;
|
|
let n = this.n;
|
|
buf[n++] = len & 255;
|
|
buf[n++] = (len >> 8) & 255;
|
|
buf[n++] = (len >> 16) & 255;
|
|
buf[n++] = (len >>> 24) & 255;
|
|
for (let i = 0; i < len; i++)
|
|
buf[n + i] = s.charCodeAt(i);
|
|
this.n = n + len;
|
|
return;
|
|
}
|
|
}
|
|
// Bulk path: encodeInto writes UTF-8 straight into the buffer (no alloc, no
|
|
// per-char loop); backpatch the byte length once it's known.
|
|
const lenPos = this.n;
|
|
this.n += 4;
|
|
const written = encoder.encodeInto(s, this.buf.subarray(this.n)).written;
|
|
this.buf[lenPos] = written & 255;
|
|
this.buf[lenPos + 1] = (written >> 8) & 255;
|
|
this.buf[lenPos + 2] = (written >> 16) & 255;
|
|
this.buf[lenPos + 3] = (written >>> 24) & 255;
|
|
this.n += written;
|
|
}
|
|
}
|