test(load): 1분 페이지 동시 부하 계측을 추가한다

국가 권한 사용자 10명과 800 NPC fixture를 만들고 Chromium 화면 순환, SSE, wall-clock 턴을 같은 시각에 측정한다.
This commit is contained in:
2026-08-21 23:17:24 +00:00
parent 7066d13c5b
commit e67a40e51d
8 changed files with 601 additions and 15 deletions
+61 -2
View File
@@ -182,9 +182,24 @@ const migrateDedicatedSchema = async (workspaceRoot: string, databaseUrl: string
type GeneralRow = Awaited<ReturnType<GamePrismaClient['general']['findMany']>>[number];
const PRIVILEGED_VIEWER_LEVELS = [12, 10, 8, 6, 11, 9, 7, 5] as const;
export const privilegedViewerPlacement = (index: number, nationId: number, cityId: number) => ({
nationId,
cityId,
officerLevel: PRIVILEGED_VIEWER_LEVELS[index % PRIVILEGED_VIEWER_LEVELS.length]!,
});
const cloneGeneral = (
source: GeneralRow,
input: { id: number; userId: string | null; npcState: number }
input: {
id: number;
userId: string | null;
npcState: number;
nationId?: number;
cityId?: number;
officerLevel?: number;
}
): GamePrisma.GeneralCreateManyInput =>
({
...source,
@@ -192,6 +207,9 @@ const cloneGeneral = (
name: `${source.name}#L${input.id}`,
userId: input.userId,
npcState: input.npcState,
nationId: input.nationId ?? source.nationId,
cityId: input.cityId ?? source.cityId,
officerLevel: input.officerLevel ?? source.officerLevel,
turnTime: new Date(source.turnTime),
recentWarTime: source.recentWarTime ? new Date(source.recentWarTime) : null,
createdAt: FIXED_NOW,
@@ -237,8 +255,17 @@ const projectFixtureState = async (db: GamePrismaClient) => {
};
const resizeSeededGenerals = async (db: GamePrismaClient, config: LoadConfig): Promise<void> => {
const source = await db.general.findMany({ orderBy: { id: 'asc' } });
const [source, nations, cities] = await Promise.all([
db.general.findMany({ orderBy: { id: 'asc' } }),
db.nation.findMany({ orderBy: { id: 'asc' } }),
db.city.findMany({ orderBy: { id: 'asc' } }),
]);
if (source.length === 0) throw new Error('scenario seed produced no generals');
const existingViewerNation = nations.find((nation) => nation.id > 0);
const viewerCity =
(existingViewerNation ? cities.find((city) => city.nationId === existingViewerNation.id) : null) ?? cities[0];
if (!viewerCity) throw new Error('scenario seed produced no city for privileged page load');
const viewerNationId = existingViewerNation?.id ?? Math.max(...nations.map((nation) => nation.id), 0) + 1;
const expectedNpc = config.capacity.npcGenerals;
const expectedHuman = config.capacity.humanGenerals;
if (expectedHuman !== config.capacity.authenticatedViewers) {
@@ -246,20 +273,43 @@ const resizeSeededGenerals = async (db: GamePrismaClient, config: LoadConfig): P
}
await db.$transaction(async (transaction) => {
await transaction.general.deleteMany();
if (!existingViewerNation) {
await transaction.nation.create({
data: {
id: viewerNationId,
name: '부하측정국',
color: '#334466',
capitalCityId: viewerCity.id,
gold: 100_000,
rice: 100_000,
tech: 1_000,
level: 1,
typeCode: 'che_중립',
meta: { cityIds: [viewerCity.id], infoText: null, secretlimit: 3 },
},
});
}
await transaction.city.update({ where: { id: viewerCity.id }, data: { nationId: viewerNationId } });
const rows: GamePrisma.GeneralCreateManyInput[] = [];
for (let index = 0; index < expectedNpc; index += 1) {
rows.push(cloneGeneral(source[index % source.length]!, { id: index + 1, userId: null, npcState: 2 }));
}
for (let index = 0; index < expectedHuman; index += 1) {
const placement = privilegedViewerPlacement(index, viewerNationId, viewerCity.id);
rows.push(
cloneGeneral(source[(expectedNpc + index) % source.length]!, {
id: expectedNpc + index + 1,
userId: `load-user-${String(index + 1).padStart(4, '0')}`,
npcState: 0,
...placement,
})
);
}
await transaction.general.createMany({ data: rows });
await transaction.nation.update({
where: { id: viewerNationId },
data: { chiefGeneralId: expectedNpc + 1 },
});
const world = await transaction.worldState.findFirstOrThrow({ select: { id: true, meta: true, config: true } });
await transaction.worldState.update({
where: { id: world.id },
@@ -435,10 +485,17 @@ export const verifyCapacityFixture = async (config: LoadConfig, env: NodeJS.Proc
const redisVersion = /^redis_version:(.+)$/mu.exec(redisInfo)?.[1]?.trim() ?? 'unknown';
const npcGenerals = state.generals.filter((general) => general.npcState >= 2).length;
const humanGenerals = state.generals.filter((general) => general.npcState === 0 && general.userId).length;
const privilegedHumanGenerals = state.generals.filter(
(general) =>
general.npcState === 0 && general.userId && general.nationId > 0 && general.officerLevel >= 5
);
const viewerNationIds = [...new Set(privilegedHumanGenerals.map((general) => general.nationId))];
const valid =
state.generals.length === config.capacity.npcGenerals + config.capacity.humanGenerals &&
npcGenerals === config.capacity.npcGenerals &&
humanGenerals === config.capacity.humanGenerals &&
privilegedHumanGenerals.length === config.capacity.humanGenerals &&
viewerNationIds.length === 1 &&
accessTokens === config.capacity.authenticatedViewers &&
manifestFixtureSha256 === fixtureSha256;
return {
@@ -447,6 +504,8 @@ export const verifyCapacityFixture = async (config: LoadConfig, env: NodeJS.Proc
generals: state.generals.length,
npcGenerals,
humanGenerals,
privilegedHumanGenerals: privilegedHumanGenerals.length,
viewerNations: viewerNationIds.length,
accessTokens,
redisManifestPresent: rawManifest !== null,
redisManifestMatches: manifestFixtureSha256 === fixtureSha256,