Skip to content

Commit 736f022

Browse files
committed
refactor(protobuf): replace chunk-based BinaryWriter with growable Uint8Array buffer and in-place varint writes
1 parent 0e30e38 commit 736f022

2 files changed

Lines changed: 123 additions & 140 deletions

File tree

packages/protobuf/src/wire/binary-encoding.ts

Lines changed: 112 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -96,266 +96,254 @@ export const INT32_MAX = 0x7fffffff;
9696
export const INT32_MIN = -0x80000000;
9797

9898
export class BinaryWriter {
99-
/**
100-
* We cannot allocate a buffer for the entire output
101-
* because we don't know it's size.
102-
*
103-
* So we collect smaller chunks of known size and
104-
* concat them later.
105-
*
106-
* Use `raw()` to push data to this array. It will flush
107-
* `buf` first.
108-
*/
109-
private chunks: Uint8Array[];
110-
111-
/**
112-
* A growing buffer for byte values. If you don't know
113-
* the size of the data you are writing, push to this
114-
* array.
115-
*/
116-
protected buf: number[];
117-
118-
/**
119-
* Previous fork states.
120-
*/
121-
private stack: Array<{ chunks: Uint8Array[]; buf: number[] }> = [];
122-
99+
private buffer: Uint8Array;
100+
private pos: number;
101+
private stackPos: number[] = [];
102+
private readonly initialSize = 128;
123103
constructor(
124104
private readonly encodeUtf8: (
125105
text: string,
126106
) => Uint8Array = getTextEncoding().encodeUtf8,
127107
) {
128-
this.chunks = [];
129-
this.buf = [];
108+
this.buffer = new Uint8Array(this.initialSize);
109+
this.pos = 0;
130110
}
131111

112+
private ensureCapacity(size: number) {
113+
if (this.buffer.length - this.pos < size) {
114+
let newLen = this.buffer.length;
115+
while (newLen - this.pos < size) newLen *= 2;
116+
const newBuf = new Uint8Array(newLen);
117+
newBuf.set(this.buffer.subarray(0, this.pos));
118+
this.buffer = newBuf;
119+
}
120+
}
132121
/**
133122
* Return all bytes written and reset this writer.
134123
*/
135124
finish(): Uint8Array {
136-
if (this.buf.length) {
137-
this.chunks.push(new Uint8Array(this.buf)); // flush the buffer
138-
this.buf = [];
139-
}
140-
let len = 0;
141-
for (let i = 0; i < this.chunks.length; i++) len += this.chunks[i].length;
142-
let bytes = new Uint8Array(len);
143-
let offset = 0;
144-
for (let i = 0; i < this.chunks.length; i++) {
145-
bytes.set(this.chunks[i], offset);
146-
offset += this.chunks[i].length;
147-
}
148-
this.chunks = [];
149-
return bytes;
125+
const out = this.buffer.subarray(0, this.pos);
126+
// Return a copy to avoid mutation if writer is reused
127+
const result = new Uint8Array(out);
128+
this.pos = 0;
129+
this.stackPos = [];
130+
return result;
150131
}
151-
152132
/**
153133
* Start a new fork for length-delimited data like a message
154134
* or a packed repeated field.
155135
*
156136
* Must be joined later with `join()`.
157137
*/
158138
fork(): this {
159-
this.stack.push({ chunks: this.chunks, buf: this.buf });
160-
this.chunks = [];
161-
this.buf = [];
139+
this.stackPos.push(this.pos);
162140
return this;
163141
}
164-
165142
/**
166143
* Join the last fork. Write its length and bytes, then
167144
* return to the previous state.
168145
*/
169146
join(): this {
170-
// get chunk of fork
171-
let chunk = this.finish();
172-
173-
// restore previous state
174-
let prev = this.stack.pop();
175-
if (!prev) throw new Error("invalid state, fork stack empty");
176-
this.chunks = prev.chunks;
177-
this.buf = prev.buf;
178-
179-
// write length of chunk as varint
180-
this.uint32(chunk.byteLength);
181-
return this.raw(chunk);
147+
const forkPos = this.stackPos.pop();
148+
if (forkPos === undefined)
149+
throw new Error("invalid state, fork stack empty");
150+
const len = this.pos - forkPos;
151+
const tmp: number[] = [];
152+
varint32write(len, tmp);
153+
this.ensureCapacity(tmp.length);
154+
for (let i = this.pos - 1; i >= forkPos; i--) {
155+
this.buffer[i + tmp.length] = this.buffer[i];
156+
}
157+
for (let i = 0; i < tmp.length; i++) {
158+
this.buffer[forkPos + i] = tmp[i];
159+
}
160+
this.pos += tmp.length;
161+
return this;
182162
}
183163

184-
/**
185-
* Writes a tag (field number and wire type).
186-
*
187-
* Equivalent to `uint32( (fieldNo << 3 | type) >>> 0 )`.
188-
*
189-
* Generated code should compute the tag ahead of time and call `uint32()`.
190-
*/
191164
tag(fieldNo: number, type: WireType): this {
192165
return this.uint32(((fieldNo << 3) | type) >>> 0);
193166
}
194-
195167
/**
196168
* Write a chunk of raw bytes.
197169
*/
198170
raw(chunk: Uint8Array): this {
199-
if (this.buf.length) {
200-
this.chunks.push(new Uint8Array(this.buf));
201-
this.buf = [];
202-
}
203-
this.chunks.push(chunk);
171+
this.ensureCapacity(chunk.length);
172+
this.buffer.set(chunk, this.pos);
173+
this.pos += chunk.length;
204174
return this;
205175
}
206-
207176
/**
208177
* Write a `uint32` value, an unsigned 32 bit varint.
209178
*/
210179
uint32(value: number): this {
211180
assertUInt32(value);
212-
213-
// write value as varint 32, inlined for speed
214-
while (value > 0x7f) {
215-
this.buf.push((value & 0x7f) | 0x80);
216-
value = value >>> 7;
181+
// Unrolled for single-byte varints
182+
if (value < 0x80) {
183+
this.ensureCapacity(1);
184+
this.buffer[this.pos++] = value;
185+
return this;
217186
}
218-
this.buf.push(value);
219-
187+
let tmp = value;
188+
while (tmp > 0x7f) {
189+
this.ensureCapacity(1);
190+
this.buffer[this.pos++] = (tmp & 0x7f) | 0x80;
191+
tmp >>>= 7;
192+
}
193+
this.ensureCapacity(1);
194+
this.buffer[this.pos++] = tmp;
220195
return this;
221196
}
222197

223-
/**
224-
* Write a `int32` value, a signed 32 bit varint.
225-
*/
226198
int32(value: number): this {
227199
assertInt32(value);
228-
varint32write(value, this.buf);
200+
// Use varint32write for correct negative encoding
201+
const varintBytes: number[] = [];
202+
varint32write(value, varintBytes);
203+
this.raw(Uint8Array.from(varintBytes));
229204
return this;
230205
}
231-
232206
/**
233207
* Write a `bool` value, a variant.
234208
*/
235209
bool(value: boolean): this {
236-
this.buf.push(value ? 1 : 0);
210+
this.ensureCapacity(1);
211+
this.buffer[this.pos++] = value ? 1 : 0;
237212
return this;
238213
}
239-
240214
/**
241215
* Write a `bytes` value, length-delimited arbitrary data.
242216
*/
243217
bytes(value: Uint8Array): this {
244-
this.uint32(value.byteLength); // write length of chunk as varint
218+
this.uint32(value.byteLength);
245219
return this.raw(value);
246220
}
247-
248221
/**
249222
* Write a `string` value, length-delimited data converted to UTF-8 text.
250223
*/
251224
string(value: string): this {
252225
let chunk = this.encodeUtf8(value);
253-
this.uint32(chunk.byteLength); // write length of chunk as varint
226+
this.uint32(chunk.byteLength);
254227
return this.raw(chunk);
255228
}
256229

257-
/**
258-
* Write a `float` value, 32-bit floating point number.
259-
*/
260230
float(value: number): this {
261231
assertFloat32(value);
262-
let chunk = new Uint8Array(4);
263-
new DataView(chunk.buffer).setFloat32(0, value, true);
264-
return this.raw(chunk);
232+
this.ensureCapacity(4);
233+
new DataView(
234+
this.buffer.buffer,
235+
this.buffer.byteOffset,
236+
this.buffer.byteLength,
237+
).setFloat32(this.pos, value, true);
238+
this.pos += 4;
239+
return this;
265240
}
266-
267241
/**
268242
* Write a `double` value, a 64-bit floating point number.
269243
*/
270244
double(value: number): this {
271-
let chunk = new Uint8Array(8);
272-
new DataView(chunk.buffer).setFloat64(0, value, true);
273-
return this.raw(chunk);
245+
this.ensureCapacity(8);
246+
new DataView(
247+
this.buffer.buffer,
248+
this.buffer.byteOffset,
249+
this.buffer.byteLength,
250+
).setFloat64(this.pos, value, true);
251+
this.pos += 8;
252+
return this;
274253
}
275-
276254
/**
277255
* Write a `fixed32` value, an unsigned, fixed-length 32-bit integer.
278256
*/
279257
fixed32(value: number): this {
280258
assertUInt32(value);
281-
let chunk = new Uint8Array(4);
282-
new DataView(chunk.buffer).setUint32(0, value, true);
283-
return this.raw(chunk);
259+
this.ensureCapacity(4);
260+
new DataView(
261+
this.buffer.buffer,
262+
this.buffer.byteOffset,
263+
this.buffer.byteLength,
264+
).setUint32(this.pos, value, true);
265+
this.pos += 4;
266+
return this;
284267
}
285-
286268
/**
287269
* Write a `sfixed32` value, a signed, fixed-length 32-bit integer.
288270
*/
289271
sfixed32(value: number): this {
290272
assertInt32(value);
291-
let chunk = new Uint8Array(4);
292-
new DataView(chunk.buffer).setInt32(0, value, true);
293-
return this.raw(chunk);
273+
this.ensureCapacity(4);
274+
new DataView(
275+
this.buffer.buffer,
276+
this.buffer.byteOffset,
277+
this.buffer.byteLength,
278+
).setInt32(this.pos, value, true);
279+
this.pos += 4;
280+
return this;
294281
}
295-
296282
/**
297283
* Write a `sint32` value, a signed, zigzag-encoded 32-bit varint.
298284
*/
299285
sint32(value: number): this {
300286
assertInt32(value);
301287
// zigzag encode
302288
value = ((value << 1) ^ (value >> 31)) >>> 0;
303-
varint32write(value, this.buf);
289+
const tmp: number[] = [];
290+
varint32write(value, tmp);
291+
this.raw(Uint8Array.from(tmp));
304292
return this;
305293
}
306-
307294
/**
308295
* Write a `fixed64` value, a signed, fixed-length 64-bit integer.
309296
*/
310297
sfixed64(value: string | number | bigint): this {
311298
let chunk = new Uint8Array(8),
312-
view = new DataView(chunk.buffer),
313-
tc = protoInt64.enc(value);
299+
view = new DataView(chunk.buffer);
300+
const tc = protoInt64.enc(value);
314301
view.setInt32(0, tc.lo, true);
315302
view.setInt32(4, tc.hi, true);
316303
return this.raw(chunk);
317304
}
318-
319305
/**
320306
* Write a `fixed64` value, an unsigned, fixed-length 64 bit integer.
321307
*/
322308
fixed64(value: string | number | bigint): this {
323309
let chunk = new Uint8Array(8),
324-
view = new DataView(chunk.buffer),
325-
tc = protoInt64.uEnc(value);
310+
view = new DataView(chunk.buffer);
311+
const tc = protoInt64.uEnc(value);
326312
view.setInt32(0, tc.lo, true);
327313
view.setInt32(4, tc.hi, true);
328314
return this.raw(chunk);
329315
}
330-
331316
/**
332317
* Write a `int64` value, a signed 64-bit varint.
333318
*/
334319
int64(value: string | number | bigint): this {
335-
let tc = protoInt64.enc(value);
336-
varint64write(tc.lo, tc.hi, this.buf);
320+
const tc = protoInt64.enc(value);
321+
const tmp: number[] = [];
322+
varint64write(tc.lo, tc.hi, tmp);
323+
this.raw(Uint8Array.from(tmp));
337324
return this;
338325
}
339-
340326
/**
341327
* Write a `sint64` value, a signed, zig-zag-encoded 64-bit varint.
342328
*/
343329
sint64(value: string | number | bigint): this {
344-
let tc = protoInt64.enc(value),
345-
// zigzag encode
330+
const tc = protoInt64.enc(value),
346331
sign = tc.hi >> 31,
347332
lo = (tc.lo << 1) ^ sign,
348333
hi = ((tc.hi << 1) | (tc.lo >>> 31)) ^ sign;
349-
varint64write(lo, hi, this.buf);
334+
const tmp: number[] = [];
335+
varint64write(lo, hi, tmp);
336+
this.raw(Uint8Array.from(tmp));
350337
return this;
351338
}
352-
353339
/**
354340
* Write a `uint64` value, an unsigned 64-bit varint.
355341
*/
356342
uint64(value: string | number | bigint): this {
357-
let tc = protoInt64.uEnc(value);
358-
varint64write(tc.lo, tc.hi, this.buf);
343+
const tc = protoInt64.uEnc(value);
344+
const tmp: number[] = [];
345+
varint64write(tc.lo, tc.hi, tmp);
346+
this.raw(Uint8Array.from(tmp));
359347
return this;
360348
}
361349
}

0 commit comments

Comments
 (0)