mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-06 19:27:58 -05:00
Optimize
This commit is contained in:
@@ -235,6 +235,21 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
|
||||
// ChatRepository
|
||||
add("ChatRepository.findAllRoomsByIds", fx.heavyChatUsers, (chatUsers) =>
|
||||
ChatRepository.findAllRoomsByIds(chatUsers.busiest.openRoomIds),
|
||||
);
|
||||
add(
|
||||
"ChatRepository.findAllOpenRoomIdsByUserId.busiest",
|
||||
fx.heavyChatUsers,
|
||||
(chatUsers) =>
|
||||
ChatRepository.findAllOpenRoomIdsByUserId(chatUsers.busiest.id),
|
||||
);
|
||||
add(
|
||||
"ChatRepository.findAllOpenRoomIdsByUserId.mostConnected",
|
||||
fx.heavyChatUsers,
|
||||
(chatUsers) =>
|
||||
ChatRepository.findAllOpenRoomIdsByUserId(chatUsers.mostConnectedId),
|
||||
);
|
||||
add("ChatRepository.findAllMessagesByRoomId", fx.heavyChatRoomId, (roomId) =>
|
||||
ChatRepository.findAllMessagesByRoomId(roomId),
|
||||
);
|
||||
@@ -243,17 +258,24 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
add(
|
||||
"ChatRepository.findMessageStatsByRoomIds",
|
||||
both(fx.heavyUser, fx.heavyChatRoomId),
|
||||
([user, roomId]) =>
|
||||
ChatRepository.findMessageStatsByRoomIds(user.id, [roomId]),
|
||||
fx.heavyChatUsers,
|
||||
(chatUsers) =>
|
||||
ChatRepository.findMessageStatsByRoomIds(
|
||||
chatUsers.busiest.id,
|
||||
chatUsers.busiest.openRoomIds,
|
||||
),
|
||||
);
|
||||
|
||||
// ChatRoomResolver
|
||||
add("ChatRoomResolver.resolve", fx.heavyChatRoomId, (roomId) =>
|
||||
ChatRoomResolver.resolve(roomId),
|
||||
// a tournament match room is the costliest to resolve: its owner join carries
|
||||
// the team members and the tournament's organizer permissions on top
|
||||
add(
|
||||
"ChatRoomResolver.resolve",
|
||||
fx.openChatRoomIdsByType?.TOURNAMENT_MATCH ?? null,
|
||||
(roomIds) => ChatRoomResolver.resolve(roomIds[0]),
|
||||
);
|
||||
add("ChatRoomResolver.findAllByUserId", fx.heavyUser, (user) =>
|
||||
ChatRoomResolver.findAllByUserId(user.id),
|
||||
add("ChatRoomResolver.findAllByUserId", fx.heavyChatUsers, (chatUsers) =>
|
||||
ChatRoomResolver.findAllByUserId(chatUsers.busiest.id),
|
||||
);
|
||||
|
||||
// FriendRepository
|
||||
@@ -568,6 +590,11 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
|
||||
// ScrimPostRepository
|
||||
add(
|
||||
"ScrimPostRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.SCRIM ?? null,
|
||||
(roomIds) => ScrimPostRepository.findAllByChatRoomIds(roomIds),
|
||||
);
|
||||
add("ScrimPostRepository.findById", fx.heavyScrimPostId, (scrimPostId) =>
|
||||
ScrimPostRepository.findById(scrimPostId),
|
||||
);
|
||||
@@ -661,6 +688,11 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
|
||||
// SQMatchRepository
|
||||
add(
|
||||
"SQMatchRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.SQ_MATCH ?? null,
|
||||
(roomIds) => SQMatchRepository.findAllByChatRoomIds(roomIds),
|
||||
);
|
||||
add("SQMatchRepository.findById", fx.heavyGroupMatchId, (matchId) =>
|
||||
SQMatchRepository.findById(matchId),
|
||||
);
|
||||
@@ -704,6 +736,11 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
|
||||
// SQGroupRepository
|
||||
add(
|
||||
"SQGroupRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.SQ_GROUP ?? null,
|
||||
(roomIds) => SQGroupRepository.findAllByChatRoomIds(roomIds),
|
||||
);
|
||||
add(
|
||||
"SQGroupRepository.findMapModePreferencesByGroupId",
|
||||
fx.heavyGroupIds,
|
||||
@@ -871,6 +908,11 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
|
||||
// TournamentMatchRepository
|
||||
add(
|
||||
"TournamentMatchRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.TOURNAMENT_MATCH ?? null,
|
||||
(roomIds) => TournamentMatchRepository.findAllByChatRoomIds(roomIds),
|
||||
);
|
||||
add(
|
||||
"TournamentMatchRepository.findMatchById",
|
||||
fx.heavyTournamentMatchId,
|
||||
@@ -1035,6 +1077,14 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
|
||||
// TournamentRepository
|
||||
add(
|
||||
"TournamentRepository.findOrganizerPermissionsByTournamentIds",
|
||||
fx.recentTournamentIds,
|
||||
(tournamentIds) =>
|
||||
TournamentRepository.findOrganizerPermissionsByTournamentIds(
|
||||
tournamentIds,
|
||||
),
|
||||
);
|
||||
add("TournamentRepository.findById", fx.heavyTournamentId, (tournamentId) =>
|
||||
TournamentRepository.findById(tournamentId),
|
||||
);
|
||||
@@ -1144,6 +1194,16 @@ export function buildCases(fx: Fixtures): {
|
||||
);
|
||||
|
||||
// TournamentTeamRepository
|
||||
add(
|
||||
"TournamentTeamRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.TOURNAMENT_TEAM ?? null,
|
||||
(roomIds) => TournamentTeamRepository.findAllByChatRoomIds(roomIds),
|
||||
);
|
||||
add(
|
||||
"TournamentTeamRepository.findAllMembersByTeamIds",
|
||||
fx.manyTournamentTeamIds,
|
||||
(teamIds) => TournamentTeamRepository.findAllMembersByTeamIds(teamIds),
|
||||
);
|
||||
add(
|
||||
"TournamentTeamRepository.findByInviteCode",
|
||||
fx.tournamentTeamInviteCode,
|
||||
|
||||
@@ -2,13 +2,15 @@ import { sub } from "date-fns";
|
||||
import { sql } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import * as ChatRepository from "~/features/chat/ChatRepository.server";
|
||||
import type { ChatRoomType } from "~/features/chat/chat-types";
|
||||
import type { SkillTeamIdentifier } from "~/features/mmr/mmr-utils";
|
||||
import type {
|
||||
MainWeaponId,
|
||||
ModeShort,
|
||||
StageId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
export interface Fixtures {
|
||||
@@ -41,10 +43,19 @@ export interface Fixtures {
|
||||
recentTournamentIds: number[] | null;
|
||||
heavyTeam: { id: number; customUrl: string; memberUserId: number } | null;
|
||||
heavyCalendarEventId: number | null;
|
||||
/** Chat room with the most messages. Null until the prod copy has post-migration chat data. */
|
||||
/** Chat room with the most messages. Null until the prod copy has chat data (`pnpm run bench:db:seed-chat`). */
|
||||
heavyChatRoomId: number | null;
|
||||
/** Newest chat message. Null until the prod copy has post-migration chat data. */
|
||||
/** Newest chat message. Null until the prod copy has chat data. */
|
||||
heavyChatMessageId: number | null;
|
||||
/** The two users the chat room list is heaviest for; the open-room lookup and the fan-out over the result peak on different ones. */
|
||||
heavyChatUsers: {
|
||||
/** In the most open rooms, so the room list fans out the widest. */
|
||||
busiest: { id: number; openRoomIds: number[] };
|
||||
/** The most membership rows, which is what the open-room lookup walks. */
|
||||
mostConnectedId: number;
|
||||
} | null;
|
||||
/** Open rooms per type, as many as one resolve pass batches. Types with no open room are left out. */
|
||||
openChatRoomIdsByType: Partial<Record<ChatRoomType, number[]>> | null;
|
||||
resultsEventId: number | null;
|
||||
calendarAuthorId: number | null;
|
||||
calendarWindow: { startTime: Date; endTime: Date } | null;
|
||||
@@ -76,6 +87,7 @@ export interface Fixtures {
|
||||
wins: { trophyId: number; userId: number };
|
||||
} | null;
|
||||
manyUserIds: number[] | null;
|
||||
manyTournamentTeamIds: number[] | null;
|
||||
notification: { userId: number; type: Tables["Notification"]["type"] } | null;
|
||||
heavyAssociation: {
|
||||
id: number;
|
||||
@@ -156,6 +168,8 @@ export async function resolveFixtures(): Promise<Fixtures> {
|
||||
heavyCalendarEventId: await resolveHeavyCalendarEventId(),
|
||||
heavyChatRoomId: await resolveHeavyChatRoomId(),
|
||||
heavyChatMessageId: await resolveHeavyChatMessageId(),
|
||||
heavyChatUsers: await resolveHeavyChatUsers(),
|
||||
openChatRoomIdsByType: await resolveOpenChatRoomIdsByType(),
|
||||
resultsEventId: await resolveResultsEventId(),
|
||||
calendarAuthorId: await resolveCalendarAuthorId(),
|
||||
calendarWindow: await resolveCalendarWindow(),
|
||||
@@ -174,6 +188,8 @@ export async function resolveFixtures(): Promise<Fixtures> {
|
||||
badgeManagerUserId: await resolveBadgeManagerUserId(),
|
||||
trophy: await resolveTrophy(),
|
||||
manyUserIds: await resolveManyUserIds(heavyTournamentId),
|
||||
manyTournamentTeamIds:
|
||||
await resolveManyTournamentTeamIds(heavyTournamentId),
|
||||
notification: await resolveNotification(),
|
||||
heavyAssociation: await resolveHeavyAssociation(),
|
||||
lfgAuthorId: await resolveLfgAuthorId(),
|
||||
@@ -613,6 +629,143 @@ async function resolveHeavyChatRoomId() {
|
||||
return row?.roomId ?? null;
|
||||
}
|
||||
|
||||
/** As many rooms of one type as a single resolve pass batches together. */
|
||||
const CHAT_ROOM_BATCH_SIZE = 25;
|
||||
|
||||
/**
|
||||
* The two users the open-room lookup is worst for. Tournament match rooms are
|
||||
* left out of the search: their participants hang off the JSON opponent ids,
|
||||
* which no index covers, so counting them would mean a scan per open room.
|
||||
*/
|
||||
async function resolveHeavyChatUsers() {
|
||||
const openRooms = () =>
|
||||
db
|
||||
.selectFrom("ChatRoom")
|
||||
.where("ChatRoom.closedAt", "is", null)
|
||||
.where("ChatRoom.expiresAt", ">", databaseTimestampNow());
|
||||
|
||||
const participations = await Promise.all([
|
||||
openRooms()
|
||||
.innerJoin("Group", "Group.chatRoomId", "ChatRoom.id")
|
||||
.innerJoin("GroupMember", "GroupMember.groupId", "Group.id")
|
||||
.select("GroupMember.userId")
|
||||
.execute(),
|
||||
openRooms()
|
||||
.innerJoin("GroupMatch", "GroupMatch.chatRoomId", "ChatRoom.id")
|
||||
.innerJoin("GroupMember", (join) =>
|
||||
join.on((eb) =>
|
||||
eb.or([
|
||||
eb("GroupMember.groupId", "=", eb.ref("GroupMatch.alphaGroupId")),
|
||||
eb("GroupMember.groupId", "=", eb.ref("GroupMatch.bravoGroupId")),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.select("GroupMember.userId")
|
||||
.execute(),
|
||||
openRooms()
|
||||
.innerJoin("TournamentTeam", "TournamentTeam.chatRoomId", "ChatRoom.id")
|
||||
.innerJoin(
|
||||
"TournamentTeamMember",
|
||||
"TournamentTeamMember.tournamentTeamId",
|
||||
"TournamentTeam.id",
|
||||
)
|
||||
.select("TournamentTeamMember.userId")
|
||||
.execute(),
|
||||
openRooms()
|
||||
.innerJoin("ScrimPost", "ScrimPost.chatRoomId", "ChatRoom.id")
|
||||
.innerJoin("ScrimPostUser", "ScrimPostUser.scrimPostId", "ScrimPost.id")
|
||||
.select("ScrimPostUser.userId")
|
||||
.execute(),
|
||||
]);
|
||||
|
||||
const roomCountByUserId = new Map<number, number>();
|
||||
for (const row of participations.flat()) {
|
||||
roomCountByUserId.set(
|
||||
row.userId,
|
||||
(roomCountByUserId.get(row.userId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
|
||||
let busiestUserId: number | null = null;
|
||||
let busiestCount = 0;
|
||||
for (const [userId, count] of roomCountByUserId) {
|
||||
if (count > busiestCount) {
|
||||
busiestUserId = userId;
|
||||
busiestCount = count;
|
||||
}
|
||||
}
|
||||
if (busiestUserId === null) return null;
|
||||
|
||||
const openRoomIds =
|
||||
await ChatRepository.findAllOpenRoomIdsByUserId(busiestUserId);
|
||||
if (openRoomIds.length === 0) return null;
|
||||
|
||||
const mostConnectedId = await resolveMostConnectedUserId();
|
||||
if (mostConnectedId === null) return null;
|
||||
|
||||
return { busiest: { id: busiestUserId, openRoomIds }, mostConnectedId };
|
||||
}
|
||||
|
||||
/** The longest membership history the open-room lookup has to walk, open rooms or not. */
|
||||
async function resolveMostConnectedUserId() {
|
||||
const memberships = await Promise.all([
|
||||
db.selectFrom("GroupMember").select("GroupMember.userId").execute(),
|
||||
db
|
||||
.selectFrom("TournamentTeamMember")
|
||||
.select("TournamentTeamMember.userId")
|
||||
.execute(),
|
||||
db.selectFrom("ScrimPostUser").select("ScrimPostUser.userId").execute(),
|
||||
]);
|
||||
|
||||
const countByUserId = new Map<number, number>();
|
||||
for (const row of memberships.flat()) {
|
||||
countByUserId.set(row.userId, (countByUserId.get(row.userId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
let mostConnectedId: number | null = null;
|
||||
let mostConnectedCount = 0;
|
||||
for (const [userId, count] of countByUserId) {
|
||||
if (count > mostConnectedCount) {
|
||||
mostConnectedId = userId;
|
||||
mostConnectedCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return mostConnectedId;
|
||||
}
|
||||
|
||||
async function resolveOpenChatRoomIdsByType() {
|
||||
const rows = await db
|
||||
.selectFrom("ChatRoom")
|
||||
.select(["ChatRoom.id", "ChatRoom.type"])
|
||||
.where("ChatRoom.closedAt", "is", null)
|
||||
.where("ChatRoom.expiresAt", ">", databaseTimestampNow())
|
||||
.execute();
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const idsByType: Partial<Record<ChatRoomType, number[]>> = {};
|
||||
for (const row of rows) {
|
||||
const ids = idsByType[row.type] ?? [];
|
||||
if (ids.length < CHAT_ROOM_BATCH_SIZE) ids.push(row.id);
|
||||
idsByType[row.type] = ids;
|
||||
}
|
||||
|
||||
return idsByType;
|
||||
}
|
||||
|
||||
async function resolveManyTournamentTeamIds(heavyTournamentId: number | null) {
|
||||
if (heavyTournamentId === null) return null;
|
||||
|
||||
const rows = await db
|
||||
.selectFrom("TournamentTeam")
|
||||
.select("id")
|
||||
.where("tournamentId", "=", heavyTournamentId)
|
||||
.limit(64)
|
||||
.execute();
|
||||
|
||||
return rows.length > 0 ? rows.map((row) => row.id) : null;
|
||||
}
|
||||
|
||||
async function resolveHeavyChatMessageId() {
|
||||
const row = await db
|
||||
.selectFrom("ChatMessage")
|
||||
|
||||
Reference in New Issue
Block a user