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
@@ -753,6 +753,21 @@ export const createReservedTurnHandler = async (options: {
blockedReason?: string;
aiState?: ReturnType<GeneralAI['getDebugState']>;
}) => void;
onActionProfiled?: (payload: {
kind: 'nation' | 'general';
generalId: number;
nationId: number | null;
officerLevel: number;
npcState: number;
year: number;
month: number;
requestedAction: string;
actionKey: string;
usedFallback: boolean;
usedAi: boolean;
aiDecisionDurationNs: bigint;
actionDurationNs: bigint;
}) => void;
}): Promise<GeneralTurnHandler> => {
const env = options.commandEnv ?? buildCommandEnv(options.scenarioConfig, options.unitSet);
const itemRegistry = createItemModuleRegistry(await loadItemModules([...ITEM_KEYS]));
@@ -1627,7 +1642,11 @@ export const createReservedTurnHandler = async (options: {
hasReservedTurn = true;
}
let nationAiState: ReturnType<GeneralAI['getDebugState']> | undefined;
let nationAiDecisionDurationNs = 0n;
let nationUsedAi = false;
if (worldView && shouldUseAi(currentGeneral, context.world)) {
nationUsedAi = true;
const aiStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n;
sharedAi = new GeneralAI({
general: currentGeneral,
city: currentCity,
@@ -1647,6 +1666,9 @@ export const createReservedTurnHandler = async (options: {
});
const ai = sharedAi;
const candidate = ai.chooseNationTurn(nationCommand);
if (options.onActionProfiled) {
nationAiDecisionDurationNs = process.hrtime.bigint() - aiStartedAt;
}
if (candidate) {
if (
(process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []).includes(
@@ -1690,7 +1712,11 @@ export const createReservedTurnHandler = async (options: {
}
nationAiState = ai.getDebugState();
}
const nationActionStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n;
const nationResult = runAction('nation', nationDefinitions, nationFallback, nationCommand, false);
const nationActionDurationNs = options.onActionProfiled
? process.hrtime.bigint() - nationActionStartedAt
: 0n;
// Ref persists the nation command here, but LazyVarUpdater only
// clears its dirty flags: it does not replace the same PHP
// General object's fractional values with the MariaDB INT row.
@@ -1722,6 +1748,21 @@ export const createReservedTurnHandler = async (options: {
...(nationResult.blockedReason ? { blockedReason: nationResult.blockedReason } : {}),
...(nationAiState ? { aiState: nationAiState } : {}),
});
options.onActionProfiled?.({
kind: 'nation',
generalId: currentGeneral.id,
nationId: currentNation?.id ?? null,
officerLevel: currentGeneral.officerLevel,
npcState: currentGeneral.npcState,
year: context.world.currentYear,
month: context.world.currentMonth,
requestedAction: nationCommand.action,
actionKey: nationResult.actionKey,
usedFallback: nationResult.usedFallback,
usedAi: nationUsedAi,
aiDecisionDurationNs: nationAiDecisionDurationNs,
actionDurationNs: nationActionDurationNs,
});
options.reservedTurns.shiftNationTurns(currentNation.id, currentGeneral.officerLevel, -1);
}
if (isBlocked && currentNation && currentGeneral.officerLevel >= 5) {
@@ -1745,7 +1786,11 @@ export const createReservedTurnHandler = async (options: {
}
let generalAiState: ReturnType<GeneralAI['getDebugState']> | undefined;
let generalAutorunMode = false;
let generalAiDecisionDurationNs = 0n;
let generalUsedAi = false;
if (!isBlocked && worldView && shouldUseAi(currentGeneral, context.world)) {
generalUsedAi = true;
const aiStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n;
const ai =
sharedAi ??
new GeneralAI({
@@ -1766,6 +1811,9 @@ export const createReservedTurnHandler = async (options: {
nationFallback,
});
const candidate = ai.chooseGeneralTurn(generalCommand);
if (options.onActionProfiled) {
generalAiDecisionDurationNs = process.hrtime.bigint() - aiStartedAt;
}
// Ref GeneralAI::calcDiplomacyState writes
// nation_env.last_attackable for ordinary generals too. The
// nation-turn path consumes this patch above, but most NPCs
@@ -1818,6 +1866,7 @@ export const createReservedTurnHandler = async (options: {
}
generalAiState = ai.getDebugState();
}
const generalActionStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n;
const generalResult = isBlocked
? {
actionKey: DEFAULT_ACTION,
@@ -1826,6 +1875,9 @@ export const createReservedTurnHandler = async (options: {
blockedReason: '블럭 대상자입니다.',
}
: runAction('general', generalDefinitions, generalFallback, generalCommand, true);
const generalActionDurationNs = options.onActionProfiled
? process.hrtime.bigint() - generalActionStartedAt
: 0n;
options.onActionResolved?.({
kind: 'general',
generalId: currentGeneral.id,
@@ -1837,6 +1889,21 @@ export const createReservedTurnHandler = async (options: {
...(generalResult.blockedReason ? { blockedReason: generalResult.blockedReason } : {}),
...(generalAiState ? { aiState: generalAiState } : {}),
});
options.onActionProfiled?.({
kind: 'general',
generalId: currentGeneral.id,
nationId: currentNation?.id ?? null,
officerLevel: currentGeneral.officerLevel,
npcState: currentGeneral.npcState,
year: context.world.currentYear,
month: context.world.currentMonth,
requestedAction: generalCommand.action,
actionKey: generalResult.actionKey,
usedFallback: generalResult.usedFallback,
usedAi: generalUsedAi,
aiDecisionDurationNs: generalAiDecisionDurationNs,
actionDurationNs: generalActionDurationNs,
});
let nextTurnAt = 'nextTurnAt' in generalResult ? generalResult.nextTurnAt : undefined;
options.reservedTurns.shiftGeneralTurns(currentGeneral.id, -1);