From 070287bb1e026c8fa07d34e6395fab09ec814239 Mon Sep 17 00:00:00 2001 From: Nigro Simone Date: Sun, 13 Sep 2026 09:06:32 +0200 Subject: [PATCH] perf(pg-protocol): decode short ascii values without buffer.toString() --- packages/pg-protocol/src/buffer-reader.ts | 54 +++++++++++++++++-- .../pg-protocol/src/inbound-parser.test.ts | 20 +++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/pg-protocol/src/buffer-reader.ts b/packages/pg-protocol/src/buffer-reader.ts index 42a4a23fa..c53a7388d 100644 --- a/packages/pg-protocol/src/buffer-reader.ts +++ b/packages/pg-protocol/src/buffer-reader.ts @@ -1,3 +1,45 @@ +const fromCharCode = String.fromCharCode + +// Longest value decoded without calling into buffer.toString(). Above this +// length the per-call cost of toString() is cheaper than decoding in JS. +const MAX_ASCII_LENGTH = 16 + +// Most values in a result set are short: ids, flags, small numbers, dates. For +// those the fixed cost of the C++ call behind buffer.toString() is bigger than +// the decoding itself, so build the string from the char codes instead. +// Returns undefined when the bytes are not ASCII, so the caller falls back to +// buffer.toString(). +// prettier-ignore +const decodeAscii = (b: Buffer, i: number, length: number): string | undefined => { + let bits = 0 + for (let k = 0; k < length; k++) { + bits |= b[i + k] + } + if (bits > 127) { + return undefined + } + switch (length) { + case 0: return '' + case 1: return fromCharCode(b[i]) + case 2: return fromCharCode(b[i], b[i + 1]) + case 3: return fromCharCode(b[i], b[i + 1], b[i + 2]) + case 4: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3]) + case 5: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4]) + case 6: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5]) + case 7: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6]) + case 8: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7]) + case 9: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8]) + case 10: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9]) + case 11: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10]) + case 12: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11]) + case 13: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12]) + case 14: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12], b[i + 13]) + case 15: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12], b[i + 13], b[i + 14]) + case 16: return fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7], b[i + 8], b[i + 9], b[i + 10], b[i + 11], b[i + 12], b[i + 13], b[i + 14], b[i + 15]) + } + return undefined +} + export class BufferReader { private buffer: Buffer = Buffer.allocUnsafe(0) @@ -36,9 +78,15 @@ export class BufferReader { } public string(length: number): string { - const result = this.buffer.toString(this.encoding, this.offset, this.offset + length) - this.offset += length - return result + const start = this.offset + this.offset = start + length + if (length <= MAX_ASCII_LENGTH) { + const ascii = decodeAscii(this.buffer, start, length) + if (ascii !== undefined) { + return ascii + } + } + return this.buffer.toString(this.encoding, start, this.offset) } public cstring(): string { diff --git a/packages/pg-protocol/src/inbound-parser.test.ts b/packages/pg-protocol/src/inbound-parser.test.ts index 8687194c3..1e23bb89d 100644 --- a/packages/pg-protocol/src/inbound-parser.test.ts +++ b/packages/pg-protocol/src/inbound-parser.test.ts @@ -315,6 +315,26 @@ describe('PgPacketStream', function () { fields: ['test'], }) }) + + it('decodes values of any length and encoding', function () { + // short ascii values take a different decoding path than long or + // multi byte ones, so check both sides of it + const values = [ + ...Array.from({ length: 20 }, (_, i) => 'x'.repeat(i)), + '\u00e9', + 'citt\u00e0', + '\u00fcn\u00efc\u00f6d\u00e9 w\u00f6rld', + '\u4e2d\u6587\u5b57\u7b26\u4e32', + '\ud83c\udf89', + 'a\ud83c\udf89b', + null, + ] + const fields: (string | null)[] = [] + new Parser().parse(buffers.dataRow(values), (msg) => { + fields.push(...(msg as any).fields) + }) + assert.deepStrictEqual(fields, values) + }) }) describe('notice message', function () {