perf(game-engine): 대규모 NPC 천통 시간을 계측

시나리오 2601의 880명·94도시를 DB와 Redis 없이 자동 실행하고 커맨드, 수뇌 여부, 연월별 시간을 JSON으로 기록한다.
This commit is contained in:
2026-08-15 19:36:38 +00:00
parent 09777ef843
commit 20c04283fb
7 changed files with 767 additions and 0 deletions
@@ -0,0 +1,90 @@
# NPC 대규모 인메모리 천통 시간 벤치마크
## 목적
`scenario_2601`의 장수 roster와 `che` 지도를 사용해 PostgreSQL·Redis를
연결하지 않은 계산 전용 월드에서 NPC가 건국부터 통일까지 자동 진행하는 시간을
측정한다. 전체 wall time과 함께 다음 구간을 분리한다.
- 일반/국가 커맨드 실행 시간
- NPC AI 판단 시간
- 수뇌(`officerLevel >= 5`)와 일반 NPC 장수 턴 시간
- 게임 연월별 월 처리 시간과 국가·장수 수
이 벤치마크는 DB throughput이나 production daemon capacity를 측정하지 않는다.
서버 계산량의 상한/회귀를 재현하는 프로파일이며 실제 운영 wall time에는
PostgreSQL transaction, flush, lease, Redis, API와 다른 프로세스 경합을 별도로
더해야 한다.
## 실행
기본 실행은 고정 seed, 시나리오 2601, 880명, 94도시, 10분 턴과 300년 안전
상한을 사용한다.
```sh
NPC_UNIFICATION_BENCHMARK_CONVERGENCE_ASSIST=none \
NPC_UNIFICATION_BENCHMARK_MAX_YEAR=300 \
NPC_UNIFICATION_BENCHMARK_REPORT_PATH=/dev/shm/npc-unification.json \
pnpm --filter @sammo-ts/game-engine profile:npc-unification-timing
```
`NPC_UNIFICATION_BENCHMARK_CONVERGENCE_ASSIST=none`이 무보정 자연 진행이다.
비교용 `nation-1-max-city`는 매월 1번 국가가 이미 소유한 도시의 인구·내정·방어·성벽과
민심을 최대값으로 복원한다. 국가 선택, 외교, 출병, 전투, 점령, 멸망과 통일
판정을 직접 만들지는 않는다. 대형 fixture가 유한 시간에 통일하도록 한 기존
`npcNationUprisingUnification.test.ts`의 수렴 보조를 94도시로 확장한 것이다.
## 실행 경계
포함하는 경로는 다음과 같다.
- `scenario_2601.json` 합성 loader, `map_che.json`, `unitset_che.json`
- `buildScenarioBootstrap()`의 deterministic 초기 배치
- `InMemoryTurnWorld`, `InMemoryReservedTurnStore`, `InMemoryTurnProcessor`
- `GeneralAI.chooseNationTurn()``chooseGeneralTurn()`
- 실제 일반/국가 action definition, 전투, 점령과 국가 멸망
- test harness의 국가 예약턴 월 갱신, 수입, NPC 세금, 전선 갱신
- 실제 `unificationHandler.ts`의 단일 국가·전 도시 소유 판정과 in-memory draft
제외하는 경로는 다음과 같다.
- PostgreSQL loader, transaction, lease, flush와 통일 최종 archive
- Redis tournament/realtime
- scenario event action과 yearbook persistence
- API, scheduler sleep, worker IPC와 browser
월마다 `world.consumeDirtyState()`와 예약 큐 acknowledgement를 호출해 log,
message와 persistence draft를 메모리 sink로 비운다. DB write는 수행하지 않지만
운영 flush 뒤 메모리 draft가 정리되는 경계는 재현한다.
## 계측 정의
`createReservedTurnHandler()`의 선택적 `onActionProfiled` hook은 hook을 넘긴
경우에만 `process.hrtime.bigint()`로 다음 시간을 잰다.
- `aiDecisionDurationNs`: `GeneralAI` 생성/재사용과 해당
`chooseNationTurn()` 또는 `chooseGeneralTurn()` 호출
- `actionDurationNs`: 선택된 action의 parse, constraint, execute와 결과 patch
생성
`InMemoryTurnProcessor`의 before/after hook은 한 장수 턴 전체를 잰다. 수뇌는
한 턴에 국가 AI와 일반 AI를 모두 실행할 수 있으므로 수뇌의 “판단/턴”은 두
decision 합계를 수뇌 턴 수로 나누어 해석한다. JSON은 커맨드별 count/total/
average/p50/p95/p99/max, 수뇌 여부, 연월별 합계와 process memory high-water를
포함한다.
계측 clock 호출과 배열 수집 자체의 overhead가 wall time에 포함된다. 같은
고정 seed의 독립 실행을 반복하여 결과 state가 같고 wall time만 변하는지 함께
확인해야 한다.
## 2026-08-15 기준 결과
AMD Ryzen 7 9800X3D, 16 logical CPU, Node v24.18.0, Linux x64의 shared 개발
호스트에서 무보정 자연 실행 두 번 모두 242년 4월, 747개월, 46개국 건국,
총 699,892 장수 턴과 최종 장수 970명으로 동일하게 통일했다. wall time은
483.68초와 466.91초(평균 475.29초)였다. 상세 수치와 커맨드 33종 표는 상위 작업공간의
`report/2026-08-15-NPC-대규모-인메모리-천통-벤치마크.md`에 기록한다.
수렴 보조 실행 두 번은 225년 11월에 375.24/422.68초로 끝났다.
도시 자원 복원이 전쟁과 통일 연월에 영향을 줬으므로 이 수치는 자연 실행과 분리한
비교 근거로만 사용한다.