From f5a25589d96fc17dd011b8228de9c0bb8ab12632 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Tue, 23 Jun 2026 00:14:12 +0900 Subject: [PATCH 1/2] refactor: reduce GC pressure from zero-copy buffer slices under high concurrency --- README.ko.md | 18 +++---- README.md | 4 +- .../tests/commands/0028.parser-edge.test.ts | 53 +++++++++++++++++++ sources/modules/parser.ts | 46 +--------------- website/components/architecture-diagram.tsx | 2 +- 5 files changed, 66 insertions(+), 57 deletions(-) diff --git a/README.ko.md b/README.ko.md index de8a0fe..d939305 100644 --- a/README.ko.md +++ b/README.ko.md @@ -254,7 +254,7 @@ _100,000번 반복 × 10,000 동시 실행 · 1 KB 페이로드 · 10회 측정_ ### High Voltage 성능 - `setImmediate` 기반 파이프라인 자동 병합 -- Zero-copy RESP 파서 (버퍼 슬라이스 재사용) +- 바이너리 세이프 RESP 파서와 독립 버퍼 반환 - 백프레셔 대응 청크 단위 소켓 쓰기 - 이벤트 루프 양보 포인트 설정 가능 @@ -385,7 +385,7 @@ graph TD direction LR Conn[Connection
TCP · TLS · Reconnect] Req[Requester
Queue · Pipeline · Timeout] - Parse[Parser
RESP2 · RESP3 · Zero-copy] + Parse[Parser
RESP2 · RESP3 · Binary-safe] PS[PubSub
Channel · Pattern · Shard] DM[Debug Memory
Ring buffer · Sanitized] end @@ -419,13 +419,13 @@ sequenceDiagram Client-->>App: 'OK' ``` -| 모듈 | 역할 | -| :--------------- | :---------------------------------------------- | -| **Connection** | TCP/TLS 소켓 관리, 재연결 백오프 | -| **Requester** | 커맨드 큐, 파이프라인 청킹, 응답 매칭, 타임아웃 | -| **Parser** | RESP 디코딩, 버퍼 관리, zero-copy 슬라이싱 | -| **PubSub** | 채널/패턴/샤드 상태 추적, 메시지 디스패치 | -| **Debug Memory** | 링 버퍼 기반 디버그 로그, credential 마스킹 | +| 모듈 | 역할 | +| :--------------- | :------------------------------------------------ | +| **Connection** | TCP/TLS 소켓 관리, 재연결 백오프 | +| **Requester** | 커맨드 큐, 파이프라인 청킹, 응답 매칭, 타임아웃 | +| **Parser** | RESP 디코딩, 버퍼 관리, 바이너리 세이프 응답 처리 | +| **PubSub** | 채널/패턴/샤드 상태 추적, 메시지 디스패치 | +| **Debug Memory** | 링 버퍼 기반 디버그 로그, credential 마스킹 | ## 이벤트 diff --git a/README.md b/README.md index bfc8427..ce4d192 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ _100,000 iterations × 10,000 concurrency · 1 KB payload · 10 repeats_ ### High Voltage Performance - `setImmediate` pipeline coalescing -- Zero-copy RESP parser (borrowed buffer slices) +- Binary-safe RESP parser with owned buffers - Chunked socket writes with backpressure - Configurable event-loop yield points @@ -385,7 +385,7 @@ graph TD direction LR Conn[Connection
TCP · TLS · Reconnect] Req[Requester
Queue · Pipeline · Timeout] - Parse[Parser
RESP2 · RESP3 · Zero-copy] + Parse[Parser
RESP2 · RESP3 · Binary-safe] PS[PubSub
Channel · Pattern · Shard] DM[Debug Memory
Ring buffer · Sanitized] end diff --git a/scripts/tests/commands/0028.parser-edge.test.ts b/scripts/tests/commands/0028.parser-edge.test.ts index 9764247..5a5af65 100644 --- a/scripts/tests/commands/0028.parser-edge.test.ts +++ b/scripts/tests/commands/0028.parser-edge.test.ts @@ -246,6 +246,59 @@ describe('parser-edge', () => { }); describe('internal buffer management', () => { + it('returns bulk buffers that are independent from the input chunk', async () => { + const parser = createParser(); + const chunk = bytes('$5\r\nhello\r\n'); + + const [reply] = await parser.queueParse(chunk); + + chunk.fill(0); + + assert.deepStrictEqual(reply, bytes('hello')); + }); + + it('keeps a returned bulk buffer stable after caller mutation and later parses', async () => { + const parser = createParser(); + + const [first] = await parser.queueParse(bytes('$3\r\nabc\r\n')); + + if (!Buffer.isBuffer(first)) { + assert.fail('expected first reply to be a Buffer'); + } + + first.fill(0); + + const [second] = await parser.queueParse(bytes('$3\r\nxyz\r\n')); + + assert.deepStrictEqual(first, Buffer.alloc(3)); + assert.deepStrictEqual(second, bytes('xyz')); + }); + + it('does not mutate a held bulk buffer when later replies shift the internal buffer', async () => { + const parser = new SolidisParser({ + ...SolidisDefaultOptions, + parser: { + buffer: { initial: 512, shiftThreshold: 16 }, + maxBulkStringLength: 1048576, + }, + }); + + const payload = 'x'.repeat(256); + const [bulk] = await parser.queueParse( + bytes(`$${payload.length}\r\n${payload}\r\n`), + ); + + if (!Buffer.isBuffer(bulk)) { + assert.fail('expected bulk reply to be a Buffer'); + } + + const snapshot = Buffer.from(bulk); + + await parser.queueParse(bytes('+OK\r\n'.repeat(30))); + + assert.deepStrictEqual(bulk, snapshot); + }); + it('grows the internal buffer when a second chunk exceeds remaining capacity', async () => { const parser = new SolidisParser({ ...SolidisDefaultOptions, diff --git a/sources/modules/parser.ts b/sources/modules/parser.ts index 890fbc8..abb9c35 100644 --- a/sources/modules/parser.ts +++ b/sources/modules/parser.ts @@ -20,8 +20,6 @@ import type { export class SolidisParser { #buffer: Buffer; - #bufferHasBorrowedSlices = false; - #bufferIsExternal = false; #initialBufferSize: number; #shiftThreshold: number; #maxBulkStringLength: number; @@ -76,20 +74,6 @@ export class SolidisParser { } #appendBuffer(parseBuffer: Buffer) { - if (this.#readOffset === this.#writeOffset) { - this.#buffer = parseBuffer; - this.#bufferHasBorrowedSlices = false; - this.#bufferIsExternal = true; - this.#readOffset = 0; - this.#writeOffset = parseBuffer.length; - - return; - } - - if (this.#bufferIsExternal) { - this.#moveUnreadBytesToInternalBuffer(parseBuffer.length); - } - if (this.#writeOffset + parseBuffer.length > this.#buffer.length) { this.#growInternalBuffer(this.#writeOffset + parseBuffer.length); } @@ -99,20 +83,6 @@ export class SolidisParser { this.#writeOffset += parseBuffer.length; } - #moveUnreadBytesToInternalBuffer(additionalCapacity: number) { - const remainingBytes = this.#writeOffset - this.#readOffset; - const minCapacity = remainingBytes + additionalCapacity; - const newBuffer = this.#allocateInternalBuffer(minCapacity); - - this.#buffer.copy(newBuffer, 0, this.#readOffset, this.#writeOffset); - - this.#buffer = newBuffer; - this.#bufferHasBorrowedSlices = false; - this.#bufferIsExternal = false; - this.#readOffset = 0; - this.#writeOffset = remainingBytes; - } - #allocateInternalBuffer(minCapacity: number) { let newCapacity = Math.max(this.#initialBufferSize, this.#buffer.length); @@ -132,8 +102,6 @@ export class SolidisParser { this.#readOffset = 0; this.#buffer = newBuffer; - this.#bufferHasBorrowedSlices = false; - this.#bufferIsExternal = false; } #tryShiftInternalBuffer() { @@ -144,10 +112,6 @@ export class SolidisParser { return; } - if (this.#bufferIsExternal || this.#bufferHasBorrowedSlices) { - return; - } - if ( this.#readOffset > this.#shiftThreshold && this.#readOffset < this.#writeOffset @@ -360,7 +324,7 @@ export class SolidisParser { } default: { - data = this.#borrowParsedBuffer(parsed.data); + data = Buffer.from(parsed.data); break; } } @@ -372,14 +336,6 @@ export class SolidisParser { }; } - #borrowParsedBuffer(buffer: Buffer) { - if (!this.#bufferIsExternal) { - this.#bufferHasBorrowedSlices = true; - } - - return buffer; - } - #parseSimpleLine(type: SolidisRespSimpleLineType): SolidisParsed { const parsed = this.#parseLine(this.#readOffset + 1, type); diff --git a/website/components/architecture-diagram.tsx b/website/components/architecture-diagram.tsx index 1bc1ab6..5836999 100644 --- a/website/components/architecture-diagram.tsx +++ b/website/components/architecture-diagram.tsx @@ -239,7 +239,7 @@ export function ArchitectureDiagram({ width={parserWidth} height={parserHeight} title="Parser" - subtitle="RESP2 · RESP3 · Zero-copy" + subtitle="RESP2 · RESP3 · Binary-safe" /> Date: Tue, 23 Jun 2026 00:16:28 +0900 Subject: [PATCH 2/2] release: 0.3.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4864485..80b4bc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vcms-io/solidis", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vcms-io/solidis", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "devDependencies": { "@biomejs/biome": "2.5.0", diff --git a/package.json b/package.json index a5564f5..077a3a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vcms-io/solidis", - "version": "0.3.0", + "version": "0.3.1", "author": "Jay Lee ", "description": "The fastest Redis client for Node.js. Zero dependencies, 2x+ faster than ioredis, battle-tested in production.", "repository": {