perf(reversi): improve performance of reversi backend

This commit is contained in:
syuilo 2024-01-22 08:39:38 +09:00
parent 3ff229af6f
commit 1a01a85182

View file

@ -12,18 +12,14 @@ import { IsNull } from 'typeorm';
import type { import type {
MiReversiGame, MiReversiGame,
ReversiGamesRepository, ReversiGamesRepository,
UsersRepository,
} from '@/models/_.js'; } from '@/models/_.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Packed } from '@/misc/json-schema.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { Serialized } from '@/types.js'; import { Serialized } from '@/types.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
@ -58,7 +54,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis @bindThis
private async cacheGame(game: MiReversiGame) { private async cacheGame(game: MiReversiGame) {
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game)); await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 60, JSON.stringify(game));
} }
@bindThis @bindThis
@ -66,6 +62,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
await this.redisClient.del(`reversi:game:cache:${gameId}`); await this.redisClient.del(`reversi:game:cache:${gameId}`);
} }
@bindThis
private getBakeProps(game: MiReversiGame) {
return {
startedAt: game.startedAt,
endedAt: game.endedAt,
// ゲームの途中からユーザーが変わることは無いので
//user1Id: game.user1Id,
//user2Id: game.user2Id,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
black: game.black,
isStarted: game.isStarted,
isEnded: game.isEnded,
winnerId: game.winnerId,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
timeLimitForEachTurn: game.timeLimitForEachTurn,
logs: game.logs,
map: game.map,
bw: game.bw,
crc32: game.crc32,
} satisfies Partial<MiReversiGame>;
}
@bindThis @bindThis
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> { public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
if (targetUser.id === me.id) { if (targetUser.id === me.id) {
@ -204,14 +227,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
let isBothReady = false; let isBothReady = false;
if (game.user1Id === user.id) { if (game.user1Id === user.id) {
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
user1Ready: ready, user1Ready: ready,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
@ -221,14 +240,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (ready && updatedGame.user2Ready) isBothReady = true; if (ready && updatedGame.user2Ready) isBothReady = true;
} else if (game.user2Id === user.id) { } else if (game.user2Id === user.id) {
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
user2Ready: ready, user2Ready: ready,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', { this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
@ -262,22 +277,15 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw = parseInt(game.bw, 10); bw = parseInt(game.bw, 10);
} }
function getRandomMap() {
const mapCount = Object.entries(Reversi.maps).length;
const rnd = Math.floor(Math.random() * mapCount);
return Object.values(Reversi.maps)[rnd].data;
}
const map = game.map != null ? game.map : getRandomMap();
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
.set({ .set({
...this.getBakeProps(game),
startedAt: new Date(), startedAt: new Date(),
isStarted: true, isStarted: true,
black: bw, black: bw,
map: map, map: game.map,
crc32, crc32,
}) })
.where('id = :id', { id: game.id }) .where('id = :id', { id: game.id })
@ -287,38 +295,23 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
const engine = new Reversi.Game(map, { const engine = new Reversi.Game(updatedGame.map, {
isLlotheo: game.isLlotheo, isLlotheo: updatedGame.isLlotheo,
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: updatedGame.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: updatedGame.loopedBoard,
}); });
if (engine.isEnded) { if (engine.isEnded) {
let winner; let winnerId;
if (engine.winner === true) { if (engine.winner === true) {
winner = bw === 1 ? game.user1Id : game.user2Id; winnerId = bw === 1 ? updatedGame.user1Id : updatedGame.user2Id;
} else if (engine.winner === false) { } else if (engine.winner === false) {
winner = bw === 1 ? game.user2Id : game.user1Id; winnerId = bw === 1 ? updatedGame.user2Id : updatedGame.user1Id;
} else { } else {
winner = null; winnerId = null;
} }
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() await this.endGame(updatedGame, winnerId, null);
.set({
isEnded: true,
endedAt: new Date(),
winnerId: winner,
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winner,
game: await this.reversiGameEntityService.packDetail(game.id),
});
return; return;
} }
@ -327,7 +320,30 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, ''); this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
this.globalEventService.publishReversiGameStream(game.id, 'started', { this.globalEventService.publishReversiGameStream(game.id, 'started', {
game: await this.reversiGameEntityService.packDetail(game.id), game: await this.reversiGameEntityService.packDetail(updatedGame),
});
}
@bindThis
private async endGame(game: MiReversiGame, winnerId: MiUser['id'] | null, reason: 'surrender' | 'timeout' | null) {
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
.set({
...this.getBakeProps(game),
isEnded: true,
endedAt: new Date(),
winnerId: winnerId,
surrenderedUserId: reason === 'surrender' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
timeoutUserId: reason === 'timeout' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winnerId,
game: await this.reversiGameEntityService.packDetail(updatedGame),
}); });
} }
@ -354,14 +370,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
// TODO: より厳格なバリデーション // TODO: より厳格なバリデーション
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
[key]: value, [key]: value,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', { this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
@ -397,17 +409,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
engine.putStone(pos); engine.putStone(pos);
let winner;
if (engine.isEnded) {
if (engine.winner === true) {
winner = game.black === 1 ? game.user1Id : game.user2Id;
} else if (engine.winner === false) {
winner = game.black === 1 ? game.user2Id : game.user1Id;
} else {
winner = null;
}
}
const logs = Reversi.Serializer.deserializeLogs(game.logs); const logs = Reversi.Serializer.deserializeLogs(game.logs);
const log = { const log = {
@ -423,17 +424,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() const updatedGame = {
.set({ ...game,
crc32, crc32,
isEnded: engine.isEnded,
winnerId: winner,
logs: serializeLogs, logs: serializeLogs,
}) };
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame); this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'log', { this.globalEventService.publishReversiGameStream(game.id, 'log', {
@ -442,10 +437,16 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}); });
if (engine.isEnded) { if (engine.isEnded) {
this.globalEventService.publishReversiGameStream(game.id, 'ended', { let winnerId;
winnerId: winner ?? null, if (engine.winner === true) {
game: await this.reversiGameEntityService.packDetail(game.id), winnerId = game.black === 1 ? game.user1Id : game.user2Id;
}); } else if (engine.winner === false) {
winnerId = game.black === 1 ? game.user2Id : game.user1Id;
} else {
winnerId = null;
}
await this.endGame(updatedGame, winnerId, null);
} else { } else {
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, ''); this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, '');
} }
@ -460,23 +461,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() await this.endGame(game, winnerId, 'surrender');
.set({
isEnded: true,
endedAt: new Date(),
winnerId: winnerId,
surrenderedUserId: user.id,
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winnerId,
game: await this.reversiGameEntityService.packDetail(game.id),
});
} }
@bindThis @bindThis
@ -500,23 +485,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if (timer === 0) { if (timer === 0) {
const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id); const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id);
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() await this.endGame(game, winnerId, 'timeout');
.set({
isEnded: true,
endedAt: new Date(),
winnerId: winnerId,
timeoutUserId: engine.turn ? (game.black === 1 ? game.user1Id : game.user2Id) : (game.black === 1 ? game.user2Id : game.user1Id),
})
.where('id = :id', { id: game.id })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.cacheGame(updatedGame);
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
winnerId: winnerId,
game: await this.reversiGameEntityService.packDetail(game.id),
});
} }
} }