107 lines
5.9 KiB
Markdown
107 lines
5.9 KiB
Markdown
# NPC 생명주기 메모리 프로파일
|
||
|
||
## 목적과 해석 경계
|
||
|
||
턴 데몬이 오래 실행될 때의 안정 상태와 NPC 생성·사망 churn을 서로 분리해
|
||
측정한다. 각 case는 별도 Node/Vitest worker에서 실행하며 명시적 GC 뒤의
|
||
retained heap, RSS, rollback participant의 V8 직렬화 크기, 실제 live 장수 수와
|
||
예약 턴 큐 수를 기록한다.
|
||
|
||
이 프로파일은 `scenario_2601`의 실제 장수 shape, `InMemoryTurnWorld`,
|
||
`InMemoryReservedTurnStore`, `EngineStateManager.transaction()`과 flush 뒤
|
||
acknowledgement를 사용한다. PostgreSQL/Redis I/O, 실제 NPC AI·전투·월간 event,
|
||
다른 PM2 process와 allocator/cgroup 경합은 포함하지 않는다. 따라서 heap과
|
||
snapshot은 엔진 상태 보유량 비교에 사용하고 RSS는 동일 host의 보조 high-water로만
|
||
해석한다.
|
||
|
||
## 시나리오
|
||
|
||
| 이름 | 고정 조건 | 확인하는 위험 |
|
||
| --- | --- | --- |
|
||
| `steady-state` | 1,200명, 매 cycle 100명 update/예약 턴 shift, 생성·사망 0 | 장기 transaction/flush 자체의 retained heap drift |
|
||
| `growth` | 1,200명에서 cycle마다 100명 생성 | live NPC 1명당 world+예약 큐 증가량 |
|
||
| `death-drain` | 종료 시 1,200명이 되도록 큰 roster에서 cycle마다 100명 사망 | 사망 뒤 world와 예약 큐가 실제로 줄어드는지 |
|
||
| `balanced-churn` | 1,200명을 유지하면서 cycle마다 100명 사망+100명 생성 | live 수가 일정해도 과거 ID/queue가 남는 누수 |
|
||
| `rollback-churn` | cycle마다 100명 생성+사망 후 강제 실패 | rollback snapshot 복원 뒤 retained state drift |
|
||
|
||
`death-drain`과 `balanced-churn`은 `retain`과 `prune`을 같은 입력으로 A/B
|
||
실행할 수 있다. `retain`은 수정 전처럼 삭제된 예약 큐를 남기는 비교 모드이고,
|
||
`prune`은 PostgreSQL world flush 성공 뒤 삭제된 장수·국가 큐를 제거하는 현재
|
||
제품 경계다. 큐 제거는 `EngineStateManager.transaction()` 안에서 수행되므로 이후
|
||
오류가 발생하면 transaction savepoint가 큐와 journal set까지 복원한다.
|
||
|
||
## 실행
|
||
|
||
기본 행렬은 1,200명, 80 cycle, cycle당 100명, 5 cycle 간격 sample이다.
|
||
|
||
```sh
|
||
NPC_LIFECYCLE_MEMORY_REPORT_PATH=/dev/shm/npc-lifecycle-memory.json \
|
||
pnpm --filter @sammo-ts/game-engine profile:npc-lifecycle-memory
|
||
```
|
||
|
||
시나리오와 규모를 좁힐 수 있다.
|
||
|
||
```sh
|
||
NPC_LIFECYCLE_MEMORY_SCENARIOS=balanced-churn@retain,balanced-churn@prune \
|
||
NPC_LIFECYCLE_MEMORY_CYCLES=240 \
|
||
NPC_LIFECYCLE_MEMORY_BATCH_SIZE=100 \
|
||
NPC_LIFECYCLE_MEMORY_SAMPLE_EVERY=15 \
|
||
NPC_LIFECYCLE_MEMORY_BASE_GENERALS=1200 \
|
||
NPC_LIFECYCLE_MEMORY_REPORT_PATH=/dev/shm/npc-lifecycle-memory-churn-240.json \
|
||
pnpm --filter @sammo-ts/game-engine profile:npc-lifecycle-memory
|
||
```
|
||
|
||
지원 변수:
|
||
|
||
- `NPC_LIFECYCLE_MEMORY_SCENARIOS`: 쉼표 구분 `name@prune|retain`
|
||
- `NPC_LIFECYCLE_MEMORY_CYCLES`: transaction/flush 반복 횟수
|
||
- `NPC_LIFECYCLE_MEMORY_BATCH_SIZE`: cycle당 update/create/delete 수
|
||
- `NPC_LIFECYCLE_MEMORY_SAMPLE_EVERY`: 명시적 GC와 snapshot sample 간격
|
||
- `NPC_LIFECYCLE_MEMORY_BASE_GENERALS`: steady/growth/churn의 live 기준 수
|
||
- `NPC_LIFECYCLE_MEMORY_REPETITIONS`: 각 독립 case 반복 횟수
|
||
- `NPC_LIFECYCLE_MEMORY_REPORT_PATH`: aggregate JSON 경로
|
||
|
||
## 2026-08-24 기준 결과
|
||
|
||
Node v24.18.0, Linux x64 shared 개발 host에서 80×100 행렬을 독립적으로 두 번
|
||
실행했다. 게임 결과에 영향을 주지 않는 profile 수치 중 retained heap delta는
|
||
두 번의 차이가 14 KiB 이내였고 snapshot delta와 최종 queue 수는 정확히
|
||
일치했다.
|
||
|
||
| case | 최종 live/queue | retained heap delta (run 1/2) | snapshot delta | 해석 |
|
||
| --- | ---: | ---: | ---: | --- |
|
||
| steady 80 | 1,200 / 1,200 | +121,664 / +126,720 B | 0 B | transaction 반복 자체는 안정 |
|
||
| growth 8,000명 | 9,200 / 9,200 | +37,487,264 / +37,494,272 B | +13,807,868 B | live NPC 약 4.69 KiB heap, 1.73 KiB snapshot/명 |
|
||
| death retain | 1,200 / 9,200 | -11,407,296 / -11,393,936 B | -7,450,860 B | world는 줄지만 죽은 큐 8,000개 잔존 |
|
||
| death prune | 1,200 / 1,200 | -37,058,496 / -37,054,712 B | -13,802,736 B | live roster와 queue가 함께 감소 |
|
||
| balanced retain | 1,200 / 9,200 | +25,970,064 / +25,969,504 B | +6,357,008 B | live 수가 같아도 과거 큐가 선형 증가 |
|
||
| balanced prune | 1,200 / 1,200 | +314,464 / +305,984 B | +5,132 B | 8,000명 churn 뒤 dead queue 0 |
|
||
| rollback churn | 1,200 / 1,200 | +1,982,920 / +1,985,664 B | +72,000 B | 80회 강제 rollback 후 live/queue 복원 |
|
||
|
||
장기 soak도 별도로 실행했다.
|
||
|
||
- steady 720 cycle: live/queue `1,200/1,200`, heap `+217,192 B`, snapshot `0 B`,
|
||
후반 slope 약 `182 B/cycle`
|
||
- balanced 24,000명 churn, retain: queue 25,200, heap `+77,121,104 B`,
|
||
snapshot `+19,063,275 B`
|
||
- 같은 churn, prune: queue 1,200, heap `+321,256 B`, snapshot `+7,400 B`
|
||
|
||
수정 전 예약 큐 보유량은 churn 1명당 retained heap 약 3.2 KiB, 직렬화된
|
||
transaction participant 약 794 B로 선형 증가했다. 정리 후 두 장기 case에서
|
||
dead queue는 0이며 live 수가 일정한 snapshot은 ID 문자열 길이 차이 외에는
|
||
거의 일정했다.
|
||
|
||
`growth` 9,200명 sample에서 in-transaction heap은 같은 cycle의 flush 후보다
|
||
최대 약 14.1 MiB 높았다. 현재 world rollback은 live state 전체를 deep clone하므로
|
||
다음 절감 후보는 mutation별 copy-on-write/delta journal이다. 다만 이 변경은
|
||
오류 rollback, RNG와 persistence 재시도 순서에 직접 닿으므로 이번 작업에서는
|
||
예약 큐처럼 소유권이 명확한 항목만 정리하고 world snapshot 구조는 바꾸지 않는다.
|
||
|
||
## 운영 관찰
|
||
|
||
턴 데몬 5분 telemetry는 world entity 수에 더해 `generalTurnQueues`와
|
||
`nationTurnQueues`를 출력한다. 정상 flush가 이어지는 장기 서버에서는
|
||
`generalTurnQueues - generals`가 사망 누적으로 계속 증가하지 않아야 한다.
|
||
heap/RSS만으로 queue leak을 추정하지 말고 두 count와 OOM/restart, 월 진행을 함께
|
||
관찰한다.
|