From 94e282b612ad3dc6fd336a82fff19b290e11d221 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 22 Jan 2024 15:41:29 +0900
Subject: [PATCH] perf(reversi): improve performance of reversi backend

---
 packages/backend/src/core/ReversiService.ts   | 37 +++++++++++++++++--
 .../core/entities/ReversiGameEntityService.ts | 35 +++++++++---------
 .../src/models/json-schema/reversi-game.ts    | 16 --------
 .../src/server/api/endpoints/reversi/games.ts |  6 ++-
 .../src/server/api/endpoints/reversi/match.ts |  2 +-
 .../server/api/endpoints/reversi/show-game.ts |  2 +-
 packages/backend/src/types.ts                 |  6 ++-
 .../misskey-js/src/autogen/apiClientJSDoc.ts  |  4 +-
 packages/misskey-js/src/autogen/endpoint.ts   |  4 +-
 packages/misskey-js/src/autogen/entities.ts   |  4 +-
 packages/misskey-js/src/autogen/models.ts     |  4 +-
 packages/misskey-js/src/autogen/types.ts      |  8 +---
 12 files changed, 73 insertions(+), 55 deletions(-)

diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 0e59d0308d..0d5f989c11 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -234,10 +234,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 			map: Reversi.maps.eighteight.data,
 			bw: 'random',
 			isLlotheo: false,
-		}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
+		}).then(x => this.reversiGamesRepository.findOneOrFail({
+			where: { id: x.identifiers[0].id },
+			relations: ['user1', 'user2'],
+		}));
 		this.cacheGame(game);
 
-		const packed = await this.reversiGameEntityService.packDetail(game, { id: parentId });
+		const packed = await this.reversiGameEntityService.packDetail(game);
 		this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed });
 
 		return game;
@@ -267,6 +270,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 			.returning('*')
 			.execute()
 			.then((response) => response.raw[0]);
+		// キャッシュ効率化のためにユーザー情報は再利用
+		updatedGame.user1 = game.user1;
+		updatedGame.user2 = game.user2;
 		this.cacheGame(updatedGame);
 
 		//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
@@ -314,6 +320,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 			.returning('*')
 			.execute()
 			.then((response) => response.raw[0]);
+		// キャッシュ効率化のためにユーザー情報は再利用
+		updatedGame.user1 = game.user1;
+		updatedGame.user2 = game.user2;
 		this.cacheGame(updatedGame);
 
 		this.globalEventService.publishReversiGameStream(game.id, 'ended', {
@@ -483,14 +492,36 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
 	public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
 		const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
 		if (cached != null) {
+			// TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい
 			const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
 			return {
 				...parsed,
 				startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
 				endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
+				user1: parsed.user1 != null ? {
+					...parsed.user1,
+					avatar: null,
+					banner: null,
+					updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null,
+					lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null,
+					lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null,
+					movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null,
+				} : null,
+				user2: parsed.user2 != null ? {
+					...parsed.user2,
+					avatar: null,
+					banner: null,
+					updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null,
+					lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null,
+					lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null,
+					movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null,
+				} : null,
 			};
 		} else {
-			const game = await this.reversiGamesRepository.findOneBy({ id });
+			const game = await this.reversiGamesRepository.findOne({
+				where: { id },
+				relations: ['user1', 'user2'],
+			});
 			if (game == null) return null;
 
 			this.cacheGame(game);
diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts
index bcb0fd5a6f..6c89a70599 100644
--- a/packages/backend/src/core/entities/ReversiGameEntityService.ts
+++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts
@@ -9,7 +9,6 @@ import type { ReversiGamesRepository } from '@/models/_.js';
 import { awaitAll } from '@/misc/prelude/await-all.js';
 import type { Packed } from '@/misc/json-schema.js';
 import type { } from '@/models/Blocking.js';
-import type { MiUser } from '@/models/User.js';
 import type { MiReversiGame } from '@/models/ReversiGame.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
@@ -29,10 +28,14 @@ export class ReversiGameEntityService {
 	@bindThis
 	public async packDetail(
 		src: MiReversiGame['id'] | MiReversiGame,
-		me?: { id: MiUser['id'] } | null | undefined,
 	): Promise<Packed<'ReversiGameDetailed'>> {
 		const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
 
+		const users = await Promise.all([
+			this.userEntityService.pack(game.user1 ?? game.user1Id),
+			this.userEntityService.pack(game.user2 ?? game.user2Id),
+		]);
+
 		return await awaitAll({
 			id: game.id,
 			createdAt: this.idService.parse(game.id).date.toISOString(),
@@ -46,10 +49,10 @@ export class ReversiGameEntityService {
 			user2Ready: game.user2Ready,
 			user1Id: game.user1Id,
 			user2Id: game.user2Id,
-			user1: this.userEntityService.pack(game.user1Id, me),
-			user2: this.userEntityService.pack(game.user2Id, me),
+			user1: users[0],
+			user2: users[1],
 			winnerId: game.winnerId,
-			winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
+			winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
 			surrenderedUserId: game.surrenderedUserId,
 			timeoutUserId: game.timeoutUserId,
 			black: game.black,
@@ -66,18 +69,21 @@ export class ReversiGameEntityService {
 	@bindThis
 	public packDetailMany(
 		xs: MiReversiGame[],
-		me?: { id: MiUser['id'] } | null | undefined,
 	) {
-		return Promise.all(xs.map(x => this.packDetail(x, me)));
+		return Promise.all(xs.map(x => this.packDetail(x)));
 	}
 
 	@bindThis
 	public async packLite(
 		src: MiReversiGame['id'] | MiReversiGame,
-		me?: { id: MiUser['id'] } | null | undefined,
 	): Promise<Packed<'ReversiGameLite'>> {
 		const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
 
+		const users = await Promise.all([
+			this.userEntityService.pack(game.user1 ?? game.user1Id),
+			this.userEntityService.pack(game.user2 ?? game.user2Id),
+		]);
+
 		return await awaitAll({
 			id: game.id,
 			createdAt: this.idService.parse(game.id).date.toISOString(),
@@ -85,16 +91,12 @@ export class ReversiGameEntityService {
 			endedAt: game.endedAt && game.endedAt.toISOString(),
 			isStarted: game.isStarted,
 			isEnded: game.isEnded,
-			form1: game.form1,
-			form2: game.form2,
-			user1Ready: game.user1Ready,
-			user2Ready: game.user2Ready,
 			user1Id: game.user1Id,
 			user2Id: game.user2Id,
-			user1: this.userEntityService.pack(game.user1Id, me),
-			user2: this.userEntityService.pack(game.user2Id, me),
+			user1: users[0],
+			user2: users[1],
 			winnerId: game.winnerId,
-			winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
+			winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
 			surrenderedUserId: game.surrenderedUserId,
 			timeoutUserId: game.timeoutUserId,
 			black: game.black,
@@ -109,9 +111,8 @@ export class ReversiGameEntityService {
 	@bindThis
 	public packLiteMany(
 		xs: MiReversiGame[],
-		me?: { id: MiUser['id'] } | null | undefined,
 	) {
-		return Promise.all(xs.map(x => this.packLite(x, me)));
+		return Promise.all(xs.map(x => this.packLite(x)));
 	}
 }
 
diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts
index 4ac4d165d8..8061d84ad6 100644
--- a/packages/backend/src/models/json-schema/reversi-game.ts
+++ b/packages/backend/src/models/json-schema/reversi-game.ts
@@ -34,22 +34,6 @@ export const packedReversiGameLiteSchema = {
 			type: 'boolean',
 			optional: false, nullable: false,
 		},
-		form1: {
-			type: 'any',
-			optional: false, nullable: true,
-		},
-		form2: {
-			type: 'any',
-			optional: false, nullable: true,
-		},
-		user1Ready: {
-			type: 'boolean',
-			optional: false, nullable: false,
-		},
-		user2Ready: {
-			type: 'boolean',
-			optional: false, nullable: false,
-		},
 		user1Id: {
 			type: 'string',
 			optional: false, nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts
index 5322cd0987..f28fe5d987 100644
--- a/packages/backend/src/server/api/endpoints/reversi/games.ts
+++ b/packages/backend/src/server/api/endpoints/reversi/games.ts
@@ -43,7 +43,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
-				.andWhere('game.isStarted = TRUE');
+				.andWhere('game.isStarted = TRUE')
+				.innerJoinAndSelect('game.user1', 'user1')
+				.innerJoinAndSelect('game.user2', 'user2');
 
 			if (ps.my && me) {
 				query.andWhere(new Brackets(qb => {
@@ -55,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const games = await query.take(ps.limit).getMany();
 
-			return await this.reversiGameEntityService.packLiteMany(games, me);
+			return await this.reversiGameEntityService.packLiteMany(games);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts
index da5a3409ef..1065ce5a89 100644
--- a/packages/backend/src/server/api/endpoints/reversi/match.ts
+++ b/packages/backend/src/server/api/endpoints/reversi/match.ts
@@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			if (game == null) return;
 
-			return await this.reversiGameEntityService.packDetail(game, me);
+			return await this.reversiGameEntityService.packDetail(game);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts
index de571053e1..86645ea4b4 100644
--- a/packages/backend/src/server/api/endpoints/reversi/show-game.ts
+++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts
@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.noSuchGame);
 			}
 
-			return await this.reversiGameEntityService.packDetail(game, me);
+			return await this.reversiGameEntityService.packDetail(game);
 		});
 	}
 }
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 361a4931eb..cfac5cd9d4 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -277,7 +277,11 @@ export type Serialized<T> = {
 				? (string | null)
 				: T[K] extends Record<string, any>
 					? Serialized<T[K]>
-					: T[K];
+					: T[K] extends (Record<string, any> | null)
+					? (Serialized<T[K]> | null)
+						: T[K] extends (Record<string, any> | undefined)
+						? (Serialized<T[K]> | undefined)
+							: T[K];
 };
 
 export type FilterUnionByProperty<
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index ea41f2cb55..d81444e5df 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2023.12.2
- * generatedAt: 2024-01-21T01:01:12.332Z
+ * version: 2024.2.0-beta.2
+ * generatedAt: 2024-01-22T06:08:45.879Z
  */
 
 import type { SwitchCaseResponseType } from '../api.js';
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index f551053524..69f02b899f 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2023.12.2
- * generatedAt: 2024-01-21T01:01:12.330Z
+ * version: 2024.2.0-beta.2
+ * generatedAt: 2024-01-22T06:08:45.877Z
  */
 
 import type {
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index b0adbeaf93..5d46ea6611 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2023.12.2
- * generatedAt: 2024-01-21T01:01:12.328Z
+ * version: 2024.2.0-beta.2
+ * generatedAt: 2024-01-22T06:08:45.876Z
  */
 
 import { operations } from './types.js';
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 306f0cd6b4..3e795f2b86 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -1,6 +1,6 @@
 /*
- * version: 2023.12.2
- * generatedAt: 2024-01-21T01:01:12.327Z
+ * version: 2024.2.0-beta.2
+ * generatedAt: 2024-01-22T06:08:45.875Z
  */
 
 import { components } from './types.js';
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 5d2b6e2e3b..271ca41159 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -2,8 +2,8 @@
 /* eslint @typescript-eslint/no-explicit-any: 0 */
 
 /*
- * version: 2023.12.2
- * generatedAt: 2024-01-21T01:01:12.246Z
+ * version: 2024.2.0-beta.2
+ * generatedAt: 2024-01-22T06:08:45.796Z
  */
 
 /**
@@ -4469,10 +4469,6 @@ export type components = {
       endedAt: string | null;
       isStarted: boolean;
       isEnded: boolean;
-      form1: Record<string, never> | null;
-      form2: Record<string, never> | null;
-      user1Ready: boolean;
-      user2Ready: boolean;
       /** Format: id */
       user1Id: string;
       /** Format: id */