<!-- task=m-performance-hotspot-optimization/08_ts_ws_1mb_payload plan=0 tag=PERF_TS_PAYLOAD -->

# TypeScript WS 1MB Payload Plan

## 이 파일을 읽는 구현 에이전트에게

이 작업은 구현 후 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 완료된다. 검증 명령을 실행하고 실제 stdout/stderr 또는 저장된 결과 파일 경로를 기록한 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.

구현 중 사용자만 결정할 수 있는 제품 선택, 사용자 소유 외부 환경, secret, 범위 충돌이 필요하면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정, 증거, 실행한 명령/출력, 자동 후속 불가 이유, 재개 조건을 적고 중단한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

현재 payload Epic의 `[ts-ws-1mb]`는 TypeScript WS 1MB payload row의 p99 또는 throughput 개선, 또는 명확한 병목 문서화를 요구한다. idle full record `agent-test/runs/20260605-115943-proto-socket-performance-full.md`에서 TypeScript WS `payload=1MB`는 throughput `35.8 rps`, p99 `217.890ms`, stability hard gate 0 PASS다. Node WS path는 receive buffer `Buffer.concat`, unmasked payload copy, outgoing `Buffer.from`와 frame `Buffer.concat`, communicator queue가 같이 관여한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지이며, code-review가 해당 요청의 정당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/milestones/performance-hotspot-optimization.md`
- Task ids:
  - `ts-ws-1mb`: TypeScript WS 1MB payload 경로를 분석하고 p99 latency 또는 throughput을 개선한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-roadmap/current.md`
- `agent-roadmap/milestones/performance-hotspot-optimization.md`
- `agent-test/local/rules.md`
- `agent-test/local/proto-socket-performance-baseline.md`
- `agent-test/local/typescript-smoke.md`
- `agent-test/local/proto-socket-full-matrix.md`
- `agent-test/runs/20260605-115943-proto-socket-performance-full.md`
- `agent-test/runs/20260603-062724-proto-socket-stress-full.md`
- `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh`
- `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh`
- `typescript/src/node_ws_client.ts`
- `typescript/src/node_ws_server.ts`
- `typescript/src/communicator.ts`
- `typescript/src/tcp_client.ts`
- `typescript/src/packets/message_common_pb.ts`
- `typescript/bench/stress.ts`
- `typescript/test/ws.test.ts`
- `typescript/package.json`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다. payload/latency/throughput 작업은 `agent-test/local/proto-socket-performance-baseline.md`로 라우팅된다.
- `typescript/**` 변경이므로 `agent-test/local/typescript-smoke.md`도 읽었다.
- compatibility 영향 확인을 위해 `agent-test/local/proto-socket-full-matrix.md`도 읽었다.
- 적용 명령:
  - TypeScript smoke: `cd typescript && npm run check && npm test`
  - targeted payload: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport ws --profile payload`
  - final performance quick: `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh --quick`
- full matrix는 protocol/API/cross-language 영향이 확인되거나 reviewer가 요구할 때 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all`로 확장한다.
- 도구 확인: `dart`, `node`, `npm`, `bash`, `typescript/node_modules/.bin/tsx`가 존재했고, `run_stress.sh`/`run_performance.sh`는 `bash -n`을 통과했다.

### 테스트 커버리지 공백

- 현재 `typescript/test/ws.test.ts`는 WS/WSS send, request-response, concurrent request, gateway read loop, handshake timeout을 검증하지만 1MB binary frame roundtrip을 직접 검증하지 않는다.
- `typescript/bench/stress.ts`의 payload profile은 full 모드에서 1MB row를 기록하지만 성능 수치 검증은 환경 의존이다. 기능 회귀는 unit/integration test로 잡고, p99/throughput 판정은 stress record로 남긴다.
- custom `NodeWebSocket` frame parser의 large-frame fragmentation/read-buffer behavior는 tests가 부족하다.

### 심볼 참조

- none. 계획된 변경은 public symbol rename/removal 없이 TypeScript Node WS 내부 transport/frame buffer에 한정한다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- shared task group: `agent-task/m-performance-hotspot-optimization/`
- sibling subtasks:
  - `07_dart_tcp_1mb_payload`: Dart TCP 1MB payload. 독립 작업, depends_on=[].
  - `08_ts_ws_1mb_payload`: TypeScript WS 1MB payload. 독립 작업, depends_on=[].
- Dart와 TypeScript는 ownership boundary와 검증 명령이 달라 별도 plan이다.
- 이 subtask 자체는 TypeScript Node WS transport 내부의 frame copy/read buffering과 tests가 함께 움직인다. public API나 broad call-site rollout이 없으므로 단일 plan으로 둔다.

### 범위 결정 근거

- 이 plan은 `[ts-ws-1mb]`만 완료 대상으로 삼는다.
- TypeScript gateway 단일 수신 경로 Epic은 이미 별도 Task로 완료되어 있으며, gateway on/off 제품 정책 변경은 이 plan 범위 밖이다.
- `proto/**`, generated proto schema, Dart/Go/Kotlin/Python 코드는 수정하지 않는다.
- TCP TypeScript path는 비교 기준으로만 사용하고, TCP 성능 최적화는 이 plan의 목표가 아니다.

### 빌드 등급

- build lane: `cloud-G07`.
- review lane: `cloud-G07`.
- 근거: custom WebSocket frame parser/encoder와 1MB benchmark evidence가 결합된 performance/concurrency-sensitive transport 변경이다.

## 구현 체크리스트

- [ ] TypeScript Node WS receive path에서 1MB frame의 read buffer/fragment copy 비용을 줄이고, fragmented large binary frame regression test를 추가한다.
- [ ] TypeScript Node WS write path에서 outgoing payload/frame copy 비용을 분석하고 안정성 hard gate를 유지하는 최소 변경을 적용하거나 병목 유지 사유를 문서화한다.
- [ ] `cd typescript && npm run check && npm test`를 실행한다.
- [ ] `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport ws --profile payload`를 실행하고 TypeScript WS `payload=1MB` row의 stability 0 및 p99/throughput 변화를 기록한다.
- [ ] `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh --quick`를 실행해 project performance quick smoke가 회귀 없이 기록되는지 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [PERF_TS_PAYLOAD-1] Node WS receive copy 줄이기

#### 문제

- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:116)는 매 socket chunk마다 `this.readBuf = Buffer.concat([this.readBuf, chunk])`로 누적 buffer 전체를 복사한다.
- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:174)는 payload view를 만들고, [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:175)는 unmasked inbound frame도 `Buffer.from(rawPayload)`로 복사한다.
- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:176)는 consumed data를 `subarray`로 남긴다. chunk fragmentation이 크면 concat/copy 비용이 누적된다.
- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:240)는 payload를 `new Uint8Array(payload)`로 emit해 Buffer view를 다시 감싼다.
- large payload full baseline은 [20260605 performance full](/config/workspace/proto-socket/agent-test/runs/20260605-115943-proto-socket-performance-full.md:210)에서 p99 `217.890ms`, throughput `35.8 rps`다.

#### 해결 방법

현재 구조:

```ts
// typescript/src/node_ws_client.ts:111-118
private onData(chunk: Buffer): void {
  if (this.readyState === NodeWebSocket.CLOSED) {
    return;
  }

  this.readBuf = Buffer.concat([this.readBuf, chunk]);
  try {
    while (this.tryReadFrame()) {
      continue;
    }
```

```ts
// typescript/src/node_ws_client.ts:172-178
const mask = masked ? this.readBuf.subarray(offset, offset + 4) : null;
offset += maskOffset;
const rawPayload = this.readBuf.subarray(offset, offset + payloadLength);
const payload = mask === null ? Buffer.from(rawPayload) : unmask(rawPayload, mask);
this.readBuf = this.readBuf.subarray(frameLength);

this.handleFrame(opcode, fin, payload);
```

구현 방향:

```ts
// private helper 예시. public API로 노출하지 않는다.
class ReadBuffer {
  append(chunk: Buffer): void;
  get length(): number;
  peek(offset: number): number;
  readUInt16BE(offset: number): number;
  readBigUInt64BE(offset: number): bigint;
  take(length: number): Buffer;
  skip(length: number): void;
}
```

- `Buffer.concat` per chunk를 피하기 위해 chunk queue + read offset helper를 `node_ws_client.ts` private class로 둔다.
- frame header는 queue에서 peek/read하고, full frame이 도착했을 때 payload만 정확히 한 번 `Buffer`로 materialize한다.
- unmasked server-to-client frame은 payload Buffer를 그대로 전달할 수 있는지 확인한다. lifetime이 read buffer queue consume 이후에도 안전하지 않으면 payload만 한 번 copy한다.
- masked frame은 현재처럼 unmask copy가 필요하지만, 추가 concat copy는 제거한다.
- `NodeWebSocket` public event contract는 `Uint8Array` message 그대로 유지한다.

#### 수정 파일 및 체크리스트

- [ ] `typescript/src/node_ws_client.ts`: read buffer helper를 추가하고 `readBuf: Buffer` 기반 concat을 교체한다.
- [ ] `typescript/src/node_ws_client.ts`: unmasked payload path에서 불필요한 `Buffer.from`/`new Uint8Array` copy를 줄인다.
- [ ] `typescript/test/ws.test.ts`: fragmented large binary request-response 또는 server push test를 추가한다.

#### 테스트 작성

- 작성한다.
- `typescript/test/ws.test.ts`에 1MB message WS roundtrip test를 추가한다.
- 가능하면 raw `net.Socket` handshake 후 WebSocket binary frame을 여러 chunk로 쪼개 보내 `NodeWebSocket` parser가 정확히 한 메시지로 dispatch하는지 검증한다.
- 테스트 timeout은 Vitest test-level timeout으로 명시한다.

#### 중간 검증

```bash
cd typescript && npm run check && npm test
```

기대 결과: TypeScript typecheck PASS, Vitest PASS.

### [PERF_TS_PAYLOAD-2] Node WS write copy와 frame encode 비용 분리

#### 문제

- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:78)는 `send(data: Uint8Array)`를 받고, [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:83)는 `Buffer.from(data)`로 payload를 복사한다.
- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:248)는 `encodeFrame` 결과를 socket에 쓴다.
- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:545)는 frame header를 만들고, [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:569)는 unmasked frame도 `Buffer.concat([header, payload])`로 전체 frame을 복사한다.
- [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:573)는 masked client frames에서 masking key를 만들고 [node_ws_client.ts](/config/workspace/proto-socket/typescript/src/node_ws_client.ts:575)는 header+masked payload concat을 수행한다.

#### 해결 방법

현재 구조:

```ts
// typescript/src/node_ws_client.ts:78-84
send(data: Uint8Array, callback?: (err?: Error | null) => void): void {
  if (this.readyState !== NodeWebSocket.OPEN) {
    callback?.(new Error("WebSocket is not open"));
    return;
  }
  this.writeFrame(0x2, Buffer.from(data), callback);
}
```

```ts
// typescript/src/node_ws_client.ts:545-575
function encodeFrame(opcode: number, payload: Buffer, mask: boolean): Buffer {
  const payloadLength = payload.byteLength;
  ...
  if (!mask) {
    return Buffer.concat([header, payload]);
  }

  const maskingKey = crypto.randomBytes(4);
  maskingKey.copy(header, offset);
  return Buffer.concat([header, maskPayload(payload, maskingKey)]);
}
```

구현 방향:

```ts
private writeFrame(opcode: number, payload: Uint8Array, callback?: (err?: Error | null) => void): void {
  const { header, payload: encodedPayload } = encodeFrameParts(opcode, payload, this.maskOutgoing);
  if (!this.socket.write(header)) {
    this.socket.once("drain", () => this.socket.write(encodedPayload, callback));
    return;
  }
  this.socket.write(encodedPayload, callback);
}
```

- `encodeFrame`을 `encodeFrameParts`로 바꾸거나 내부적으로 header/payload parts를 반환하게 해 unmasked server frames에서 header+payload concat을 피한다.
- masked client frames는 masking 때문에 payload copy가 필요하다. 단, `Buffer.from(data)`와 `maskPayload`의 중복 copy를 피하도록 `maskPayload`가 `Uint8Array` view를 직접 받아 masked Buffer를 만든다.
- `socket.write` callback이 최종 payload write 완료 시점에 한 번만 호출되도록 error/drain handling을 명확히 한다.
- WSS path는 `ws` package object를 `NodeWsClient`로 감싸므로 custom `NodeWebSocket` encode path와 다를 수 있다. 이 plan의 target은 WS row이며 WSS는 smoke로만 보호한다.

#### 수정 파일 및 체크리스트

- [ ] `typescript/src/node_ws_client.ts`: `send`/`writeFrame`/`encodeFrame`을 header+payload parts 기반으로 바꾼다.
- [ ] `typescript/src/node_ws_client.ts`: client masked frame path에서 필요한 copy와 불필요한 copy를 분리한다.
- [ ] `typescript/test/ws.test.ts`: 1MB WS request-response가 정확히 echo되는지 검증한다.

#### 테스트 작성

- 작성한다.
- `typescript/test/ws.test.ts`에 1MB payload request-response test를 추가한다.
- frame encode helper가 private이면 public WS roundtrip test로 검증한다.

#### 중간 검증

```bash
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport ws --profile payload
```

기대 결과: TypeScript WS payload 1KB/64KB/1MB rows가 PASS, stability violations 0. 1MB row는 `agent-test/runs/20260605-115943-proto-socket-performance-full.md`의 throughput `35.8 rps` 또는 p99 `217.890ms` 대비 개선되거나, 개선되지 않은 원인을 review에 기록한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `typescript/src/node_ws_client.ts` | PERF_TS_PAYLOAD-1, PERF_TS_PAYLOAD-2 |
| `typescript/test/ws.test.ts` | PERF_TS_PAYLOAD-1, PERF_TS_PAYLOAD-2 |

## 최종 검증

```bash
cd typescript && npm run check && npm test
```

기대 결과: 종료 코드 0, TypeScript typecheck PASS, Vitest PASS.

```bash
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --full --lang typescript --transport ws --profile payload
```

기대 결과: 결과 기록 파일 생성, TypeScript WS payload rows PASS, `payload=1MB` row stability counters all 0, p99 또는 throughput 개선 여부 기록.

```bash
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_performance.sh --quick
```

기대 결과: performance quick 결과 기록 파일 생성, same-language/cross-language/typescript-gateway 구성요소가 실행 범위에 포함되고 새 TypeScript 변경으로 인한 stability FAIL 없음. Kotlin deferred 등 기존 마일스톤 수용 항목이 나타나면 기존 수용 근거와 구분해 기록한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
