chore: update stress test scripts and archive slow_mix task files
This commit is contained in:
parent
de832efac6
commit
09041950e5
12 changed files with 1103 additions and 121 deletions
|
|
@ -31,6 +31,8 @@ Profiles (comma separated, default: per-language all):
|
||||||
(quick: 2s, full: 30s/5m/30m candidates)
|
(quick: 2s, full: 30s/5m/30m candidates)
|
||||||
parallel multi-connection FIFO / nonce isolation
|
parallel multi-connection FIFO / nonce isolation
|
||||||
(quick: 4 clients, full: 16/128/512/1024 client candidates)
|
(quick: 4 clients, full: 16/128/512/1024 client candidates)
|
||||||
|
slow-mix mixed fast/slow handler load, same-connection delay and cross-connection isolation
|
||||||
|
(quick: 4 clients, full: 16 clients)
|
||||||
payload payload size matrix request-response (quick: 1KB/64KB, full: 1KB/64KB/1MB)
|
payload payload size matrix request-response (quick: 1KB/64KB, full: 1KB/64KB/1MB)
|
||||||
with per-payload latency, throughput, peak memory, stability counters
|
with per-payload latency, throughput, peak memory, stability counters
|
||||||
cross cross-language request-response correctness / stability adapter
|
cross cross-language request-response correctness / stability adapter
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
<!-- task=m-high-performance-parallel-operations/05+04_slow_mix plan=0 tag=TEST -->
|
||||||
|
|
||||||
|
# Code Review Reference - TEST
|
||||||
|
|
||||||
|
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||||
|
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||||
|
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||||
|
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||||
|
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
||||||
|
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
date=2026-06-03
|
||||||
|
task=m-high-performance-parallel-operations/05+04_slow_mix, plan=0, tag=TEST
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md`
|
||||||
|
- Task ids:
|
||||||
|
- `slow-mix`: 빠른 handler와 느린 handler가 섞인 혼합 부하 추가
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 이 파일을 읽는 리뷰 에이전트에게
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||||
|
|
||||||
|
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||||
|
|
||||||
|
## 구현 항목별 완료 여부
|
||||||
|
|
||||||
|
| 항목 | 완료 여부 |
|
||||||
|
|------|---------|
|
||||||
|
| [TEST-1] Runner profile contract | [x] |
|
||||||
|
| [TEST-2] Slow mix harness | [x] |
|
||||||
|
| [TEST-3] Verification | [x] |
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [x] [TEST-1] `run_stress.sh` usage와 result contract에 `slow-mix` profile을 추가한다.
|
||||||
|
- [x] [TEST-2] Dart/Go/Kotlin/Python/TypeScript harness에 `slow-mix` profile을 추가한다. 검증: 느린 handler가 같은 connection FIFO를 계약대로 지연시키되 다른 connection의 nonce/FIFO 상태를 오염시키지 않는다.
|
||||||
|
- [x] [TEST-3] focused slow-mix stress, TypeScript smoke, 전체 matrix를 실행하고 결과 파일 경로를 review stub에 기록한다.
|
||||||
|
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||||
|
|
||||||
|
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
|
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||||
|
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_0.log`로 아카이브한다.
|
||||||
|
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_0.log`로 아카이브한다.
|
||||||
|
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
|
||||||
|
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||||
|
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||||
|
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||||
|
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||||
|
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||||
|
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||||
|
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||||
|
|
||||||
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
|
- Kotlin `slow-mix` quick WS 실행에서 기존 WS inbound launch-per-frame FIFO 한계가 같은 방식으로 드러났다. 계획의 "기존 deferred bottleneck 정책 보존"에 맞춰 `burst`와 같은 required SKIP 근거를 `slow-mix`에도 적용했다.
|
||||||
|
- Go는 public request API가 send와 response wait를 한 호출로 묶고 있어 같은 connection 안에서는 순차 request 루프를 사용하고, 여러 connection을 동시에 실행해 느린 handler 지연과 connection 간 nonce/type 격리를 기록했다.
|
||||||
|
|
||||||
|
## 주요 설계 결정
|
||||||
|
|
||||||
|
- `slow-mix` 축은 quick `clients=4, perClient=20, slowEvery=5, delayMs=50`, full `clients=16, perClient=200, slowEvery=10, delayMs=100`으로 추가했다.
|
||||||
|
- 서버 handler는 `(index + 1) % slowEvery == 0`인 request만 지연하고, 응답은 `index * 2`와 `echo:<message>`로 반환해 기존 nonce/type 검증 패턴을 유지했다.
|
||||||
|
- TypeScript/Dart/Python/Kotlin runnable 경로는 같은 connection 안의 request completion order를 `fifoViolations`에 반영한다. Kotlin TCP와 Kotlin WS slow-mix는 기존/동일 계열 병목으로 required SKIP 처리한다.
|
||||||
|
- result row axis는 `clients=<n>,slowEvery=<n>,delayMs=<n>`로 통일했고, `run_stress.sh --help` usage에 `slow-mix` 설명을 추가했다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
|
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||||
|
|
||||||
|
- 상태: 없음
|
||||||
|
- 사유 유형: 없음
|
||||||
|
- 결정 필요: 없음
|
||||||
|
- 차단 근거: 없음
|
||||||
|
- 실행한 검증/명령: 없음
|
||||||
|
- 자동 후속 불가 이유: 없음
|
||||||
|
- 재개 조건: 없음
|
||||||
|
|
||||||
|
## 리뷰어를 위한 체크포인트
|
||||||
|
|
||||||
|
- `04+01_load_profile_scales` dependency가 complete log로 충족된 뒤 구현됐는지 확인한다.
|
||||||
|
- slow handler가 same-connection delay는 허용하되 cross-connection nonce/FIFO counters를 오염시키지 않는지 확인한다.
|
||||||
|
- 새 profile이 unknown/optional SKIP으로 빠지지 않고 실제 row 또는 명확한 required SKIP을 남기는지 확인한다.
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||||
|
|
||||||
|
### TEST-1 중간 검증
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --help
|
||||||
|
Profiles (comma separated, default: per-language all):
|
||||||
|
roundtrip request-response latency at concurrency 1/16/64/256
|
||||||
|
burst single-connection fire-and-forget burst ordering / leak check
|
||||||
|
(quick: 200, full: 1k/10k/100k)
|
||||||
|
sustained time-boxed sustained request-response with peak memory
|
||||||
|
(quick: 2s, full: 30s/5m/30m candidates)
|
||||||
|
parallel multi-connection FIFO / nonce isolation
|
||||||
|
(quick: 4 clients, full: 16/128/512/1024 client candidates)
|
||||||
|
slow-mix mixed fast/slow handler load, same-connection delay and cross-connection isolation
|
||||||
|
(quick: 4 clients, full: 16 clients)
|
||||||
|
payload payload size matrix request-response (quick: 1KB/64KB, full: 1KB/64KB/1MB)
|
||||||
|
with per-payload latency, throughput, peak memory, stability counters
|
||||||
|
```
|
||||||
|
|
||||||
|
### TEST-2 중간 검증
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
||||||
|
RUN stress lang=typescript mode=quick transports=tcp profiles=slow-mix
|
||||||
|
INFO stress harness language=TypeScript mode=quick transports=tcp profiles=slow-mix typeName=TestData
|
||||||
|
[slow-mix] transport=tcp mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
[slow-mix] transport=tcp clients=4 done peakMemMb=80.8 violations=0
|
||||||
|
DONE typescript: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
전체 결과값: PASS
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-031533-proto-socket-stress-quick.md`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 검증
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
||||||
|
RUN stress lang=typescript mode=quick transports=tcp profiles=slow-mix
|
||||||
|
INFO stress harness language=TypeScript mode=quick transports=tcp profiles=slow-mix typeName=TestData
|
||||||
|
[slow-mix] transport=tcp mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
[slow-mix] transport=tcp clients=4 done peakMemMb=80.8 violations=0
|
||||||
|
DONE typescript: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
전체 결과값: PASS
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-031533-proto-socket-stress-quick.md`
|
||||||
|
|
||||||
|
$ cd typescript && npm run check && npm test
|
||||||
|
(stdout/stderr)
|
||||||
|
> proto-socket@1.0.5 check
|
||||||
|
> tsc --noEmit
|
||||||
|
> proto-socket@1.0.5 test
|
||||||
|
> vitest run
|
||||||
|
Test Files 5 passed (5)
|
||||||
|
Tests 50 passed (50)
|
||||||
|
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang dart,go,python,typescript --transport tcp
|
||||||
|
RUN stress lang=dart mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE dart: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
RUN stress lang=go mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE go: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
RUN stress lang=python mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE python: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
RUN stress lang=typescript mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE typescript: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
전체 결과값: PASS
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-031543-proto-socket-stress-quick.md`
|
||||||
|
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang kotlin --transport tcp,ws
|
||||||
|
INFO stress harness language=Kotlin mode=quick transports=tcp,ws profiles=slow-mix typeName=TestData
|
||||||
|
[slow-mix] transport=tcp mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
[slow-mix] transport=ws mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
DONE kotlin: INCOMPLETE (rows=0 skips=2 required_skips=2 violations=0)
|
||||||
|
전체 결과값: INCOMPLETE
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-031624-proto-socket-stress-quick.md`
|
||||||
|
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all
|
||||||
|
(stdout/stderr summary)
|
||||||
|
**Proto 동기화**
|
||||||
|
| schema sync | `tools/check_proto_sync.sh` | PASS |
|
||||||
|
**동일언어**
|
||||||
|
| Dart | PASS |
|
||||||
|
| Go | PASS |
|
||||||
|
| Kotlin | PASS |
|
||||||
|
| Python | PASS |
|
||||||
|
| TypeScript | PASS |
|
||||||
|
**언어 PASS 매트릭스**
|
||||||
|
| Dart.io | all clients PASS |
|
||||||
|
| Go | all clients PASS |
|
||||||
|
| Kotlin | all clients PASS |
|
||||||
|
| Python | all clients PASS |
|
||||||
|
| TypeScript | all clients PASS |
|
||||||
|
**크로스테스트 상세**
|
||||||
|
all listed directions PASS, PASS scenarios match Expected, FAIL lines 0
|
||||||
|
로그 디렉터리: `/tmp/proto-socket-matrix.URBBqh`
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-031633-proto-socket-full-matrix.md`
|
||||||
|
|
||||||
|
$ git diff --check
|
||||||
|
(no output)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||||
|
|
||||||
|
## 코드리뷰 결과
|
||||||
|
|
||||||
|
- 종합 판정: FAIL
|
||||||
|
- 차원별 평가
|
||||||
|
- correctness: Fail
|
||||||
|
- completeness: Fail
|
||||||
|
- test coverage: Fail
|
||||||
|
- API contract: Pass
|
||||||
|
- code quality: Pass
|
||||||
|
- plan deviation: Fail
|
||||||
|
- verification trust: Fail
|
||||||
|
- 발견된 문제
|
||||||
|
- Required: `typescript/bench/stress.ts:644`, `dart/bench/stress.dart:625`, `go/bench/stress.go:734`, `python/bench/stress.py:554`, `kotlin/crosstest/stress.kt:583`에서 `slow-mix` row를 emit한 뒤에야 client close와 `pendingLeak` 검사를 수행한다. Dart/Go/Python/Kotlin은 `emitRow` 시점에 total violation을 이미 확정하므로 close 후 pending leak이 생겨도 SUMMARY가 PASS로 남고, TypeScript도 출력된 ROW의 `PendLeak`/status가 cleanup 전 값으로 기록된다. Stability hard gate의 `pending-queue leak 0` 계약을 지키려면 모든 slow-mix 구현에서 close와 leak 검사까지 끝낸 뒤 row/SUMMARY에 반영되도록 emit 순서를 옮기거나 cleanup 전용 row 값을 재계산해야 한다.
|
||||||
|
- Required: `go/bench/stress.go:693`의 Go slow-mix는 client별 goroutine 안에서 `for index := 0; index < perClient; index++`로 `SendRequestTyped`를 순차 호출한다. 따라서 같은 connection 안에 느린 request 뒤의 빠른 request가 동시에 pending 되는 상황이 없어서, 계획의 "느린 handler가 같은 connection FIFO를 계약대로 지연"시키는 축을 Go row가 검증하지 못한 채 PASS를 낼 수 있다. Go에서 결정적 same-connection in-flight 요청을 만들 수 있게 보강하거나, public request API 제약으로 불가능하면 Go slow-mix를 required SKIP/INCOMPLETE로 기록해야 한다.
|
||||||
|
- 다음 단계: FAIL 후속 PLAN/CODE_REVIEW를 작성한다. USER_REVIEW gate는 트리거하지 않는다.
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
<!-- task=m-high-performance-parallel-operations/05+04_slow_mix plan=1 tag=REVIEW_TEST -->
|
||||||
|
|
||||||
|
# Code Review Reference - REVIEW_TEST
|
||||||
|
|
||||||
|
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||||
|
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||||
|
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||||
|
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||||
|
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||||
|
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||||
|
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
date=2026-06-03
|
||||||
|
task=m-high-performance-parallel-operations/05+04_slow_mix, plan=1, tag=REVIEW_TEST
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md`
|
||||||
|
- Task ids:
|
||||||
|
- `slow-mix`: 빠른 handler와 느린 handler가 섞인 혼합 부하 추가
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 이 파일을 읽는 리뷰 에이전트에게
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||||
|
|
||||||
|
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||||
|
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||||
|
|
||||||
|
1. 판정을 append한다.
|
||||||
|
2. `CODE_REVIEW-{review_lane}-GNN.md` → `code_review_{review_lane}_GNN_N.log`, `PLAN-{build_lane}-GNN.md` → `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||||
|
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||||
|
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||||
|
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 구현 항목별 완료 여부
|
||||||
|
|
||||||
|
| 항목 | 완료 여부 |
|
||||||
|
|------|---------|
|
||||||
|
| [REVIEW_TEST-1] Cleanup gate before row | [x] |
|
||||||
|
| [REVIEW_TEST-2] Go same-connection slow/fast verification | [x] |
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [x] [REVIEW_TEST-1] TypeScript/Dart/Go/Python/Kotlin `slow-mix`에서 client close와 pending leak 검사를 row/SUMMARY emission 전에 반영한다.
|
||||||
|
- [x] [REVIEW_TEST-2] Go `slow-mix`가 같은 connection의 in-flight slow/fast request FIFO 지연을 실제로 검증하도록 보강한다.
|
||||||
|
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||||
|
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||||
|
|
||||||
|
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
|
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||||
|
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_1.log`로 아카이브한다.
|
||||||
|
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_1.log`로 아카이브한다.
|
||||||
|
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
|
||||||
|
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||||
|
- [x] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||||
|
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||||
|
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||||
|
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||||
|
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||||
|
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||||
|
|
||||||
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
|
- Go `slow-mix`는 deterministic PASS row 대신 required SKIP/INCOMPLETE로 전환했다. public `SendRequestTyped`는 request enqueue 완료 시점을 노출하지 않고, concurrent request completion order는 goroutine scheduling 영향을 받는다. 서버가 connection별 arrival `seq`를 응답에 넣는 실험에서도 completion order false positive가 발생해 deterministic response-arrival FIFO 검증 근거로 쓸 수 없었다.
|
||||||
|
- TypeScript focused와 Go focused를 최초 병렬 실행하면서 timestamp가 같은 결과 파일명이 충돌했다. TypeScript focused는 단독 재실행해 `agent-test/runs/20260603-035913-proto-socket-stress-quick.md`로 보존했다.
|
||||||
|
|
||||||
|
## 주요 설계 결정
|
||||||
|
|
||||||
|
- TypeScript/Dart/Python/Kotlin의 executable `slow-mix` row는 client close와 pending leak 검사 이후에 emit하도록 순서를 보정했다. 따라서 ROW와 SUMMARY 모두 cleanup 이후 stability 값을 기준으로 한다.
|
||||||
|
- Go는 `slow-mix` profile name과 axis를 유지하되 required SKIP을 emit한다. 이는 PASS 위장이 아니라 현재 public API로 deterministic same-connection in-flight slow/fast 검증을 만들 수 없다는 명시적 baseline 미완결 신호다.
|
||||||
|
- Kotlin TCP/WS deferred bottleneck 정책은 유지했다. Kotlin `slow-mix`는 기존과 같이 required SKIP 2개와 INCOMPLETE를 기록한다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
|
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||||
|
|
||||||
|
- 상태: 없음
|
||||||
|
- 사유 유형: 없음
|
||||||
|
- 결정 필요: 없음
|
||||||
|
- 차단 근거: 없음
|
||||||
|
- 실행한 검증/명령: 없음
|
||||||
|
- 자동 후속 불가 이유: 없음
|
||||||
|
- 재개 조건: 없음
|
||||||
|
|
||||||
|
## 리뷰어를 위한 체크포인트
|
||||||
|
|
||||||
|
- `slow-mix` row/SUMMARY가 client close와 pending leak 검사 이후의 stability 값을 기록하는지 확인한다.
|
||||||
|
- Go `slow-mix`가 같은 connection 안에 slow/fast request가 겹친 검증 근거 없이 PASS row를 내지 않는지 확인한다.
|
||||||
|
- Kotlin TCP/WS deferred bottleneck은 기존 required SKIP/INCOMPLETE 정책을 유지하는지 확인한다.
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||||
|
|
||||||
|
필수 규칙:
|
||||||
|
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||||
|
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||||
|
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||||
|
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||||
|
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||||
|
|
||||||
|
### REVIEW_TEST-1 중간 검증
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
||||||
|
RUN stress lang=typescript mode=quick transports=tcp profiles=slow-mix
|
||||||
|
INFO stress harness language=TypeScript mode=quick transports=tcp profiles=slow-mix typeName=TestData
|
||||||
|
[slow-mix] transport=tcp mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
[slow-mix] transport=tcp clients=4 done peakMemMb=83.9 violations=0
|
||||||
|
DONE typescript: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
전체 결과값: PASS
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-035913-proto-socket-stress-quick.md`
|
||||||
|
```
|
||||||
|
|
||||||
|
### REVIEW_TEST-2 중간 검증
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang go --transport tcp
|
||||||
|
RUN stress lang=go mode=quick transports=tcp profiles=slow-mix
|
||||||
|
INFO stress harness language=Go mode=quick transports=tcp profiles=slow-mix typeName=TestData
|
||||||
|
[slow-mix] transport=tcp mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
[slow-mix] transport=tcp deferred=deferred: optimize Epic — Go public request API does not expose deterministic same-connection in-flight slow/fast response-arrival order; concurrent SendRequestTyped completion order is goroutine-scheduling dependent
|
||||||
|
DONE go: INCOMPLETE (rows=0 skips=1 required_skips=1 violations=0)
|
||||||
|
전체 결과값: INCOMPLETE
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-035334-proto-socket-stress-quick.md`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 검증
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
||||||
|
RUN stress lang=typescript mode=quick transports=tcp profiles=slow-mix
|
||||||
|
INFO stress harness language=TypeScript mode=quick transports=tcp profiles=slow-mix typeName=TestData
|
||||||
|
[slow-mix] transport=tcp mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
[slow-mix] transport=tcp clients=4 done peakMemMb=83.9 violations=0
|
||||||
|
DONE typescript: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
전체 결과값: PASS
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-035913-proto-socket-stress-quick.md`
|
||||||
|
|
||||||
|
$ cd typescript && npm run check && npm test
|
||||||
|
(stdout/stderr)
|
||||||
|
> proto-socket@1.0.5 check
|
||||||
|
> tsc --noEmit
|
||||||
|
> proto-socket@1.0.5 test
|
||||||
|
> vitest run
|
||||||
|
Test Files 5 passed (5)
|
||||||
|
Tests 50 passed (50)
|
||||||
|
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang dart,go,python,typescript --transport tcp
|
||||||
|
RUN stress lang=dart mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE dart: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
RUN stress lang=go mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE go: INCOMPLETE (rows=0 skips=1 required_skips=1 violations=0)
|
||||||
|
RUN stress lang=python mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE python: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
RUN stress lang=typescript mode=quick transports=tcp profiles=slow-mix
|
||||||
|
DONE typescript: PASS (rows=1 skips=0 required_skips=0 violations=0)
|
||||||
|
전체 결과값: INCOMPLETE
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-035342-proto-socket-stress-quick.md`
|
||||||
|
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang kotlin --transport tcp,ws
|
||||||
|
INFO stress harness language=Kotlin mode=quick transports=tcp,ws profiles=slow-mix typeName=TestData
|
||||||
|
[slow-mix] transport=tcp mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
[slow-mix] transport=ws mode=quick clients=4,slowEvery=5,delayMs=50 perClient=20
|
||||||
|
DONE kotlin: INCOMPLETE (rows=0 skips=2 required_skips=2 violations=0)
|
||||||
|
전체 결과값: INCOMPLETE
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-035352-proto-socket-stress-quick.md`
|
||||||
|
|
||||||
|
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all
|
||||||
|
(stdout/stderr summary)
|
||||||
|
**Proto 동기화**
|
||||||
|
| schema sync | `tools/check_proto_sync.sh` | PASS |
|
||||||
|
**동일언어**
|
||||||
|
| Dart | PASS |
|
||||||
|
| Go | PASS |
|
||||||
|
| Kotlin | PASS |
|
||||||
|
| Python | PASS |
|
||||||
|
| TypeScript | PASS |
|
||||||
|
**언어 PASS 매트릭스**
|
||||||
|
| Dart.io | all clients PASS |
|
||||||
|
| Go | all clients PASS |
|
||||||
|
| Kotlin | all clients PASS |
|
||||||
|
| Python | all clients PASS |
|
||||||
|
| TypeScript | all clients PASS |
|
||||||
|
**크로스테스트 상세**
|
||||||
|
all listed directions PASS, PASS scenarios match Expected, FAIL lines 0
|
||||||
|
로그 디렉터리: `/tmp/proto-socket-matrix.TlRMyN`
|
||||||
|
결과 기록 파일: `agent-test/runs/20260603-035403-proto-socket-full-matrix.md`
|
||||||
|
|
||||||
|
$ git diff --check
|
||||||
|
(no output)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||||
|
> If anything is blank, go back and fill it in before saving this file.
|
||||||
|
> Leave review-agent-only sections unchanged.
|
||||||
|
|
||||||
|
## 코드리뷰 결과
|
||||||
|
|
||||||
|
- 종합 판정: PASS
|
||||||
|
- 차원별 평가
|
||||||
|
- correctness: Pass
|
||||||
|
- completeness: Pass
|
||||||
|
- test coverage: Pass
|
||||||
|
- API contract: Pass
|
||||||
|
- code quality: Pass
|
||||||
|
- plan deviation: Pass
|
||||||
|
- verification trust: Pass
|
||||||
|
- 발견된 문제: 없음
|
||||||
|
- 다음 단계: PASS이므로 `complete.log`를 작성하고 task directory를 archive로 이동한다.
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Complete - m-high-performance-parallel-operations/05+04_slow_mix
|
||||||
|
|
||||||
|
## 완료 일시
|
||||||
|
|
||||||
|
2026-06-03
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
slow-mix profile 추가와 follow-up 검증 신뢰 보강을 2회 리뷰 루프로 완료했다. 최종 판정은 PASS다.
|
||||||
|
|
||||||
|
## 루프 이력
|
||||||
|
|
||||||
|
| Plan | Review | Verdict | 메모 |
|
||||||
|
|------|--------|---------|------|
|
||||||
|
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | FAIL | cleanup 전 row emit으로 pendingLeak hard gate가 거짓 PASS를 낼 수 있었고, Go sequential loop가 same-connection in-flight slow/fast 축을 검증하지 못해 follow-up 생성 |
|
||||||
|
| `plan_cloud_G08_1.log` | `code_review_cloud_G08_1.log` | PASS | cleanup 이후 row/SUMMARY emission으로 보정하고, Go slow-mix를 deterministic 검증 불가 required SKIP/INCOMPLETE로 전환해 PASS 위장을 제거 |
|
||||||
|
|
||||||
|
## 구현/정리 내용
|
||||||
|
|
||||||
|
- `slow-mix` profile을 TypeScript, Dart, Go, Python, Kotlin stress harness와 `run_stress.sh --help` usage에 추가했다.
|
||||||
|
- TypeScript/Dart/Python executable slow-mix row는 mixed fast/slow handler load에서 cleanup 이후 stability counters를 기록한다.
|
||||||
|
- Go slow-mix는 public request API로 deterministic same-connection in-flight slow/fast 검증을 만들 수 없어 required SKIP/INCOMPLETE로 기록한다.
|
||||||
|
- Kotlin slow-mix는 기존 TCP delayed-ACK/WS inbound FIFO deferred bottleneck 정책을 보존해 required SKIP/INCOMPLETE로 기록한다.
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp` - PASS; result=`agent-test/runs/20260603-035913-proto-socket-stress-quick.md`; TypeScript slow-mix TCP row PASS, stability violations 0.
|
||||||
|
- `cd typescript && npm run check && npm test` - PASS; `tsc --noEmit`, Vitest 5 files / 50 tests passed.
|
||||||
|
- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang dart,go,python,typescript --transport tcp` - INCOMPLETE by intended Go required SKIP; result=`agent-test/runs/20260603-035342-proto-socket-stress-quick.md`; Dart/Python/TypeScript rows PASS, Go required SKIP 1, stability violations 0.
|
||||||
|
- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang kotlin --transport tcp,ws` - INCOMPLETE by existing Kotlin required SKIP; result=`agent-test/runs/20260603-035352-proto-socket-stress-quick.md`; required SKIP 2, stability violations 0.
|
||||||
|
- `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` - PASS; result=`agent-test/runs/20260603-035403-proto-socket-full-matrix.md`; proto sync, same-language tests, language PASS matrix, and crosstest details all PASS.
|
||||||
|
- `git diff --check` - PASS; no output.
|
||||||
|
|
||||||
|
## Roadmap Completion
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md`
|
||||||
|
- Completed task ids:
|
||||||
|
- `slow-mix`: PASS; evidence=`plan_cloud_G08_1.log`, `code_review_cloud_G08_1.log`; verification=`agent-test/runs/20260603-035913-proto-socket-stress-quick.md`, `agent-test/runs/20260603-035342-proto-socket-stress-quick.md`, `agent-test/runs/20260603-035352-proto-socket-stress-quick.md`, `agent-test/runs/20260603-035403-proto-socket-full-matrix.md`
|
||||||
|
- Not completed task ids: 없음
|
||||||
|
|
||||||
|
## 잔여 Nit
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
||||||
|
## 후속 작업
|
||||||
|
|
||||||
|
- Go/Kotlin executable slow-mix baseline은 optimize Epic의 deferred bottleneck 해소 후 required SKIP을 실제 row로 전환할 수 있다.
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
<!-- task=m-high-performance-parallel-operations/05+04_slow_mix plan=1 tag=REVIEW_TEST -->
|
||||||
|
|
||||||
|
# Plan - REVIEW_TEST
|
||||||
|
|
||||||
|
## 이 파일을 읽는 구현 에이전트에게
|
||||||
|
|
||||||
|
`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 명령, stdout/stderr로 채우는 것이 구현의 마지막 단계다. 이전 리뷰는 `code_review_cloud_G08_0.log`이며, Required 2건만 해결한다. archive, `complete.log`, 최종 판정은 code-review 전용이다.
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
첫 리뷰에서 `slow-mix` row가 cleanup 전에 emit되어 `pendingLeak` hard gate가 결과에 반영되지 않는 문제가 발견됐다. 또한 Go `slow-mix`는 같은 connection 안에서 요청을 순차로만 보내므로 느린 handler 뒤에 빠른 요청이 pending 되는 FIFO 지연 축을 검증하지 못한다. 이번 후속은 새 profile의 검증 신뢰를 회복하는 데만 집중한다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청 흐름
|
||||||
|
|
||||||
|
구현 중 blocker는 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 그 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 판단한다.
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md`
|
||||||
|
- Task ids:
|
||||||
|
- `slow-mix`: 빠른 handler와 느린 handler가 섞인 혼합 부하 추가
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 분석 결과
|
||||||
|
|
||||||
|
### 읽은 파일
|
||||||
|
|
||||||
|
- `agent-ops/rules/project/rules.md`
|
||||||
|
- `agent-ops/rules/common/rules-roadmap.md`
|
||||||
|
- `agent-roadmap/current.md`
|
||||||
|
- `agent-roadmap/milestones/high-performance-parallel-operations.md`
|
||||||
|
- `agent-ops/skills/common/router.md`
|
||||||
|
- `agent-ops/skills/common/code-review/SKILL.md`
|
||||||
|
- `agent-ops/skills/common/plan/SKILL.md`
|
||||||
|
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
|
||||||
|
- `agent-test/local/rules.md`
|
||||||
|
- `agent-test/local/proto-socket-full-matrix.md`
|
||||||
|
- `agent-test/local/typescript-smoke.md`
|
||||||
|
- `agent-task/m-high-performance-parallel-operations/05+04_slow_mix/plan_cloud_G08_0.log`
|
||||||
|
- `agent-task/m-high-performance-parallel-operations/05+04_slow_mix/code_review_cloud_G08_0.log`
|
||||||
|
- `agent-task/archive/2026/06/m-high-performance-parallel-operations/04+01_load_profile_scales/complete.log`
|
||||||
|
- `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh`
|
||||||
|
- `typescript/bench/stress.ts`
|
||||||
|
- `dart/bench/stress.dart`
|
||||||
|
- `go/bench/stress.go`
|
||||||
|
- `python/bench/stress.py`
|
||||||
|
- `kotlin/crosstest/stress.kt`
|
||||||
|
- `typescript/src/communicator.ts`
|
||||||
|
- `dart/lib/src/communicator.dart`
|
||||||
|
- `go/communicator.go`
|
||||||
|
- `python/proto_socket/communicator.py`
|
||||||
|
- `kotlin/src/main/kotlin/com/tokilabs/proto_socket/Communicator.kt`
|
||||||
|
- `PROTOCOL.md`
|
||||||
|
- `agent-test/runs/20260603-031533-proto-socket-stress-quick.md`
|
||||||
|
- `agent-test/runs/20260603-031543-proto-socket-stress-quick.md`
|
||||||
|
- `agent-test/runs/20260603-031624-proto-socket-stress-quick.md`
|
||||||
|
- `agent-test/runs/20260603-031633-proto-socket-full-matrix.md`
|
||||||
|
|
||||||
|
### 테스트 환경 규칙
|
||||||
|
|
||||||
|
`test_env=local`. `agent-test/local/rules.md`를 읽었고 project-wide/full-cycle은 `agent-test/local/proto-socket-full-matrix.md`, TypeScript 변경은 `agent-test/local/typescript-smoke.md`가 매칭된다. 후속 검증은 focused `run_stress.sh --profile slow-mix`, TypeScript smoke, 전체 matrix, `git diff --check`를 사용한다.
|
||||||
|
|
||||||
|
### 테스트 커버리지 공백
|
||||||
|
|
||||||
|
- `slow-mix` cleanup leak: 현재 row emit이 close/leak 검사보다 먼저라 close 후 leak을 결과 row와 일부 SUMMARY가 보지 못한다.
|
||||||
|
- Go same-connection FIFO delay: 현재 Go row는 client별 순차 `SendRequestTyped` 루프라 같은 connection의 in-flight slow/fast mix를 검증하지 못한다.
|
||||||
|
|
||||||
|
### 심볼 참조
|
||||||
|
|
||||||
|
renamed/removed symbol 없음.
|
||||||
|
|
||||||
|
### 분할 판단
|
||||||
|
|
||||||
|
split policy를 재평가했다. 이 작업은 기존 `05+04_slow_mix` 첫 리뷰의 Required 후속이고, 같은 파일 집합 안에서 row contract를 보강하는 좁은 수정이다. 별도 subtask로 나누면 같은 profile 계약을 둘로 쪼개 검증이 중복되므로 단일 follow-up plan으로 유지한다.
|
||||||
|
|
||||||
|
### 범위 결정 근거
|
||||||
|
|
||||||
|
wire format, proto schema, nonce 의미, production communicator API 변경은 제외한다. Kotlin deferred bottleneck 자체 수정은 optimize Epic 범위라 이번 후속에서 해결하지 않는다. `slow-mix` profile의 결과 신뢰와 Go stress harness 보강만 다룬다.
|
||||||
|
|
||||||
|
### 빌드 등급
|
||||||
|
|
||||||
|
cloud-G08. concurrency stress harness와 verification trust 회복이 핵심이고, 첫 리뷰 Required가 테스트 결과의 PASS 신뢰성에 직접 닿는다.
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] [REVIEW_TEST-1] TypeScript/Dart/Go/Python/Kotlin `slow-mix`에서 client close와 pending leak 검사를 row/SUMMARY emission 전에 반영한다.
|
||||||
|
- [ ] [REVIEW_TEST-2] Go `slow-mix`가 같은 connection의 in-flight slow/fast request FIFO 지연을 실제로 검증하도록 보강한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## [REVIEW_TEST-1] Cleanup Gate Before Row
|
||||||
|
|
||||||
|
### 문제
|
||||||
|
|
||||||
|
`slow-mix` row emission이 cleanup보다 먼저 실행된다.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// typescript/bench/stress.ts:644
|
||||||
|
summarizeLatencies("slow-mix", axis, latencies, performance.now() - started, stability, "off", peakMem, {
|
||||||
|
transport: transportName(transport),
|
||||||
|
payloadBytes: testDataPayloadBytes("sm-0-0"),
|
||||||
|
clientCount,
|
||||||
|
});
|
||||||
|
// ...
|
||||||
|
if (!leakClear(client)) {
|
||||||
|
stability.pendingLeak += 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
같은 패턴이 `dart/bench/stress.dart:625`, `go/bench/stress.go:734`, `python/bench/stress.py:554`, `kotlin/crosstest/stress.kt:583`에도 있다.
|
||||||
|
|
||||||
|
### 해결 방법
|
||||||
|
|
||||||
|
각 `slow-mix` 구현에서 요청 완료 후 바로 row를 emit하지 말고, client close와 `pendingLeak` 검사까지 끝낸 뒤 row를 emit한다. TypeScript는 printed ROW와 SUMMARY가 같은 최종 stability 값을 보도록 `summarizeLatencies` 호출을 cleanup 이후로 옮긴다. Dart/Go/Python/Kotlin은 `_totalViolations`/`totalViol` 누적이 emit 시점 값이므로 반드시 cleanup 이후에 `_emitRow`/`emitRow`가 호출되어야 한다.
|
||||||
|
|
||||||
|
### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `typescript/bench/stress.ts` slow-mix cleanup 순서를 보정한다.
|
||||||
|
- [ ] `dart/bench/stress.dart` slow-mix cleanup 순서를 보정한다.
|
||||||
|
- [ ] `go/bench/stress.go` slow-mix cleanup 순서를 보정한다.
|
||||||
|
- [ ] `python/bench/stress.py` slow-mix cleanup 순서를 보정한다.
|
||||||
|
- [ ] `kotlin/crosstest/stress.kt` slow-mix cleanup 순서를 보정한다.
|
||||||
|
|
||||||
|
### 테스트 작성
|
||||||
|
|
||||||
|
별도 unit test는 작성하지 않는다. 이 파일들이 stress result producer 자체이므로 focused stress 결과 row가 regression artifact다.
|
||||||
|
|
||||||
|
### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
기대: TypeScript slow-mix row PASS, `PendLeak` 0, SUMMARY `stability_violations=0`, 결과 파일 기록.
|
||||||
|
|
||||||
|
## [REVIEW_TEST-2] Go Same-Connection Slow/Fast Verification
|
||||||
|
|
||||||
|
### 문제
|
||||||
|
|
||||||
|
[go/bench/stress.go](/config/workspace/proto-socket/go/bench/stress.go:693)의 Go slow-mix는 client별 goroutine 안에서 request를 하나 보내고 응답을 기다린 뒤 다음 request를 보낸다. 이 방식은 cross-connection concurrency는 만들지만, 같은 connection 안에서 느린 request 뒤 빠른 request가 이미 pending인 상황을 만들지 못한다.
|
||||||
|
|
||||||
|
### 해결 방법
|
||||||
|
|
||||||
|
Go slow-mix가 PASS row를 낼 때는 같은 connection에 slow/fast 요청이 겹친 근거가 있어야 한다. 선호 구현은 bench-only helper를 추가해 한 connection에서 여러 request를 in-flight로 만들고, 느린 index 뒤 fast index가 먼저 완료되면 `fifoViolations`를 증가시키는 방식이다. public request API만으로 deterministic send order를 만들 수 없다는 것이 구현 중 확인되면, Go slow-mix는 PASS row 대신 required SKIP/INCOMPLETE를 emit하고 `계획 대비 변경 사항`에 public API 제약과 재개 조건을 남긴다. 단, sequential per-client request loop를 PASS로 유지하면 안 된다.
|
||||||
|
|
||||||
|
### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `go/bench/stress.go` slow-mix가 same-connection in-flight slow/fast 축을 검증하도록 보강한다.
|
||||||
|
- [ ] deterministic in-flight 검증이 불가능하면 Go slow-mix를 required SKIP으로 바꾸고, runner 결과가 PASS가 아닌 INCOMPLETE로 남는지 확인한다.
|
||||||
|
- [ ] Dart/TypeScript/Python/Kotlin의 slow-mix semantics와 result axis는 변경하지 않는다.
|
||||||
|
|
||||||
|
### 테스트 작성
|
||||||
|
|
||||||
|
별도 unit test는 작성하지 않는다. Go stress harness의 focused `slow-mix` row 또는 required SKIP 결과를 검증 증거로 사용한다.
|
||||||
|
|
||||||
|
### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang go --transport tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
기대: Go가 PASS row를 내는 경우 same-connection in-flight FIFO 검증이 실제 수행되고 stability counters가 0이다. deterministic 검증 불가로 required SKIP을 선택한 경우 전체 결과는 INCOMPLETE이며 PASS로 기록하지 않는다.
|
||||||
|
|
||||||
|
## 의존 관계 및 구현 순서
|
||||||
|
|
||||||
|
1. REVIEW_TEST-1을 먼저 적용해 result contract 신뢰를 회복한다.
|
||||||
|
2. REVIEW_TEST-2로 Go slow-mix의 PASS 의미를 보강한다.
|
||||||
|
3. 최종 검증을 실행하고 active review stub에 실제 stdout/stderr와 결과 파일 경로를 기록한다.
|
||||||
|
|
||||||
|
## 수정 파일 요약
|
||||||
|
|
||||||
|
| 파일 | 항목 |
|
||||||
|
|---|---|
|
||||||
|
| `typescript/bench/stress.ts` | REVIEW_TEST-1 |
|
||||||
|
| `dart/bench/stress.dart` | REVIEW_TEST-1 |
|
||||||
|
| `go/bench/stress.go` | REVIEW_TEST-1, REVIEW_TEST-2 |
|
||||||
|
| `python/bench/stress.py` | REVIEW_TEST-1 |
|
||||||
|
| `kotlin/crosstest/stress.kt` | REVIEW_TEST-1 |
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
||||||
|
cd typescript && npm run check && npm test
|
||||||
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang dart,go,python,typescript --transport tcp
|
||||||
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang kotlin --transport tcp,ws
|
||||||
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
기대: focused TypeScript slow-mix PASS, TypeScript smoke PASS, Dart/Go/Python/TypeScript quick TCP slow-mix PASS 또는 Go가 deterministic 검증 불가를 증명한 required SKIP/INCOMPLETE, Kotlin 기존 required SKIP/INCOMPLETE 유지, full matrix PASS, whitespace 오류 없음.
|
||||||
|
|
||||||
|
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
<!-- task=m-high-performance-parallel-operations/05+04_slow_mix plan=0 tag=TEST -->
|
|
||||||
|
|
||||||
# Code Review Reference - TEST
|
|
||||||
|
|
||||||
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
|
||||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
|
||||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
|
||||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
|
||||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
|
||||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.
|
|
||||||
|
|
||||||
## 개요
|
|
||||||
|
|
||||||
date=2026-06-03
|
|
||||||
task=m-high-performance-parallel-operations/05+04_slow_mix, plan=0, tag=TEST
|
|
||||||
|
|
||||||
## Roadmap Targets
|
|
||||||
|
|
||||||
- Milestone: `agent-roadmap/milestones/high-performance-parallel-operations.md`
|
|
||||||
- Task ids:
|
|
||||||
- `slow-mix`: 빠른 handler와 느린 handler가 섞인 혼합 부하 추가
|
|
||||||
- Completion mode: check-on-pass
|
|
||||||
|
|
||||||
## 이 파일을 읽는 리뷰 에이전트에게
|
|
||||||
|
|
||||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
|
||||||
|
|
||||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
|
||||||
|
|
||||||
## 구현 항목별 완료 여부
|
|
||||||
|
|
||||||
| 항목 | 완료 여부 |
|
|
||||||
|------|---------|
|
|
||||||
| [TEST-1] Runner profile contract | [ ] |
|
|
||||||
| [TEST-2] Slow mix harness | [ ] |
|
|
||||||
| [TEST-3] Verification | [ ] |
|
|
||||||
|
|
||||||
## 구현 체크리스트
|
|
||||||
|
|
||||||
- [ ] [TEST-1] `run_stress.sh` usage와 result contract에 `slow-mix` profile을 추가한다.
|
|
||||||
- [ ] [TEST-2] Dart/Go/Kotlin/Python/TypeScript harness에 `slow-mix` profile을 추가한다. 검증: 느린 handler가 같은 connection FIFO를 계약대로 지연시키되 다른 connection의 nonce/FIFO 상태를 오염시키지 않는다.
|
|
||||||
- [ ] [TEST-3] focused slow-mix stress, TypeScript smoke, 전체 matrix를 실행하고 결과 파일 경로를 review stub에 기록한다.
|
|
||||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
|
||||||
|
|
||||||
## 코드리뷰 전용 체크리스트
|
|
||||||
|
|
||||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
|
||||||
|
|
||||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
|
||||||
- [ ] active review/plan을 `.log`로 아카이브한다.
|
|
||||||
- [ ] PASS이면 `complete.log`를 작성하고 task directory를 archive로 이동한다.
|
|
||||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap을 직접 수정하지 않는다.
|
|
||||||
|
|
||||||
## 계획 대비 변경 사항
|
|
||||||
|
|
||||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
|
||||||
|
|
||||||
## 주요 설계 결정
|
|
||||||
|
|
||||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
|
||||||
|
|
||||||
## 사용자 리뷰 요청
|
|
||||||
|
|
||||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
|
||||||
|
|
||||||
- 상태: 없음
|
|
||||||
- 사유 유형: 없음
|
|
||||||
- 결정 필요: 없음
|
|
||||||
- 차단 근거: 없음
|
|
||||||
- 실행한 검증/명령: 없음
|
|
||||||
- 자동 후속 불가 이유: 없음
|
|
||||||
- 재개 조건: 없음
|
|
||||||
|
|
||||||
## 리뷰어를 위한 체크포인트
|
|
||||||
|
|
||||||
- `04+01_load_profile_scales` dependency가 complete log로 충족된 뒤 구현됐는지 확인한다.
|
|
||||||
- slow handler가 same-connection delay는 허용하되 cross-connection nonce/FIFO counters를 오염시키지 않는지 확인한다.
|
|
||||||
- 새 profile이 unknown/optional SKIP으로 빠지지 않고 실제 row 또는 명확한 required SKIP을 남기는지 확인한다.
|
|
||||||
|
|
||||||
## 검증 결과
|
|
||||||
|
|
||||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
|
||||||
|
|
||||||
### TEST-1 중간 검증
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --help
|
|
||||||
(output)
|
|
||||||
```
|
|
||||||
|
|
||||||
### TEST-2 중간 검증
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
|
||||||
(output)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 최종 검증
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang typescript --transport tcp
|
|
||||||
(output)
|
|
||||||
$ cd typescript && npm run check && npm test
|
|
||||||
(output)
|
|
||||||
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --profile slow-mix --lang dart,go,python,typescript --transport tcp
|
|
||||||
(output)
|
|
||||||
$ bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all
|
|
||||||
(output)
|
|
||||||
$ git diff --check
|
|
||||||
(output)
|
|
||||||
```
|
|
||||||
|
|
||||||
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
|
||||||
|
|
@ -18,7 +18,14 @@ import 'package:proto_socket/proto_socket.dart';
|
||||||
const _host = '127.0.0.1';
|
const _host = '127.0.0.1';
|
||||||
const _language = 'Dart';
|
const _language = 'Dart';
|
||||||
const _wsPath = '/';
|
const _wsPath = '/';
|
||||||
const _allProfiles = ['roundtrip', 'burst', 'sustained', 'parallel', 'payload'];
|
const _allProfiles = [
|
||||||
|
'roundtrip',
|
||||||
|
'burst',
|
||||||
|
'sustained',
|
||||||
|
'parallel',
|
||||||
|
'slow-mix',
|
||||||
|
'payload'
|
||||||
|
];
|
||||||
|
|
||||||
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
||||||
const _payloadSeed = '0123456789abcdef';
|
const _payloadSeed = '0123456789abcdef';
|
||||||
|
|
@ -275,6 +282,20 @@ _ServerHandle _makeEchoServer(String transport, int port) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ServerHandle _makeSlowMixServer(
|
||||||
|
String transport, int port, int slowEvery, Duration delay) {
|
||||||
|
return _makeServer(transport, port, (comm) {
|
||||||
|
comm.addRequestListener<TestData, TestData>((req) async {
|
||||||
|
if ((req.index + 1) % slowEvery == 0) {
|
||||||
|
await Future<void>.delayed(delay);
|
||||||
|
}
|
||||||
|
return TestData()
|
||||||
|
..index = req.index * 2
|
||||||
|
..message = 'echo:${req.message}';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<_ClientHandle> _dial(String transport, int port) async {
|
Future<_ClientHandle> _dial(String transport, int port) async {
|
||||||
if (transport == 'tcp') {
|
if (transport == 'tcp') {
|
||||||
final socket = await ProtobufClient.connect(_host, port)
|
final socket = await ProtobufClient.connect(_host, port)
|
||||||
|
|
@ -540,6 +561,94 @@ Future<void> _profileParallel(String transport, String mode) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 빠른/느린 request handler를 섞어 same-connection FIFO delay와 connection 간 nonce 독립성을 검증한다.
|
||||||
|
Future<void> _profileSlowMix(String transport, String mode) async {
|
||||||
|
final clientCount = mode == 'quick' ? 4 : 16;
|
||||||
|
final perClient = mode == 'quick' ? 20 : 200;
|
||||||
|
final slowEvery = mode == 'quick' ? 5 : 10;
|
||||||
|
final delayMs = mode == 'quick' ? 50 : 100;
|
||||||
|
final timeout = Duration(seconds: mode == 'quick' ? 10 : 30);
|
||||||
|
final axis = 'clients=$clientCount,slowEvery=$slowEvery,delayMs=$delayMs';
|
||||||
|
final s = _Stability();
|
||||||
|
var peak = _memMb();
|
||||||
|
final latencies = <double>[];
|
||||||
|
var elapsedMs = 0.0;
|
||||||
|
_log('[slow-mix] transport=$transport mode=$mode $axis perClient=$perClient');
|
||||||
|
|
||||||
|
final port = await _freePort();
|
||||||
|
final server = _makeSlowMixServer(
|
||||||
|
transport, port, slowEvery, Duration(milliseconds: delayMs));
|
||||||
|
await server.start();
|
||||||
|
final handles = <_ClientHandle>[];
|
||||||
|
final clientIds = <int>[];
|
||||||
|
try {
|
||||||
|
for (var clientId = 0; clientId < clientCount; clientId++) {
|
||||||
|
try {
|
||||||
|
handles.add(await _dialWithRetry(transport, port));
|
||||||
|
clientIds.add(clientId);
|
||||||
|
} catch (error) {
|
||||||
|
s.timeouts += 1;
|
||||||
|
_log('[slow-mix] client=$clientId dial error=$error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final completedByClient = List<int>.filled(clientCount, -1);
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
await Future.wait(List.generate(handles.length, (slot) async {
|
||||||
|
final handle = handles[slot];
|
||||||
|
final clientId = clientIds[slot];
|
||||||
|
await Future.wait(List.generate(perClient, (index) async {
|
||||||
|
final message = 'sm-$clientId-$index';
|
||||||
|
final reqSw = Stopwatch()..start();
|
||||||
|
try {
|
||||||
|
final res = await handle.comm
|
||||||
|
.sendRequest<TestData, TestData>(TestData()
|
||||||
|
..index = index
|
||||||
|
..message = message)
|
||||||
|
.timeout(timeout);
|
||||||
|
latencies.add(reqSw.elapsedMicroseconds / 1000.0);
|
||||||
|
if (res.index != index * 2 || res.message != 'echo:$message') {
|
||||||
|
s.nonceMismatch += 1;
|
||||||
|
}
|
||||||
|
if (index <= completedByClient[clientId]) {
|
||||||
|
s.fifoViolations += 1;
|
||||||
|
}
|
||||||
|
completedByClient[clientId] = index;
|
||||||
|
} catch (error) {
|
||||||
|
_classifyRequestError(error.toString(), s);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
final cur = _memMb();
|
||||||
|
if (cur > peak) peak = cur;
|
||||||
|
elapsedMs = sw.elapsedMicroseconds / 1000.0;
|
||||||
|
} finally {
|
||||||
|
for (final handle in handles) {
|
||||||
|
await handle.close();
|
||||||
|
if (handle.comm.isAlive) s.pendingLeak += 1;
|
||||||
|
}
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
final sorted = List<double>.from(latencies)..sort();
|
||||||
|
final throughput =
|
||||||
|
elapsedMs > 0 ? latencies.length / elapsedMs * 1000.0 : 0.0;
|
||||||
|
_emitRow(
|
||||||
|
profile: 'slow-mix',
|
||||||
|
axis: axis,
|
||||||
|
transport: transport,
|
||||||
|
payloadBytes: _testDataPayloadBytes('sm-0-0'),
|
||||||
|
clientCount: clientCount,
|
||||||
|
requests: latencies.length,
|
||||||
|
throughput: throughput,
|
||||||
|
p50: _percentile(sorted, 50),
|
||||||
|
p95: _percentile(sorted, 95),
|
||||||
|
p99: _percentile(sorted, 99),
|
||||||
|
mem: peak,
|
||||||
|
s: s,
|
||||||
|
);
|
||||||
|
_log('[slow-mix] transport=$transport clients=$clientCount done '
|
||||||
|
'peakMemMb=${peak.toStringAsFixed(1)} violations=${s.violations()}');
|
||||||
|
}
|
||||||
|
|
||||||
/// payload size matrix. 동일 connection request-response를 1KB/64KB/1MB payload에서 측정한다.
|
/// payload size matrix. 동일 connection request-response를 1KB/64KB/1MB payload에서 측정한다.
|
||||||
/// message field를 deterministic filler로 채우고 실제 serialized payload bytes, payload별
|
/// message field를 deterministic filler로 채우고 실제 serialized payload bytes, payload별
|
||||||
/// p50/p95/p99 latency, throughput, peak RSS, stability counters를 결과 row에 남긴다.
|
/// p50/p95/p99 latency, throughput, peak RSS, stability counters를 결과 row에 남긴다.
|
||||||
|
|
@ -648,6 +757,7 @@ final _runners = <String, Future<void> Function(String, String)>{
|
||||||
'burst': _profileBurst,
|
'burst': _profileBurst,
|
||||||
'sustained': _profileSustained,
|
'sustained': _profileSustained,
|
||||||
'parallel': _profileParallel,
|
'parallel': _profileParallel,
|
||||||
|
'slow-mix': _profileSlowMix,
|
||||||
'payload': _profilePayload,
|
'payload': _profilePayload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const (
|
||||||
transWS = "ws"
|
transWS = "ws"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allProfiles = []string{"roundtrip", "burst", "sustained", "parallel", "payload"}
|
var allProfiles = []string{"roundtrip", "burst", "sustained", "parallel", "slow-mix", "payload"}
|
||||||
|
|
||||||
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
||||||
const payloadSeed = "0123456789abcdef"
|
const payloadSeed = "0123456789abcdef"
|
||||||
|
|
@ -623,6 +623,24 @@ func profileParallel(transport string, mode string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func profileSlowMix(transport string, mode string) {
|
||||||
|
clientCount := 4
|
||||||
|
perClient := 20
|
||||||
|
slowEvery := 5
|
||||||
|
delayMs := 50
|
||||||
|
if mode == "full" {
|
||||||
|
clientCount = 16
|
||||||
|
perClient = 200
|
||||||
|
slowEvery = 10
|
||||||
|
delayMs = 100
|
||||||
|
}
|
||||||
|
axis := fmt.Sprintf("clients=%d,slowEvery=%d,delayMs=%d", clientCount, slowEvery, delayMs)
|
||||||
|
logf("[slow-mix] transport=%s mode=%s %s perClient=%d", transport, mode, axis, perClient)
|
||||||
|
reason := "deferred: optimize Epic — Go public request API does not expose deterministic same-connection in-flight slow/fast response-arrival order; concurrent SendRequestTyped completion order is goroutine-scheduling dependent"
|
||||||
|
emitSkip("slow-mix", axis, transport, reason)
|
||||||
|
logf("[slow-mix] transport=%s deferred=%s", transport, reason)
|
||||||
|
}
|
||||||
|
|
||||||
// profilePayload는 payload size matrix를 측정한다. 동일 connection request-response를 1KB/64KB/1MB
|
// profilePayload는 payload size matrix를 측정한다. 동일 connection request-response를 1KB/64KB/1MB
|
||||||
// payload에서 돌리고, message field를 deterministic filler로 채워 실제 serialized payload bytes,
|
// payload에서 돌리고, message field를 deterministic filler로 채워 실제 serialized payload bytes,
|
||||||
// payload별 p50/p95/p99 latency, throughput, peak heap, stability counters를 결과 row에 남긴다.
|
// payload별 p50/p95/p99 latency, throughput, peak heap, stability counters를 결과 row에 남긴다.
|
||||||
|
|
@ -801,6 +819,7 @@ func main() {
|
||||||
"burst": profileBurst,
|
"burst": profileBurst,
|
||||||
"sustained": profileSustained,
|
"sustained": profileSustained,
|
||||||
"parallel": profileParallel,
|
"parallel": profileParallel,
|
||||||
|
"slow-mix": profileSlowMix,
|
||||||
"payload": profilePayload,
|
"payload": profilePayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import com.tokilabs.proto_socket.typeNameOf
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -41,7 +42,7 @@ import kotlin.system.exitProcess
|
||||||
private const val HOST = "127.0.0.1"
|
private const val HOST = "127.0.0.1"
|
||||||
private const val LANGUAGE = "Kotlin"
|
private const val LANGUAGE = "Kotlin"
|
||||||
private const val WS_PATH = "/"
|
private const val WS_PATH = "/"
|
||||||
private val ALL_PROFILES = listOf("roundtrip", "burst", "sustained", "parallel", "payload")
|
private val ALL_PROFILES = listOf("roundtrip", "burst", "sustained", "parallel", "slow-mix", "payload")
|
||||||
|
|
||||||
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
||||||
private const val PAYLOAD_SEED = "0123456789abcdef"
|
private const val PAYLOAD_SEED = "0123456789abcdef"
|
||||||
|
|
@ -172,8 +173,8 @@ private fun deferredBottleneck(transport: String, profile: String, concurrency:
|
||||||
// stability hard gate를 안정적으로 통과하지 못하므로 lang-baseline에서는 deferred로 기록한다.
|
// stability hard gate를 안정적으로 통과하지 못하므로 lang-baseline에서는 deferred로 기록한다.
|
||||||
return "deferred: optimize Epic — Kotlin TcpClient without TCP_NODELAY is delayed-ACK latency-bound and intermittently exceeds the stability timeout under request/response concurrency"
|
return "deferred: optimize Epic — Kotlin TcpClient without TCP_NODELAY is delayed-ACK latency-bound and intermittently exceeds the stability timeout under request/response concurrency"
|
||||||
}
|
}
|
||||||
if (transport == "ws" && profile == "burst") {
|
if (transport == "ws" && (profile == "burst" || profile == "slow-mix")) {
|
||||||
return "deferred: optimize Epic — WS inbound launch-per-frame on Dispatchers.IO breaks per-connection FIFO under burst"
|
return "deferred: optimize Epic — WS inbound launch-per-frame on Dispatchers.IO breaks per-connection FIFO under burst or mixed slow handlers"
|
||||||
}
|
}
|
||||||
if (transport == "ws" && concurrency != null && concurrency > 64) {
|
if (transport == "ws" && concurrency != null && concurrency > 64) {
|
||||||
return "deferred: optimize Epic — WS inbound staging cap (64) disconnects at concurrency>64"
|
return "deferred: optimize Epic — WS inbound staging cap (64) disconnects at concurrency>64"
|
||||||
|
|
@ -242,6 +243,14 @@ private fun startEchoServer(transport: String, port: Int): StressServerHandle =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startSlowMixServer(transport: String, port: Int, slowEvery: Int, delayMs: Long): StressServerHandle =
|
||||||
|
startServer(transport, port) { comm ->
|
||||||
|
addRequestListenerTyped<TestData, TestData>(comm) { req ->
|
||||||
|
if ((req.index + 1) % slowEvery == 0) delay(delayMs)
|
||||||
|
TestData.newBuilder().setIndex(req.index * 2).setMessage("echo:" + req.message).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun dial(transport: String, port: Int): StressClientHandle = when (transport) {
|
private suspend fun dial(transport: String, port: Int): StressClientHandle = when (transport) {
|
||||||
"tcp" -> {
|
"tcp" -> {
|
||||||
val c = DialTcp(HOST, port, 0, 0, parserMap())
|
val c = DialTcp(HOST, port, 0, 0, parserMap())
|
||||||
|
|
@ -306,6 +315,34 @@ private suspend fun sendPayloadOne(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun sendSlowMixOne(
|
||||||
|
comm: Communicator,
|
||||||
|
clientId: Int,
|
||||||
|
index: Int,
|
||||||
|
timeoutMs: Long,
|
||||||
|
completedByClient: Array<AtomicInteger>,
|
||||||
|
latencies: ConcurrentLinkedQueue<Double>,
|
||||||
|
s: StressStability,
|
||||||
|
) {
|
||||||
|
val message = "sm-$clientId-$index"
|
||||||
|
val started = System.nanoTime()
|
||||||
|
try {
|
||||||
|
val res: TestData = com.tokilabs.proto_socket.sendRequestTyped(
|
||||||
|
comm, TestData.newBuilder().setIndex(index).setMessage(message).build(), timeoutMs,
|
||||||
|
)
|
||||||
|
latencies.add((System.nanoTime() - started) / 1e6)
|
||||||
|
if (res.index != index * 2 || res.message != "echo:$message") {
|
||||||
|
s.nonceMismatch.incrementAndGet()
|
||||||
|
}
|
||||||
|
if (index <= completedByClient[clientId].get()) {
|
||||||
|
s.fifoViolations.incrementAndGet()
|
||||||
|
}
|
||||||
|
completedByClient[clientId].set(index)
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
classifyRequestError(error, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun runRequestLoad(comm: Communicator, total: Int, concurrency: Int, timeoutMs: Long, s: StressStability): List<Double> {
|
private suspend fun runRequestLoad(comm: Communicator, total: Int, concurrency: Int, timeoutMs: Long, s: StressStability): List<Double> {
|
||||||
val latencies = ConcurrentLinkedQueue<Double>()
|
val latencies = ConcurrentLinkedQueue<Double>()
|
||||||
var issued = 0
|
var issued = 0
|
||||||
|
|
@ -499,6 +536,65 @@ private suspend fun profileParallel(transport: String, mode: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun profileSlowMix(transport: String, mode: String) {
|
||||||
|
val clientCount = if (mode == "quick") 4 else 16
|
||||||
|
val perClient = if (mode == "quick") 20 else 200
|
||||||
|
val slowEvery = if (mode == "quick") 5 else 10
|
||||||
|
val delayMs = if (mode == "quick") 50L else 100L
|
||||||
|
val timeoutMs = if (mode == "quick") 10_000L else 30_000L
|
||||||
|
val axis = "clients=$clientCount,slowEvery=$slowEvery,delayMs=$delayMs"
|
||||||
|
log("[slow-mix] transport=$transport mode=$mode $axis perClient=$perClient")
|
||||||
|
val deferred = deferredBottleneck(transport, "slow-mix", perClient)
|
||||||
|
if (deferred != null) {
|
||||||
|
emitSkip("slow-mix", axis, transport, deferred)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val s = StressStability()
|
||||||
|
var peak = memMb()
|
||||||
|
val srv = startSlowMixServer(transport, freePort(), slowEvery, delayMs)
|
||||||
|
val handles = mutableListOf<Pair<Int, StressClientHandle>>()
|
||||||
|
val latencies = ConcurrentLinkedQueue<Double>()
|
||||||
|
var elapsedMs = 0.0
|
||||||
|
try {
|
||||||
|
for (clientId in 0 until clientCount) {
|
||||||
|
try {
|
||||||
|
handles.add(clientId to dialWithRetry(transport, srv.port))
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
s.timeouts.incrementAndGet()
|
||||||
|
log("[slow-mix] client=$clientId dial error=$error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val completedByClient = Array(clientCount) { AtomicInteger(-1) }
|
||||||
|
val started = System.nanoTime()
|
||||||
|
coroutineScope {
|
||||||
|
for ((clientId, handle) in handles) {
|
||||||
|
for (index in 0 until perClient) {
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
sendSlowMixOne(handle.comm, clientId, index, timeoutMs, completedByClient, latencies, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val cur = memMb()
|
||||||
|
if (cur > peak) peak = cur
|
||||||
|
elapsedMs = (System.nanoTime() - started) / 1e6
|
||||||
|
} finally {
|
||||||
|
for ((_, handle) in handles) {
|
||||||
|
handle.close()
|
||||||
|
if (handle.comm.isAlive()) s.pendingLeak.incrementAndGet()
|
||||||
|
}
|
||||||
|
srv.stop()
|
||||||
|
}
|
||||||
|
val sorted = latencies.toList().sorted()
|
||||||
|
val throughput = if (elapsedMs > 0) latencies.size / elapsedMs * 1000.0 else 0.0
|
||||||
|
emitRow(
|
||||||
|
"slow-mix", axis, transport, testDataPayloadBytes("sm-0-0"), clientCount, latencies.size,
|
||||||
|
throughput, percentile(sorted, 50.0), percentile(sorted, 95.0), percentile(sorted, 99.0), peak, s,
|
||||||
|
)
|
||||||
|
log("[slow-mix] transport=$transport clients=$clientCount done peakMemMb=${String.format("%.1f", peak)} violations=${s.violations()}")
|
||||||
|
}
|
||||||
|
|
||||||
// profilePayload는 payload size matrix를 측정한다. 동일 connection request-response를 1KB/64KB/1MB
|
// profilePayload는 payload size matrix를 측정한다. 동일 connection request-response를 1KB/64KB/1MB
|
||||||
// payload에서 돌리고, message field를 deterministic filler로 채워 실제 serialized payload bytes,
|
// payload에서 돌리고, message field를 deterministic filler로 채워 실제 serialized payload bytes,
|
||||||
// payload별 p50/p95/p99 latency, throughput, used heap, stability counters를 결과 row에 남긴다.
|
// payload별 p50/p95/p99 latency, throughput, used heap, stability counters를 결과 row에 남긴다.
|
||||||
|
|
@ -595,6 +691,7 @@ fun main(args: Array<String>) = runBlocking {
|
||||||
"burst" to ::profileBurst,
|
"burst" to ::profileBurst,
|
||||||
"sustained" to ::profileSustained,
|
"sustained" to ::profileSustained,
|
||||||
"parallel" to ::profileParallel,
|
"parallel" to ::profileParallel,
|
||||||
|
"slow-mix" to ::profileSlowMix,
|
||||||
"payload" to ::profilePayload,
|
"payload" to ::profilePayload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ from proto_socket.ws_server import WsServer
|
||||||
HOST = "127.0.0.1"
|
HOST = "127.0.0.1"
|
||||||
LANGUAGE = "Python"
|
LANGUAGE = "Python"
|
||||||
WS_PATH = "/"
|
WS_PATH = "/"
|
||||||
ALL_PROFILES = ["roundtrip", "burst", "sustained", "parallel", "payload"]
|
ALL_PROFILES = ["roundtrip", "burst", "sustained", "parallel", "slow-mix", "payload"]
|
||||||
|
|
||||||
# payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
# payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
|
||||||
PAYLOAD_SEED = "0123456789abcdef"
|
PAYLOAD_SEED = "0123456789abcdef"
|
||||||
|
|
@ -282,6 +282,19 @@ def make_echo_server(transport: str, port: int):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_slow_mix_server(transport: str, port: int, slow_every: int, delay: float):
|
||||||
|
async def on_request(req: TestData) -> TestData:
|
||||||
|
if (req.index + 1) % slow_every == 0:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
return TestData(index=req.index * 2, message="echo:" + req.message)
|
||||||
|
|
||||||
|
return make_server(
|
||||||
|
transport,
|
||||||
|
port,
|
||||||
|
lambda client: client.communicator.add_request_listener(type_name_of(TestData), on_request),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_request_load(client, total: int, concurrency: int, timeout: float, stability: Stability) -> list[float]:
|
async def run_request_load(client, total: int, concurrency: int, timeout: float, stability: Stability) -> list[float]:
|
||||||
latencies: list[float] = []
|
latencies: list[float] = []
|
||||||
issued = 0
|
issued = 0
|
||||||
|
|
@ -484,6 +497,88 @@ async def profile_parallel(transport: str, mode: str) -> None:
|
||||||
log(f"[parallel] transport={transport} clients={client_count} done violations={stability.violations()}")
|
log(f"[parallel] transport={transport} clients={client_count} done violations={stability.violations()}")
|
||||||
|
|
||||||
|
|
||||||
|
async def profile_slow_mix(transport: str, mode: str) -> None:
|
||||||
|
client_count = 4 if mode == "quick" else 16
|
||||||
|
per_client = 20 if mode == "quick" else 200
|
||||||
|
slow_every = 5 if mode == "quick" else 10
|
||||||
|
delay_ms = 50 if mode == "quick" else 100
|
||||||
|
timeout = 10.0 if mode == "quick" else 30.0
|
||||||
|
axis = f"clients={client_count},slowEvery={slow_every},delayMs={delay_ms}"
|
||||||
|
stability = Stability()
|
||||||
|
peak = mem_mb()
|
||||||
|
latencies: list[float] = []
|
||||||
|
elapsed = 0.0
|
||||||
|
log(f"[slow-mix] transport={transport} mode={mode} {axis} perClient={per_client}")
|
||||||
|
|
||||||
|
port = free_port()
|
||||||
|
server = make_slow_mix_server(transport, port, slow_every, delay_ms / 1000.0)
|
||||||
|
await server.start()
|
||||||
|
clients: list[tuple[int, object]] = []
|
||||||
|
try:
|
||||||
|
for client_id in range(client_count):
|
||||||
|
try:
|
||||||
|
clients.append((client_id, await dial_with_retry(transport, port)))
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
stability.timeouts += 1
|
||||||
|
log(f"[slow-mix] client={client_id} dial error={exc}")
|
||||||
|
|
||||||
|
completed_by_client = [-1] * client_count
|
||||||
|
started = time.perf_counter()
|
||||||
|
|
||||||
|
async def one_request(client, client_id: int, index: int) -> None:
|
||||||
|
message = f"sm-{client_id}-{index}"
|
||||||
|
req_start = time.perf_counter()
|
||||||
|
try:
|
||||||
|
res = await client.communicator.send_request(
|
||||||
|
TestData(index=index, message=message), TestData, timeout=timeout
|
||||||
|
)
|
||||||
|
latencies.append((time.perf_counter() - req_start) * 1000.0)
|
||||||
|
if res.index != index * 2 or res.message != "echo:" + message:
|
||||||
|
stability.nonce_mismatch += 1
|
||||||
|
if index <= completed_by_client[client_id]:
|
||||||
|
stability.fifo_violations += 1
|
||||||
|
completed_by_client[client_id] = index
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
classify_request_error(str(exc), stability)
|
||||||
|
|
||||||
|
await asyncio.gather(
|
||||||
|
*(
|
||||||
|
one_request(client, client_id, index)
|
||||||
|
for client_id, client in clients
|
||||||
|
for index in range(per_client)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
peak = max(peak, mem_mb())
|
||||||
|
elapsed = (time.perf_counter() - started) * 1000.0
|
||||||
|
finally:
|
||||||
|
for _, client in clients:
|
||||||
|
with suppress(Exception):
|
||||||
|
await asyncio.wait_for(client.close(), 2.0)
|
||||||
|
if client.communicator.is_alive():
|
||||||
|
stability.pending_leak += 1
|
||||||
|
await server.stop()
|
||||||
|
ordered = sorted(latencies)
|
||||||
|
throughput = (len(latencies) / elapsed * 1000.0) if elapsed > 0 else 0.0
|
||||||
|
emit_row(
|
||||||
|
profile="slow-mix",
|
||||||
|
axis=axis,
|
||||||
|
transport=transport,
|
||||||
|
payload_bytes=test_data_payload_bytes("sm-0-0"),
|
||||||
|
client_count=client_count,
|
||||||
|
requests=len(latencies),
|
||||||
|
throughput=throughput,
|
||||||
|
p50=percentile(ordered, 50),
|
||||||
|
p95=percentile(ordered, 95),
|
||||||
|
p99=percentile(ordered, 99),
|
||||||
|
mem=peak,
|
||||||
|
stability=stability,
|
||||||
|
)
|
||||||
|
log(
|
||||||
|
f"[slow-mix] transport={transport} clients={client_count} done "
|
||||||
|
f"peakMemMb={peak:.1f} violations={stability.violations()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def profile_payload(transport: str, mode: str) -> None:
|
async def profile_payload(transport: str, mode: str) -> None:
|
||||||
sizes = PAYLOAD_SIZES_QUICK if mode == "quick" else PAYLOAD_SIZES_FULL
|
sizes = PAYLOAD_SIZES_QUICK if mode == "quick" else PAYLOAD_SIZES_FULL
|
||||||
concurrency = 4 if mode == "quick" else 8
|
concurrency = 4 if mode == "quick" else 8
|
||||||
|
|
@ -584,6 +679,7 @@ RUNNERS = {
|
||||||
"burst": profile_burst,
|
"burst": profile_burst,
|
||||||
"sustained": profile_sustained,
|
"sustained": profile_sustained,
|
||||||
"parallel": profile_parallel,
|
"parallel": profile_parallel,
|
||||||
|
"slow-mix": profile_slow_mix,
|
||||||
"payload": profile_payload,
|
"payload": profile_payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* - burst : 단일 connection fire-and-forget burst, dispatch 순서/누락/leak.
|
* - burst : 단일 connection fire-and-forget burst, dispatch 순서/누락/leak.
|
||||||
* - sustained : 일정 시간 지속 request-response, throughput/p95/p99/peak memory.
|
* - sustained : 일정 시간 지속 request-response, throughput/p95/p99/peak memory.
|
||||||
* - parallel : 다중 connection 동시 request-response, connection별 FIFO/nonce 독립성.
|
* - parallel : 다중 connection 동시 request-response, connection별 FIFO/nonce 독립성.
|
||||||
|
* - slow-mix : 빠른 handler와 느린 handler가 섞인 다중 connection FIFO/nonce 독립성.
|
||||||
* - gateway : communicator `onReceivedFrame` opt-in 경로에서 worker_threads gateway(on) vs
|
* - gateway : communicator `onReceivedFrame` opt-in 경로에서 worker_threads gateway(on) vs
|
||||||
* inline decode fallback(off) 비교 baseline. TcpClient는 inline decode로 enqueue하므로
|
* inline decode fallback(off) 비교 baseline. TcpClient는 inline decode로 enqueue하므로
|
||||||
* gateway 효과는 이 frame-ingest 경로에서만 켤 수 있고, 작은 payload에서는 이득이
|
* gateway 효과는 이 frame-ingest 경로에서만 켤 수 있고, 작은 payload에서는 이득이
|
||||||
|
|
@ -50,13 +51,13 @@ function newTcpClient(socket: net.Socket): TcpClient {
|
||||||
|
|
||||||
const HOST = "127.0.0.1";
|
const HOST = "127.0.0.1";
|
||||||
const WS_PATH = "/";
|
const WS_PATH = "/";
|
||||||
const ALL_PROFILES = ["roundtrip", "burst", "sustained", "parallel", "payload", "gateway"] as const;
|
const ALL_PROFILES = ["roundtrip", "burst", "sustained", "parallel", "slow-mix", "payload", "gateway"] as const;
|
||||||
type Profile = (typeof ALL_PROFILES)[number];
|
type Profile = (typeof ALL_PROFILES)[number];
|
||||||
type Mode = "quick" | "full";
|
type Mode = "quick" | "full";
|
||||||
/** same-language transport baseline 축. gateway profile은 transport-agnostic frame-ingest 경로라 별도다. */
|
/** same-language transport baseline 축. gateway profile은 transport-agnostic frame-ingest 경로라 별도다. */
|
||||||
const ALL_TRANSPORTS = ["tcp", "ws"] as const;
|
const ALL_TRANSPORTS = ["tcp", "ws"] as const;
|
||||||
type WireTransport = (typeof ALL_TRANSPORTS)[number];
|
type WireTransport = (typeof ALL_TRANSPORTS)[number];
|
||||||
const TRANSPORT_PROFILES: ReadonlySet<Profile> = new Set(["roundtrip", "burst", "sustained", "parallel", "payload"]);
|
const TRANSPORT_PROFILES: ReadonlySet<Profile> = new Set(["roundtrip", "burst", "sustained", "parallel", "slow-mix", "payload"]);
|
||||||
|
|
||||||
/** payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다. */
|
/** payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다. */
|
||||||
const PAYLOAD_SIZES_QUICK = [1_024, 65_536] as const;
|
const PAYLOAD_SIZES_QUICK = [1_024, 65_536] as const;
|
||||||
|
|
@ -585,6 +586,104 @@ async function profileParallel(transport: WireTransport, mode: Mode): Promise<vo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 빠른/느린 request handler를 섞어 same-connection FIFO delay와 connection 간 nonce 독립성을 검증한다. */
|
||||||
|
async function profileSlowMix(transport: WireTransport, mode: Mode): Promise<void> {
|
||||||
|
const clientCount = mode === "quick" ? 4 : 16;
|
||||||
|
const perClient = mode === "quick" ? 20 : 200;
|
||||||
|
const slowEvery = mode === "quick" ? 5 : 10;
|
||||||
|
const delayMs = mode === "quick" ? 50 : 100;
|
||||||
|
const timeoutMs = mode === "quick" ? 10_000 : 30_000;
|
||||||
|
const axis = `clients=${clientCount},slowEvery=${slowEvery},delayMs=${delayMs}`;
|
||||||
|
const stability = newStability();
|
||||||
|
let peakMem = memMb();
|
||||||
|
const latencies: number[] = [];
|
||||||
|
let elapsedMs = 0;
|
||||||
|
log(`[slow-mix] transport=${transport} mode=${mode} ${axis} perClient=${perClient}`);
|
||||||
|
|
||||||
|
await withSlowMixServer(transport, slowEvery, delayMs, async (port) => {
|
||||||
|
const clients: Array<{ client: BenchClient; clientId: number }> = [];
|
||||||
|
try {
|
||||||
|
for (let clientId = 0; clientId < clientCount; clientId += 1) {
|
||||||
|
try {
|
||||||
|
clients.push({ client: await connectClient(transport, port), clientId });
|
||||||
|
} catch (err) {
|
||||||
|
stability.timeouts += 1;
|
||||||
|
log(`[slow-mix] client=${clientId} dial error=${errorMessage(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedByClient = Array(clientCount).fill(-1) as number[];
|
||||||
|
const started = performance.now();
|
||||||
|
await Promise.all(
|
||||||
|
clients.map(({ client, clientId }) =>
|
||||||
|
Promise.all(
|
||||||
|
Array.from({ length: perClient }, async (_, index) => {
|
||||||
|
const message = `sm-${clientId}-${index}`;
|
||||||
|
const reqStart = performance.now();
|
||||||
|
try {
|
||||||
|
const res = await client.communicator.sendRequest(
|
||||||
|
create(TestDataSchema, { index, message }),
|
||||||
|
TestDataSchema,
|
||||||
|
timeoutMs,
|
||||||
|
);
|
||||||
|
latencies.push(performance.now() - reqStart);
|
||||||
|
if (res.index !== index * 2 || res.message !== `echo:${message}`) {
|
||||||
|
stability.nonceMismatch += 1;
|
||||||
|
}
|
||||||
|
if (index <= completedByClient[clientId]) {
|
||||||
|
stability.fifoViolations += 1;
|
||||||
|
}
|
||||||
|
completedByClient[clientId] = index;
|
||||||
|
} catch (err) {
|
||||||
|
classifyRequestError(errorMessage(err), stability);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
peakMem = Math.max(peakMem, memMb());
|
||||||
|
elapsedMs = performance.now() - started;
|
||||||
|
} finally {
|
||||||
|
await Promise.all(clients.map(async ({ client }) => client.close()));
|
||||||
|
for (const { client } of clients) {
|
||||||
|
if (!leakClear(client)) {
|
||||||
|
stability.pendingLeak += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
summarizeLatencies("slow-mix", axis, latencies, elapsedMs, stability, "off", peakMem, {
|
||||||
|
transport: transportName(transport),
|
||||||
|
payloadBytes: testDataPayloadBytes("sm-0-0"),
|
||||||
|
clientCount,
|
||||||
|
});
|
||||||
|
log(`[slow-mix] transport=${transport} clients=${clientCount} done peakMemMb=${peakMem.toFixed(1)} violations=${stabilityViolations(stability)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withSlowMixServer(
|
||||||
|
transport: WireTransport,
|
||||||
|
slowEvery: number,
|
||||||
|
delayMs: number,
|
||||||
|
body: (port: number) => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
const server = makeServer(transport);
|
||||||
|
server.onClientConnected = (client: { communicator: Communicator }) => {
|
||||||
|
client.communicator.addRequestListener(TestDataSchema.typeName, async (msg) => {
|
||||||
|
const data = msg as TestData;
|
||||||
|
if ((data.index + 1) % slowEvery === 0) {
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
return create(TestDataSchema, { index: data.index * 2, message: `echo:${data.message}` });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
await server.start();
|
||||||
|
try {
|
||||||
|
await body(server.port);
|
||||||
|
} finally {
|
||||||
|
await server.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* payload size matrix. 동일 connection request-response를 1KB/64KB/1MB payload에서 측정한다.
|
* payload size matrix. 동일 connection request-response를 1KB/64KB/1MB payload에서 측정한다.
|
||||||
* message field를 deterministic filler로 채우고 실제 serialized payload bytes를 기록하며, payload별
|
* message field를 deterministic filler로 채우고 실제 serialized payload bytes를 기록하며, payload별
|
||||||
|
|
@ -828,6 +927,7 @@ async function main(): Promise<void> {
|
||||||
burst: profileBurst,
|
burst: profileBurst,
|
||||||
sustained: profileSustained,
|
sustained: profileSustained,
|
||||||
parallel: profileParallel,
|
parallel: profileParallel,
|
||||||
|
"slow-mix": profileSlowMix,
|
||||||
payload: profilePayload,
|
payload: profilePayload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue