This commit is contained in:
Kalle
2026-08-26 21:19:48 +03:00
parent f1bb6a3635
commit 3038bf29da
8 changed files with 394 additions and 127 deletions

View File

@@ -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,

View File

@@ -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")