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)
|
||||
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
|
||||
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 _language = 'Dart';
|
||||
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)을 측정한다.
|
||||
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 {
|
||||
if (transport == 'tcp') {
|
||||
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에서 측정한다.
|
||||
/// message field를 deterministic filler로 채우고 실제 serialized payload bytes, payload별
|
||||
/// p50/p95/p99 latency, throughput, peak RSS, stability counters를 결과 row에 남긴다.
|
||||
|
|
@ -648,6 +757,7 @@ final _runners = <String, Future<void> Function(String, String)>{
|
|||
'burst': _profileBurst,
|
||||
'sustained': _profileSustained,
|
||||
'parallel': _profileParallel,
|
||||
'slow-mix': _profileSlowMix,
|
||||
'payload': _profilePayload,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const (
|
|||
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)을 측정한다.
|
||||
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
|
||||
// payload에서 돌리고, message field를 deterministic filler로 채워 실제 serialized payload bytes,
|
||||
// payload별 p50/p95/p99 latency, throughput, peak heap, stability counters를 결과 row에 남긴다.
|
||||
|
|
@ -801,6 +819,7 @@ func main() {
|
|||
"burst": profileBurst,
|
||||
"sustained": profileSustained,
|
||||
"parallel": profileParallel,
|
||||
"slow-mix": profileSlowMix,
|
||||
"payload": profilePayload,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tokilabs.proto_socket.typeNameOf
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -41,7 +42,7 @@ import kotlin.system.exitProcess
|
|||
private const val HOST = "127.0.0.1"
|
||||
private const val LANGUAGE = "Kotlin"
|
||||
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)을 측정한다.
|
||||
private const val PAYLOAD_SEED = "0123456789abcdef"
|
||||
|
|
@ -172,8 +173,8 @@ private fun deferredBottleneck(transport: String, profile: String, concurrency:
|
|||
// 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"
|
||||
}
|
||||
if (transport == "ws" && profile == "burst") {
|
||||
return "deferred: optimize Epic — WS inbound launch-per-frame on Dispatchers.IO breaks per-connection FIFO under 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 or mixed slow handlers"
|
||||
}
|
||||
if (transport == "ws" && concurrency != null && 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) {
|
||||
"tcp" -> {
|
||||
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> {
|
||||
val latencies = ConcurrentLinkedQueue<Double>()
|
||||
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
|
||||
// payload에서 돌리고, message field를 deterministic filler로 채워 실제 serialized payload bytes,
|
||||
// payload별 p50/p95/p99 latency, throughput, used heap, stability counters를 결과 row에 남긴다.
|
||||
|
|
@ -595,6 +691,7 @@ fun main(args: Array<String>) = runBlocking {
|
|||
"burst" to ::profileBurst,
|
||||
"sustained" to ::profileSustained,
|
||||
"parallel" to ::profileParallel,
|
||||
"slow-mix" to ::profileSlowMix,
|
||||
"payload" to ::profilePayload,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from proto_socket.ws_server import WsServer
|
|||
HOST = "127.0.0.1"
|
||||
LANGUAGE = "Python"
|
||||
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_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]:
|
||||
latencies: list[float] = []
|
||||
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()}")
|
||||
|
||||
|
||||
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:
|
||||
sizes = PAYLOAD_SIZES_QUICK if mode == "quick" else PAYLOAD_SIZES_FULL
|
||||
concurrency = 4 if mode == "quick" else 8
|
||||
|
|
@ -584,6 +679,7 @@ RUNNERS = {
|
|||
"burst": profile_burst,
|
||||
"sustained": profile_sustained,
|
||||
"parallel": profile_parallel,
|
||||
"slow-mix": profile_slow_mix,
|
||||
"payload": profile_payload,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
* - burst : 단일 connection fire-and-forget burst, dispatch 순서/누락/leak.
|
||||
* - sustained : 일정 시간 지속 request-response, throughput/p95/p99/peak memory.
|
||||
* - parallel : 다중 connection 동시 request-response, connection별 FIFO/nonce 독립성.
|
||||
* - slow-mix : 빠른 handler와 느린 handler가 섞인 다중 connection FIFO/nonce 독립성.
|
||||
* - gateway : communicator `onReceivedFrame` opt-in 경로에서 worker_threads gateway(on) vs
|
||||
* inline decode fallback(off) 비교 baseline. TcpClient는 inline decode로 enqueue하므로
|
||||
* gateway 효과는 이 frame-ingest 경로에서만 켤 수 있고, 작은 payload에서는 이득이
|
||||
|
|
@ -50,13 +51,13 @@ function newTcpClient(socket: net.Socket): TcpClient {
|
|||
|
||||
const HOST = "127.0.0.1";
|
||||
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 Mode = "quick" | "full";
|
||||
/** same-language transport baseline 축. gateway profile은 transport-agnostic frame-ingest 경로라 별도다. */
|
||||
const ALL_TRANSPORTS = ["tcp", "ws"] as const;
|
||||
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)을 측정한다. */
|
||||
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에서 측정한다.
|
||||
* message field를 deterministic filler로 채우고 실제 serialized payload bytes를 기록하며, payload별
|
||||
|
|
@ -828,6 +927,7 @@ async function main(): Promise<void> {
|
|||
burst: profileBurst,
|
||||
sustained: profileSustained,
|
||||
parallel: profileParallel,
|
||||
"slow-mix": profileSlowMix,
|
||||
payload: profilePayload,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue