Files
core2026/docs/architecture/npc-lifecycle-memory-profile.md
T

5.9 KiB

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-drainbalanced-churnretainprune을 같은 입력으로 A/B 실행할 수 있다. retain은 수정 전처럼 삭제된 예약 큐를 남기는 비교 모드이고, prune은 PostgreSQL world flush 성공 뒤 삭제된 장수·국가 큐를 제거하는 현재 제품 경계다. 큐 제거는 EngineStateManager.transaction() 안에서 수행되므로 이후 오류가 발생하면 transaction savepoint가 큐와 journal set까지 복원한다.

실행

기본 행렬은 1,200명, 80 cycle, cycle당 100명, 5 cycle 간격 sample이다.

NPC_LIFECYCLE_MEMORY_REPORT_PATH=/dev/shm/npc-lifecycle-memory.json \
pnpm --filter @sammo-ts/game-engine profile:npc-lifecycle-memory

시나리오와 규모를 좁힐 수 있다.

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 수에 더해 generalTurnQueuesnationTurnQueues를 출력한다. 정상 flush가 이어지는 장기 서버에서는 generalTurnQueues - generals가 사망 누적으로 계속 증가하지 않아야 한다. heap/RSS만으로 queue leak을 추정하지 말고 두 count와 OOM/restart, 월 진행을 함께 관찰한다.