 <!-- task=m-performance-hotspot-optimization/06_python_sustained_memory_metric plan=0 tag=TEST -->

# Python Sustained Memory Metric Plan - TEST

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

구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`에서 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 코드를 수정한 뒤 아래 검증을 실행하고, 실제 stdout/stderr를 리뷰 스텁에 붙여 넣은 채 active 파일을 유지하고 리뷰 준비 상태로 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 정책 변경, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌 없이는 사용자에게 직접 질문하지 않는다. 그런 blocker가 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정, 근거, 실행한 명령/출력, 자동 후속 불가 이유, 재개 조건을 기록하고 중단한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

Python sustained full rows는 stability hard gate를 통과하지만 30분 memory peak가 크게 증가한다. 현재 `python/bench/stress.py`의 sustained profile은 모든 latency sample을 `list[float]`에 누적하므로, 장시간 row의 memory peak가 구현 leak이 아니라 benchmark artifact일 가능성이 크다. 이 계획은 sustained latency 저장량을 bounded sample로 제한하면서 request count/throughput은 전체 성공 요청 기준으로 유지해 memory row를 재해석 가능하게 만든다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 템플릿을 따른다. 구현 에이전트는 사용자에게 직접 선택지를 제시하지 않고, code-review가 blocker 정당성을 검토해 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/milestones/performance-hotspot-optimization.md`
- Task ids:
  - `python-sustained-memory-metric`: Python 30분 sustained memory peak 증가가 구현 leak인지 bench artifact인지 분리한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/milestones/performance-hotspot-optimization.md`
- `agent-test/local/rules.md`
- `agent-test/local/proto-socket-performance-baseline.md`
- `agent-test/local/python-smoke.md`
- `agent-test/local/proto-socket-full-matrix.md`
- `agent-test/runs/20260602-232000-proto-socket-stress-full.md` row excerpts
- `agent-test/runs/20260603-062724-proto-socket-stress-full.md` row excerpts
- `agent-test/runs/20260603-035342-proto-socket-stress-quick.md` row excerpts
- `agent-test/runs/20260605-115943-proto-socket-performance-full.md` row excerpts
- `python/bench/stress.py`
- `python/test/test_communicator.py`
- `python/proto_socket/communicator.py`
- `python/proto_socket/tcp_client.py`
- `python/proto_socket/ws_client.py`
- `python/proto_socket/__init__.py`
- `python/README.md`
- `python/pyproject.toml`

### 테스트 환경 규칙

test_env는 `local`이다. `agent-test/local/rules.md`를 읽었고, 성능/latency/sustained 작업이므로 `agent-test/local/proto-socket-performance-baseline.md`를 적용한다. Python 파일 변경이 있으므로 `agent-test/local/python-smoke.md`도 적용한다. 최종 검증은 `cd python && python3 -m pytest -q`와 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang python --transport tcp --profile sustained`이다. 두 명령은 계획 작성 전 sanity check에서 PASS했다.

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

변경 대상은 Python 제품 transport가 아니라 benchmark harness의 sustained metric 저장 방식이다. 기존 `python/test/test_communicator.py`는 communicator gateway/FIFO/backpressure/cleanup을 검증하지만 `python/bench/stress.py`의 sustained latency sample count와 throughput count 분리를 검증하지 않는다. 새 focused test를 `python/test/test_stress_harness.py`에 추가해야 한다.

### 심볼 참조

Renamed/removed symbols: none. 새 helper/class를 추가하고 기존 `summarize` 호출의 하위 호환을 유지한다.

### 분할 판단

split decision policy를 먼저 평가했다. 이 작업은 `python/bench/stress.py`와 focused Python test 하나에 한정되고, API/foundation과 광범위 call-site rollout이 분리되지 않는다. 검증도 Python unit과 focused stress quick로 닫히며 다른 언어나 protocol schema를 건드리지 않는다. 따라서 shared task group `m-performance-hotspot-optimization` 아래 단일 subtask `06_python_sustained_memory_metric`으로 둔다.

### 범위 결정 근거

Python runtime transport(`tcp_client.py`, `ws_client.py`)와 `Communicator` gateway policy는 이미 마일스톤에서 즉시 정리했으므로 이 계획에서 변경하지 않는다. full 30분 성능 측정은 구현자가 로컬 quick 검증으로 bounded sampling 동작을 확인한 뒤, 필요하면 별도 idle host run에서 수행할 후속 evidence로 둔다. Dart/Go/Kotlin/TypeScript stress harness는 변경하지 않는다.

### 빌드 등급

build=`local-G05`, review=`local-G05`. 변경 범위는 Python benchmark harness와 focused test로 제한되고, 검증 명령이 deterministic하게 재실행 가능하다.

## 구현 체크리스트

- [ ] `python/bench/stress.py` sustained profile에 bounded latency sampling을 추가하고 전체 request count/throughput 계산을 sample length와 분리한다.
- [ ] `python/test/test_stress_harness.py`에 bounded sample count와 request count 기반 throughput regression test를 추가한다.
- [ ] `cd python && python3 -m pytest -q`를 실행해 Python unit 전체가 PASS인지 확인한다.
- [ ] `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang python --transport tcp --profile sustained`를 실행해 focused sustained row가 PASS인지 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [TEST-1] Bound Sustained Latency Samples

#### 문제

`python/bench/stress.py:422-447`은 sustained duration 동안 모든 latency를 `latencies: list[float]`에 누적하고 `summarize()`는 `len(latencies)`로 requests와 throughput을 계산한다. full 30분 row에서는 요청 수가 수천만 건까지 증가하므로 memory peak가 Python implementation leak인지 latency sample 보관 artifact인지 분리되지 않는다.

```python
409 async def profile_sustained(transport: str, mode: str) -> None:
...
421             client = await dial_with_retry(transport, port)
422             latencies: list[float] = []
...
437                             latencies.append((time.perf_counter() - started) * 1000.0)
...
447                 summarize("sustained", f"duration={duration_ms}ms", transport, latencies, float(duration_ms), 1, stability, peak)
```

#### 해결 방법

`SUSTAINED_MAX_LATENCY_SAMPLES = 100_000`과 deterministic reservoir sampler를 추가한다. `summarize()`에는 optional `request_count`를 추가해 기존 호출은 유지하고, sustained profile만 `request_count=sampler.count`를 넘긴다.

```python
SUSTAINED_MAX_LATENCY_SAMPLES = 100_000


class LatencyReservoir:
    def __init__(self, max_samples: int) -> None:
        self.max_samples = max_samples
        self.count = 0
        self.samples: list[float] = []

    def add(self, value: float) -> None:
        self.count += 1
        if len(self.samples) < self.max_samples:
            self.samples.append(value)
            return
        slot = (self.count * 1103515245 + 12345) % self.count
        if slot < self.max_samples:
            self.samples[slot] = value
```

`summarize()`는 `requests = request_count if request_count is not None else len(latencies)`를 사용하고 throughput도 `requests` 기준으로 계산한다. p50/p95/p99는 bounded sample snapshot 기준으로 계산한다.

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

- [ ] `python/bench/stress.py`: bounded latency reservoir/helper 추가.
- [ ] `python/bench/stress.py`: `summarize()`에 `request_count` optional parameter 추가.
- [ ] `python/bench/stress.py`: `profile_sustained()`에서 `latencies` 대신 reservoir를 사용하고 summary requests를 전체 성공 count로 유지.
- [ ] `python/test/test_stress_harness.py`: reservoir bound와 summarize request_count regression test 추가.

#### 테스트 작성

`python/test/test_stress_harness.py`를 새로 작성한다. 테스트 이름은 `test_latency_reservoir_bounds_samples_and_tracks_count`, `test_summarize_uses_request_count_for_throughput`로 둔다. 첫 테스트는 sample count가 max를 넘지 않고 count는 전체 입력 수를 보존하는지 확인한다. 두 번째 테스트는 `emit_row` monkeypatch로 `summarize(..., request_count=200)`이 `requests=200`, `throughput=200.0`을 emit하는지 확인한다.

#### 중간 검증

```bash
cd python && python3 -m pytest -q test/test_stress_harness.py
```

기대 결과: 새 focused tests PASS.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `python/bench/stress.py` | TEST-1 |
| `python/test/test_stress_harness.py` | TEST-1 |

## 최종 검증

```bash
cd python && python3 -m pytest -q
```

기대 결과: Python unit 전체 PASS.

```bash
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_stress.sh --quick --lang python --transport tcp --profile sustained
```

기대 결과: Python sustained quick row PASS, stability violations 0, 결과 기록 파일 생성.

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