perf: NPC 생명주기 메모리 프로파일과 큐 정리를 추가한다

This commit is contained in:
2026-08-24 13:02:19 +00:00
parent 8ef93ecadc
commit f9cb60a50f
15 changed files with 997 additions and 2 deletions
+7
View File
@@ -96,10 +96,17 @@ export const runTurnDaemonCli = async (options: TurnDaemonCliOptions = {}): Prom
intervalMs: memoryReportIntervalMs,
getContext: () => {
const state = runtime.world.getState();
const queueCounts = runtime.reservedTurns?.getQueueCounts();
return {
year: state.currentYear,
month: state.currentMonth,
...runtime.world.getEntityCounts(),
...(queueCounts
? {
generalTurnQueues: queueCounts.generalQueues,
nationTurnQueues: queueCounts.nationQueues,
}
: {}),
lifecycleState: runtime.lifecycle.getStatus().state,
};
},
@@ -1787,6 +1787,7 @@ export const createDatabaseTurnHooks = async (
world.acknowledgeDirtyState(changes);
if (options?.reservedTurns && reservedTurnChanges) {
options.reservedTurns.acknowledgeDirtyState(reservedTurnChanges);
options.reservedTurns.pruneDeletedEntityQueues(deletedGenerals, deletedNations);
}
applyRealtimeReadModelBaseline(readModelBaseline, changes);
worldReadModelBaseline = persisted.worldReadModelSignature;
@@ -105,6 +105,17 @@ export interface InMemoryReservedTurnStateSnapshot {
leasedNationKeys: string[];
}
export interface ReservedTurnQueueCounts {
generalQueues: number;
nationQueues: number;
dirtyGeneralQueues: number;
dirtyNationQueues: number;
pendingGeneralInitializations: number;
pendingNationInitializations: number;
leasedGeneralQueues: number;
leasedNationQueues: number;
}
export class ReservedTurnLeaseConflictError extends Error {
constructor(readonly queueKey: string) {
super(`Reserved turn queue lease conflict: ${queueKey}.`);
@@ -189,6 +200,64 @@ export class InMemoryReservedTurnStore {
return this.captureState();
}
getQueueCounts(): ReservedTurnQueueCounts {
return {
generalQueues: this.generalTurns.size,
nationQueues: this.nationTurns.size,
dirtyGeneralQueues: this.dirtyGeneralIds.size,
dirtyNationQueues: this.dirtyNationKeys.size,
pendingGeneralInitializations: this.pendingGeneralInitializationIds.size,
pendingNationInitializations: this.pendingNationInitializationKeys.size,
leasedGeneralQueues: this.leasedGeneralIds.size,
leasedNationQueues: this.leasedNationKeys.size,
};
}
/**
* Drops queues whose owning rows were deleted by a successful world flush.
* The daemon calls this inside EngineStateManager.transaction(), so a later
* failure still restores these maps and journals from the transaction savepoint.
*/
pruneDeletedEntityQueues(
generalIds: readonly number[],
nationIds: readonly number[]
): { generalQueues: number; nationQueues: number } {
let generalQueues = 0;
for (const generalId of new Set(generalIds)) {
if (this.generalTurns.delete(generalId)) {
generalQueues += 1;
}
this.dirtyGeneralIds.delete(generalId);
this.pendingGeneralInitializationIds.delete(generalId);
this.leasedGeneralIds.delete(generalId);
}
const deletedNations = new Set(nationIds);
let nationQueues = 0;
const pruneNationKeys = (keys: Iterable<string>, remove: (key: string) => boolean | void): void => {
for (const key of keys) {
const nationId = Number(key.split(':', 1)[0]);
if (deletedNations.has(nationId) && remove(key) !== false) {
nationQueues += 1;
}
}
};
pruneNationKeys(Array.from(this.nationTurns.keys()), (key) => this.nationTurns.delete(key));
for (const keys of [
this.dirtyNationKeys,
this.pendingNationInitializationKeys,
this.leasedNationKeys,
]) {
for (const key of Array.from(keys)) {
const nationId = Number(key.split(':', 1)[0]);
if (deletedNations.has(nationId)) {
keys.delete(key);
}
}
}
return { generalQueues, nationQueues };
}
inspectGeneralTurnActivity(): Array<[number, boolean]> {
return Array.from(this.generalTurns, ([generalId, turns]) => [
generalId,
+3 -1
View File
@@ -41,7 +41,7 @@ import {
} from './monthlyNationStatsHandler.js';
import { createFrontStateHandler } from './frontStateHandler.js';
import { createReservedTurnHandler } from './reservedTurnHandler.js';
import { createReservedTurnStore } from './reservedTurnStore.js';
import { createReservedTurnStore, type InMemoryReservedTurnStore } from './reservedTurnStore.js';
import { createTurnDaemonCommandHandler } from './worldCommandHandler.js';
import { loadTurnCommandProfile } from './turnCommandProfile.js';
import { loadTurnWorldFromDatabase } from './worldLoader.js';
@@ -132,6 +132,7 @@ export interface TurnDaemonRuntime {
stateStore: InMemoryTurnStateStore;
stateManager: EngineStateManager;
processor: InMemoryTurnProcessor;
reservedTurns: InMemoryReservedTurnStore | null;
hooks?: TurnDaemonHooks;
close(): Promise<void>;
}
@@ -1012,6 +1013,7 @@ const createTurnDaemonRuntimeWithLease = async (
stateStore,
stateManager,
processor,
reservedTurns: reservedTurnStoreHandle?.store ?? null,
hooks,
close,
};
@@ -8,6 +8,8 @@ export interface TurnDaemonMemoryContext {
nations: number;
troops: number;
events: number;
generalTurnQueues?: number;
nationTurnQueues?: number;
lifecycleState: string;
}
@@ -48,6 +50,10 @@ export const buildTurnDaemonMemoryReport = (
`nations=${context.nations}`,
`troops=${context.troops}`,
`events=${context.events}`,
...(context.generalTurnQueues === undefined
? []
: [`generalTurnQueues=${context.generalTurnQueues}`]),
...(context.nationTurnQueues === undefined ? [] : [`nationTurnQueues=${context.nationTurnQueues}`]),
`lifecycle=${context.lifecycleState}`,
].join(' ');
return { message, warning: heapRatio >= HEAP_WARNING_RATIO };