Misc performance optimizations

This commit is contained in:
Kalle 2024-04-06 00:11:48 +03:00
parent 2f75446fe6
commit 67a2efc9fe
18 changed files with 161 additions and 92 deletions

View File

@ -1,6 +1,7 @@
import { Link } from "@remix-run/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import type { RootLoaderData } from "~/root";
import { usePatrons } from "~/hooks/swr";
import { discordFullName } from "~/utils/strings";
import {
CONTRIBUTIONS_PAGE,
@ -13,18 +14,14 @@ import {
SUPPORT_PAGE,
userPage,
} from "~/utils/urls";
import { Image } from "../Image";
import { DiscordIcon } from "../icons/Discord";
import { GitHubIcon } from "../icons/GitHub";
import { PatreonIcon } from "../icons/Patreon";
import { Image } from "../Image";
const currentYear = new Date().getFullYear();
export function Footer({
patrons = [],
}: {
patrons?: RootLoaderData["patrons"];
}) {
function _Footer() {
const { t } = useTranslation();
return (
@ -64,31 +61,7 @@ export function Footer({
<PatreonIcon className="layout__footer__social-icon patreon" />
</Link>
</div>
{patrons.length > 0 ? (
<div>
<h4 className="layout__footer__patron-title">
{t("footer.thanks")}
<Image
alt=""
path={SENDOU_LOVE_EMOJI_PATH}
width={24}
height={24}
/>
</h4>
<ul className="layout__footer__patron-list">
{patrons.map((patron) => (
<li key={patron.id}>
<Link
to={userPage(patron)}
className="layout__footer__patron-list__patron"
>
{discordFullName(patron)}
</Link>
</li>
))}
</ul>
</div>
) : null}
<PatronsList />
<div className="layout__copyright-note">
<p>
sendou.ink © Copyright of Sendou and contributors 2019-{currentYear}.
@ -114,3 +87,31 @@ export function Footer({
</footer>
);
}
function PatronsList() {
const { t } = useTranslation();
const { patrons } = usePatrons();
return (
<div>
<h4 className="layout__footer__patron-title">
{t("footer.thanks")}
<Image alt="" path={SENDOU_LOVE_EMOJI_PATH} width={24} height={24} />
</h4>
<ul className="layout__footer__patron-list">
{patrons?.map((patron) => (
<li key={patron.id}>
<Link
to={userPage(patron)}
className="layout__footer__patron-list__patron"
>
{discordFullName(patron)}
</Link>
</li>
))}
</ul>
</div>
);
}
export const Footer = React.memo(_Footer);

View File

@ -1,12 +1,10 @@
import { Link } from "@remix-run/react";
import navItems from "~/components/layout/nav-items.json";
import { useTranslation } from "react-i18next";
import { navIconUrl } from "~/utils/urls";
import { Image } from "../Image";
import * as React from "react";
export function SideNav() {
const { t } = useTranslation(["common"]);
export function _SideNav() {
return (
<nav className="layout__side-nav layout__item_size">
{navItems.map((item) => {
@ -21,7 +19,7 @@ export function SideNav() {
path={navIconUrl(item.name)}
height={32}
width={32}
alt={t(`common:pages.${item.name}` as any)}
alt={item.name}
/>
</div>
</Link>
@ -30,3 +28,5 @@ export function SideNav() {
</nav>
);
}
export const SideNav = React.memo(_SideNav);

View File

@ -0,0 +1,38 @@
import { SUPPORT_PAGE } from "~/utils/urls";
import { LinkButton } from "../Button";
import { HeartIcon } from "../icons/Heart";
import { LanguageChanger } from "./LanguageChanger";
import { ThemeChanger } from "./ThemeChanger";
import { UserItem } from "./UserItem";
import { useTranslation } from "react-i18next";
import * as React from "react";
export function _TopRightButtons({
showSupport,
isErrored,
}: {
showSupport: boolean;
isErrored: boolean;
}) {
const { t } = useTranslation(["common"]);
return (
<div className="layout__header__right-container">
{showSupport ? (
<LinkButton
to={SUPPORT_PAGE}
size="tiny"
icon={<HeartIcon />}
variant="outlined"
>
{t("common:pages.support")}
</LinkButton>
) : null}
<LanguageChanger />
<ThemeChanger />
{!isErrored ? <UserItem /> : null}
</div>
);
}
export const TopRightButtons = React.memo(_TopRightButtons);

View File

@ -5,14 +5,12 @@ import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix";
import { Footer } from "./Footer";
import { useTranslation } from "react-i18next";
import { Image } from "../Image";
import { UserItem } from "./UserItem";
import { LanguageChanger } from "./LanguageChanger";
import { ThemeChanger } from "./ThemeChanger";
import { LinkButton } from "../Button";
import { SUPPORT_PAGE } from "~/utils/urls";
import { HeartIcon } from "../icons/Heart";
import { useIsMounted } from "~/hooks/useIsMounted";
import clsx from "clsx";
import { TopRightButtons } from "./TopRightButtons";
function useBreadcrumbs() {
const { t } = useTranslation();
@ -94,25 +92,16 @@ export const Layout = React.memo(function Layout({
</>
) : null}
</div>
<div className="layout__header__right-container">
{data && typeof data?.user?.patronTier !== "number" ? (
<LinkButton
to={SUPPORT_PAGE}
size="tiny"
icon={<HeartIcon />}
variant="outlined"
>
{t("common:pages.support")}
</LinkButton>
) : null}
<LanguageChanger />
<ThemeChanger />
{!isErrored ? <UserItem /> : null}
</div>
<TopRightButtons
isErrored={isErrored}
showSupport={Boolean(
data && typeof data?.user?.patronTier !== "number",
)}
/>
</header>
{showLeaderboard ? <MyRampUnit /> : null}
{children}
<Footer patrons={data?.patrons} />
<Footer />
</div>
);
});

View File

@ -0,0 +1,19 @@
import type { SerializeFrom } from "@remix-run/node";
import { json } from "@remix-run/node";
import * as UserRepository from "~/features/user-page/UserRepository.server";
export type PatronsListLoaderData = SerializeFrom<typeof loader>;
export const loader = async () => {
return json(
{
patrons: await UserRepository.findAllPatrons(),
},
{
headers: {
// 4 hours
"Cache-Control": "public, max-age=14400",
},
},
);
};

View File

@ -136,7 +136,7 @@ export function addPlacementRank<T>(entries: T[]) {
}));
}
export async function ownEntryPeek({
export function ownEntryPeek({
leaderboard,
userId,
season,
@ -154,7 +154,7 @@ export async function ownEntryPeek({
const withTier = addTiers([found], season)[0];
const { intervals } = await userSkills(season);
const { intervals } = userSkills(season);
return {
entry: withTier,

View File

@ -143,7 +143,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
return {
userLeaderboard: filteredLeaderboard ?? userLeaderboard,
ownEntryPeek: showOwnEntryPeek
? await ownEntryPeek({
? ownEntryPeek({
leaderboard: fullUserLeaderboard,
season,
userId: user.id,

View File

@ -1,17 +1,15 @@
import type { Skill } from "~/db/types";
import { cache, syncCached } from "~/utils/cache.server";
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "../leaderboards/leaderboards-constants";
import { USER_SKILLS_CACHE_KEY } from "../sendouq/q-constants";
import {
TEAM_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN,
TIERS,
TIERS_BEFORE_LEVIATHAN,
USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN,
type TierName,
TEAM_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN,
TIERS_BEFORE_LEVIATHAN,
} from "./mmr-constants";
import type { Skill } from "~/db/types";
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "../leaderboards/leaderboards-constants";
import { orderedMMRBySeason } from "./queries/orderedMMRBySeason.server";
import { cachified } from "@epic-web/cachified";
import { cache, ttl } from "~/utils/cache.server";
import { HALF_HOUR_IN_MS, ONE_HOUR_IN_MS } from "~/constants";
import { USER_SKILLS_CACHE_KEY } from "../sendouq/q-constants";
export interface TieredSkill {
ordinal: number;
@ -55,18 +53,17 @@ export function freshUserSkills(season: number): {
};
}
export async function userSkills(season: number) {
const cachedSkills = await cachified({
key: `${USER_SKILLS_CACHE_KEY}-${season}`,
cache,
ttl: ttl(HALF_HOUR_IN_MS),
staleWhileRevalidate: ttl(ONE_HOUR_IN_MS),
getFreshValue() {
return freshUserSkills(season);
},
});
const userSkillsCacheKey = (season: number) =>
`${USER_SKILLS_CACHE_KEY}-${season}`;
return cachedSkills;
export function userSkills(season: number) {
return syncCached(userSkillsCacheKey(season), () => freshUserSkills(season));
}
export function refreshUserSkills(season: number) {
cache.delete(userSkillsCacheKey(season));
userSkills(season);
}
export type SkillTierInterval = ReturnType<

View File

@ -272,10 +272,10 @@ type CreateMatchMementoArgs = {
};
mapList: TournamentMapListMap[];
};
export async function createMatchMemento(
export function createMatchMemento(
args: CreateMatchMementoArgs,
): Promise<Omit<ParsedMemento, "mapPreferences">> {
const skills = await userSkills(currentOrPreviousSeason(new Date())!.nth);
): Omit<ParsedMemento, "mapPreferences"> {
const skills = userSkills(currentOrPreviousSeason(new Date())!.nth);
const withTiers = addSkillsToGroups({
groups: {
neutral: [],

View File

@ -297,7 +297,7 @@ export const action: ActionFunction = async ({ request }) => {
alphaGroupId: ourGroup.id,
bravoGroupId: theirGroup.id,
mapList,
memento: await createMatchMemento({
memento: createMatchMemento({
own: { group: ourGroup, preferences: ourGroupPreferences },
their: { group: theirGroup, preferences: theirGroupPreferences },
mapList,
@ -455,7 +455,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const season = currentOrPreviousSeason(new Date());
const { intervals, userSkills: calculatedUserSkills } = await userSkills(
const { intervals, userSkills: calculatedUserSkills } = userSkills(
season!.nth,
);
const groupsWithSkills = addSkillsToGroups({

View File

@ -74,7 +74,7 @@ import {
summarizeMaps,
summarizePlayerResults,
} from "../core/summarizer.server";
import { FULL_GROUP_SIZE, USER_SKILLS_CACHE_KEY } from "../q-constants";
import { FULL_GROUP_SIZE } from "../q-constants";
import { matchSchema } from "../q-schemas.server";
import { matchIdFromParams, winnersArrayToWinner } from "../q-utils";
import { addDummySkill } from "../queries/addDummySkill.server";
@ -103,7 +103,8 @@ import cachified from "@epic-web/cachified";
import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server";
import { CrossIcon } from "~/components/icons/Cross";
import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids";
import { currentSeason } from "~/features/mmr/season";
import { currentOrPreviousSeason, currentSeason } from "~/features/mmr/season";
import { refreshUserSkills } from "~/features/mmr/tiered.server";
import "../q.css";
@ -287,8 +288,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
if (clearCaches) {
// this is kind of useless to do when admin reports since skills don't change
// but it's no the most common case so it's ok
cache.delete(USER_SKILLS_CACHE_KEY);
// but it's not the most common case so it's ok
refreshUserSkills(currentOrPreviousSeason(new Date())!.nth);
refreshStreamsCache();
}

View File

@ -16,9 +16,9 @@ export const handle: SendouRouteHandle = {
i18n: ["q"],
};
export const loader = async () => {
export const loader = () => {
const season = currentOrPreviousSeason(new Date());
const { intervals } = await userSkills(season!.nth);
const { intervals } = userSkills(season!.nth);
return {
intervals,

View File

@ -63,6 +63,7 @@ import { roundMapsFromInput } from "../core/mapList.server";
import { updateRoundMaps } from "~/features/tournament/queries/updateRoundMaps.server";
import { checkInMany } from "~/features/tournament/queries/checkInMany.server";
import { logger } from "~/utils/logger";
import { refreshUserSkills } from "~/features/mmr/tiered.server";
import "../components/Bracket/bracket.css";
import "../tournament-bracket.css";
@ -175,6 +176,10 @@ export const action: ActionFunction = async ({ params, request }) => {
season,
});
if (tournament.ranked) {
refreshUserSkills(season!);
}
break;
}
case "BRACKET_CHECK_IN": {

View File

@ -85,7 +85,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
await UserRepository.identifierToUserId(identifier),
);
const { isAccurateTiers, userSkills } = await _userSkills(season);
const { isAccurateTiers, userSkills } = _userSkills(season);
const { tier } = userSkills[user.id] ?? {
approximate: false,
ordinal: 0,

View File

@ -1,11 +1,13 @@
import useSWRImmutable from "swr/immutable";
import type { EventsWithMapPoolsLoaderData } from "~/features/calendar/routes/map-pool-events";
import type { PatronsListLoaderData } from "~/features/front-page/routes/patrons-list";
import type { TrustersLoaderData } from "~/features/sendouq/routes/trusters";
import type { WeaponUsageLoaderData } from "~/features/sendouq/routes/weapon-usage";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
import {
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
GET_TRUSTERS_ROUTE,
PATRONS_LIST_ROUTE,
getWeaponUsage,
} from "~/utils/urls";
@ -61,3 +63,16 @@ export function useTrusted() {
isError: error,
};
}
export function usePatrons() {
const { data, error } = useSWRImmutable<PatronsListLoaderData>(
PATRONS_LIST_ROUTE,
fetcher(PATRONS_LIST_ROUTE),
);
return {
patrons: data?.patrons,
isLoading: !error && !data,
isError: error,
};
}

View File

@ -19,7 +19,6 @@ import NProgress from "nprogress";
import * as React from "react";
import { useTranslation, type CustomTypeOptions } from "react-i18next";
import { useChangeLanguage } from "remix-i18next/react";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { type SendouRouteHandle } from "~/utils/remix";
import { Catcher } from "./components/Catcher";
import { ConditionalScrollRestoration } from "./components/ConditionalScrollRestoration";
@ -86,7 +85,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
{
locale,
theme: themeSession.getTheme(),
patrons: await UserRepository.findAllPatrons(),
tournaments: await cachified({
key: "tournament-showcase",
cache,

View File

@ -130,6 +130,7 @@ export const soundPath = (fileName: string) =>
export const GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE = "/calendar/map-pool-events";
export const GET_TRUSTERS_ROUTE = "/trusters";
export const PATRONS_LIST_ROUTE = "/patrons-list";
interface UserLinkArgs {
discordId: User["discordId"];

View File

@ -36,6 +36,11 @@ export default defineConfig(() => {
return defineRoutes((route) => {
route("/", "features/front-page/routes/index.tsx");
route(
"/patrons-list",
"features/front-page/routes/patrons-list.ts",
);
route("/u", "features/user-search/routes/u.tsx");
route(