perf(reversi): improve performance of reversi backend

This commit is contained in:
syuilo 2024-01-22 15:41:29 +09:00
parent 259992c65f
commit 94e282b612
12 changed files with 73 additions and 55 deletions

View file

@ -234,10 +234,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
map: Reversi.maps.eighteight.data, map: Reversi.maps.eighteight.data,
bw: 'random', bw: 'random',
isLlotheo: false, 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); 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 }); this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed });
return game; return game;
@ -267,6 +270,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
.returning('*') .returning('*')
.execute() .execute()
.then((response) => response.raw[0]); .then((response) => response.raw[0]);
// キャッシュ効率化のためにユーザー情報は再利用
updatedGame.user1 = game.user1;
updatedGame.user2 = game.user2;
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
@ -314,6 +320,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
.returning('*') .returning('*')
.execute() .execute()
.then((response) => response.raw[0]); .then((response) => response.raw[0]);
// キャッシュ効率化のためにユーザー情報は再利用
updatedGame.user1 = game.user1;
updatedGame.user2 = game.user2;
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', { 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> { public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
const cached = await this.redisClient.get(`reversi:game:cache:${id}`); const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
if (cached != null) { if (cached != null) {
// TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>; const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
return { return {
...parsed, ...parsed,
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null, startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : 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 { } else {
const game = await this.reversiGamesRepository.findOneBy({ id }); const game = await this.reversiGamesRepository.findOne({
where: { id },
relations: ['user1', 'user2'],
});
if (game == null) return null; if (game == null) return null;
this.cacheGame(game); this.cacheGame(game);

View file

@ -9,7 +9,6 @@ import type { ReversiGamesRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js'; import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiReversiGame } from '@/models/ReversiGame.js'; import type { MiReversiGame } from '@/models/ReversiGame.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -29,10 +28,14 @@ export class ReversiGameEntityService {
@bindThis @bindThis
public async packDetail( public async packDetail(
src: MiReversiGame['id'] | MiReversiGame, src: MiReversiGame['id'] | MiReversiGame,
me?: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'ReversiGameDetailed'>> { ): Promise<Packed<'ReversiGameDetailed'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); 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({ return await awaitAll({
id: game.id, id: game.id,
createdAt: this.idService.parse(game.id).date.toISOString(), createdAt: this.idService.parse(game.id).date.toISOString(),
@ -46,10 +49,10 @@ export class ReversiGameEntityService {
user2Ready: game.user2Ready, user2Ready: game.user2Ready,
user1Id: game.user1Id, user1Id: game.user1Id,
user2Id: game.user2Id, user2Id: game.user2Id,
user1: this.userEntityService.pack(game.user1Id, me), user1: users[0],
user2: this.userEntityService.pack(game.user2Id, me), user2: users[1],
winnerId: game.winnerId, 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, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
@ -66,18 +69,21 @@ export class ReversiGameEntityService {
@bindThis @bindThis
public packDetailMany( public packDetailMany(
xs: MiReversiGame[], 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 @bindThis
public async packLite( public async packLite(
src: MiReversiGame['id'] | MiReversiGame, src: MiReversiGame['id'] | MiReversiGame,
me?: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'ReversiGameLite'>> { ): Promise<Packed<'ReversiGameLite'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); 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({ return await awaitAll({
id: game.id, id: game.id,
createdAt: this.idService.parse(game.id).date.toISOString(), createdAt: this.idService.parse(game.id).date.toISOString(),
@ -85,16 +91,12 @@ export class ReversiGameEntityService {
endedAt: game.endedAt && game.endedAt.toISOString(), endedAt: game.endedAt && game.endedAt.toISOString(),
isStarted: game.isStarted, isStarted: game.isStarted,
isEnded: game.isEnded, isEnded: game.isEnded,
form1: game.form1,
form2: game.form2,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
user1Id: game.user1Id, user1Id: game.user1Id,
user2Id: game.user2Id, user2Id: game.user2Id,
user1: this.userEntityService.pack(game.user1Id, me), user1: users[0],
user2: this.userEntityService.pack(game.user2Id, me), user2: users[1],
winnerId: game.winnerId, 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, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
@ -109,9 +111,8 @@ export class ReversiGameEntityService {
@bindThis @bindThis
public packLiteMany( public packLiteMany(
xs: MiReversiGame[], 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)));
} }
} }

View file

@ -34,22 +34,6 @@ export const packedReversiGameLiteSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, 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: { user1Id: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -43,7 +43,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) 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) { if (ps.my && me) {
query.andWhere(new Brackets(qb => { 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(); const games = await query.take(ps.limit).getMany();
return await this.reversiGameEntityService.packLiteMany(games, me); return await this.reversiGameEntityService.packLiteMany(games);
}); });
} }
} }

View file

@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (game == null) return; if (game == null) return;
return await this.reversiGameEntityService.packDetail(game, me); return await this.reversiGameEntityService.packDetail(game);
}); });
} }
} }

View file

@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchGame); throw new ApiError(meta.errors.noSuchGame);
} }
return await this.reversiGameEntityService.packDetail(game, me); return await this.reversiGameEntityService.packDetail(game);
}); });
} }
} }

View file

@ -277,7 +277,11 @@ export type Serialized<T> = {
? (string | null) ? (string | null)
: T[K] extends Record<string, any> : T[K] extends Record<string, any>
? Serialized<T[K]> ? 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< export type FilterUnionByProperty<

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.332Z * generatedAt: 2024-01-22T06:08:45.879Z
*/ */
import type { SwitchCaseResponseType } from '../api.js'; import type { SwitchCaseResponseType } from '../api.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.330Z * generatedAt: 2024-01-22T06:08:45.877Z
*/ */
import type { import type {

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.328Z * generatedAt: 2024-01-22T06:08:45.876Z
*/ */
import { operations } from './types.js'; import { operations } from './types.js';

View file

@ -1,6 +1,6 @@
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.327Z * generatedAt: 2024-01-22T06:08:45.875Z
*/ */
import { components } from './types.js'; import { components } from './types.js';

View file

@ -2,8 +2,8 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */ /* eslint @typescript-eslint/no-explicit-any: 0 */
/* /*
* version: 2023.12.2 * version: 2024.2.0-beta.2
* generatedAt: 2024-01-21T01:01:12.246Z * generatedAt: 2024-01-22T06:08:45.796Z
*/ */
/** /**
@ -4469,10 +4469,6 @@ export type components = {
endedAt: string | null; endedAt: string | null;
isStarted: boolean; isStarted: boolean;
isEnded: boolean; isEnded: boolean;
form1: Record<string, never> | null;
form2: Record<string, never> | null;
user1Ready: boolean;
user2Ready: boolean;
/** Format: id */ /** Format: id */
user1Id: string; user1Id: string;
/** Format: id */ /** Format: id */