diff --git a/agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh b/agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh index ca49c3f..ad7e473 100755 --- a/agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh +++ b/agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh @@ -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 diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/code_review_cloud_G08_0.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/code_review_cloud_G08_0.log new file mode 100644 index 0000000..08f7443 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/code_review_cloud_G08_0.log @@ -0,0 +1,209 @@ + + +# 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-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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:`로 반환해 기존 nonce/type 검증 패턴을 유지했다. +- TypeScript/Dart/Python/Kotlin runnable 경로는 같은 connection 안의 request completion order를 `fifoViolations`에 반영한다. Kotlin TCP와 Kotlin WS slow-mix는 기존/동일 계열 병목으로 required SKIP 처리한다. +- result row axis는 `clients=,slowEvery=,delayMs=`로 통일했고, `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는 트리거하지 않는다. diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/code_review_cloud_G08_1.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/code_review_cloud_G08_1.log new file mode 100644 index 0000000..6bac4c1 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/code_review_cloud_G08_1.log @@ -0,0 +1,221 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고한다. 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-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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로 이동한다. diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/complete.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/complete.log new file mode 100644 index 0000000..f7daa9d --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/complete.log @@ -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로 전환할 수 있다. diff --git a/agent-task/m-high-performance-parallel-operations/05+04_slow_mix/PLAN-cloud-G08.md b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/plan_cloud_G08_0.log similarity index 100% rename from agent-task/m-high-performance-parallel-operations/05+04_slow_mix/PLAN-cloud-G08.md rename to agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/plan_cloud_G08_0.log diff --git a/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/plan_cloud_G08_1.log b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/plan_cloud_G08_1.log new file mode 100644 index 0000000..a885f50 --- /dev/null +++ b/agent-task/archive/2026/06/m-high-performance-parallel-operations/05+04_slow_mix/plan_cloud_G08_1.log @@ -0,0 +1,194 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-high-performance-parallel-operations/05+04_slow_mix/CODE_REVIEW-cloud-G08.md b/agent-task/m-high-performance-parallel-operations/05+04_slow_mix/CODE_REVIEW-cloud-G08.md deleted file mode 100644 index 5af6131..0000000 --- a/agent-task/m-high-performance-parallel-operations/05+04_slow_mix/CODE_REVIEW-cloud-G08.md +++ /dev/null @@ -1,113 +0,0 @@ - - -# 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-`이면 완료 이벤트 메타데이터를 보고하고 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?** diff --git a/dart/bench/stress.dart b/dart/bench/stress.dart index 2c64b9e..ee463b9 100644 --- a/dart/bench/stress.dart +++ b/dart/bench/stress.dart @@ -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((req) async { + if ((req.index + 1) % slowEvery == 0) { + await Future.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 _profileParallel(String transport, String mode) async { } } +/// 빠른/느린 request handler를 섞어 same-connection FIFO delay와 connection 간 nonce 독립성을 검증한다. +Future _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 = []; + 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 = []; + 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.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() + ..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.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 = Function(String, String)>{ 'burst': _profileBurst, 'sustained': _profileSustained, 'parallel': _profileParallel, + 'slow-mix': _profileSlowMix, 'payload': _profilePayload, }; diff --git a/go/bench/stress.go b/go/bench/stress.go index e2bc2fd..9732bfc 100644 --- a/go/bench/stress.go +++ b/go/bench/stress.go @@ -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, } diff --git a/kotlin/crosstest/stress.kt b/kotlin/crosstest/stress.kt index ac5d407..c2a3684 100644 --- a/kotlin/crosstest/stress.kt +++ b/kotlin/crosstest/stress.kt @@ -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(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, + latencies: ConcurrentLinkedQueue, + 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 { val latencies = ConcurrentLinkedQueue() 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>() + val latencies = ConcurrentLinkedQueue() + 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) = runBlocking { "burst" to ::profileBurst, "sustained" to ::profileSustained, "parallel" to ::profileParallel, + "slow-mix" to ::profileSlowMix, "payload" to ::profilePayload, ) diff --git a/python/bench/stress.py b/python/bench/stress.py index 3fd970b..78e6bf3 100644 --- a/python/bench/stress.py +++ b/python/bench/stress.py @@ -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, } diff --git a/typescript/bench/stress.ts b/typescript/bench/stress.ts index 0807b80..a41a64c 100644 --- a/typescript/bench/stress.ts +++ b/typescript/bench/stress.ts @@ -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 = new Set(["roundtrip", "burst", "sustained", "parallel", "payload"]); +const TRANSPORT_PROFILES: ReadonlySet = 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 { + 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, +): Promise { + 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 { burst: profileBurst, sustained: profileSustained, parallel: profileParallel, + "slow-mix": profileSlowMix, payload: profilePayload, };