Skip to content

Commit 29ef50d

Browse files
committed
feat: Split into small requests in VirtualContest API
1 parent 9f7ba73 commit 29ef50d

File tree

7 files changed

+200
-138
lines changed

7 files changed

+200
-138
lines changed

atcoder-problems-frontend/src/api/APIClient.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import {
1010
RankingEntry,
1111
SumRankingEntry,
1212
} from "../interfaces/RankingEntry";
13-
import { isUserRankEntry, UserRankEntry } from "../interfaces/UserRankEntry";
1413
import { ContestId, ProblemId, UserId } from "../interfaces/Status";
1514
import { isSubmission } from "../interfaces/Submission";
15+
import { isUserRankEntry, UserRankEntry } from "../interfaces/UserRankEntry";
1616
import { clipDifficulty, isValidResult } from "../utils";
17+
import { toChunks } from "../utils/Chunk";
1718
import { ratingInfoOf } from "../utils/RatingInfo";
1819
import { hasPropertyAsType, isString } from "../utils/TypeUtils";
1920
import { useSWRData } from "./index";
@@ -294,23 +295,45 @@ export const useVirtualContestSubmissions = (
294295
problems: ProblemId[],
295296
fromSecond: number,
296297
toSecond: number,
297-
refreshInterval: number
298+
enableAutoRefresh: boolean
298299
) => {
299-
const userList = users.join(",");
300-
const problemList = problems.join(",");
301-
const url = `${ATCODER_API_URL}/v3/users_and_time?users=${userList}&problems=${problemList}&from=${fromSecond}&to=${toSecond}`;
300+
const PROBLEM_CHUNK_SIZE = 10;
301+
const USER_CHUNK_SIZE = 10;
302+
const requestCount =
303+
Math.ceil(users.length / USER_CHUNK_SIZE) *
304+
Math.ceil(problems.length / PROBLEM_CHUNK_SIZE);
305+
306+
const refreshInterval = enableAutoRefresh
307+
? Math.max(1, requestCount / 10) * 60_000
308+
: 1_000_000_000;
309+
310+
const userChunks = toChunks(users, USER_CHUNK_SIZE);
311+
const problemChunks = toChunks(problems, PROBLEM_CHUNK_SIZE);
312+
const singleFetch = async (users: UserId[], problems: ProblemId[]) => {
313+
const userList = users.join(",");
314+
const problemList = problems.join(",");
315+
const url = `${ATCODER_API_URL}/v3/users_and_time?users=${userList}&problems=${problemList}&from=${fromSecond}&to=${toSecond}`;
316+
const submissions = await fetchTypedArray(url, isSubmission);
317+
return submissions.filter((submission) => isValidResult(submission.result));
318+
};
319+
320+
const fetcher = async () => {
321+
const promises = userChunks
322+
.flatMap((users) =>
323+
problemChunks.map((problems) => ({ users, problems }))
324+
)
325+
.map(({ users, problems }) => singleFetch(users, problems));
326+
const submissionChunks = await Promise.all(promises);
327+
return submissionChunks.flatMap((x) => x);
328+
};
329+
302330
return useSWRData(
303-
url,
304-
(url) =>
305-
userList.length > 0
306-
? fetchTypedArray(url, isSubmission).then((submissions) =>
307-
submissions.filter((submission) => isValidResult(submission.result))
308-
)
309-
: Promise.resolve([]),
331+
"useVirtualContestSubmissions",
332+
() => (users.length > 0 ? fetcher() : Promise.resolve([])),
310333
{
311334
refreshInterval,
312335
}
313-
).data;
336+
);
314337
};
315338

316339
export const useRecentSubmissions = () => {

atcoder-problems-frontend/src/pages/Internal/VirtualContest/ShowContest/ContestTable.tsx

Lines changed: 130 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,50 @@
1-
import { Table } from "reactstrap";
21
import React from "react";
32
import { useLocation } from "react-router-dom";
3+
import { Alert, Spinner, Table } from "reactstrap";
44
import {
55
useMergedProblemMap,
66
useProblemModelMap,
77
useVirtualContestSubmissions,
88
} from "../../../../api/APIClient";
9-
import { clipDifficulty, ordinalSuffixOf } from "../../../../utils";
10-
import { VirtualContestItem } from "../../types";
119
import { ProblemLink } from "../../../../components/ProblemLink";
12-
import { ProblemId, UserId } from "../../../../interfaces/Status";
13-
import {
10+
import { TweetButton } from "../../../../components/TweetButton";
11+
import MergedProblem from "../../../../interfaces/MergedProblem";
12+
import ProblemModel, {
1413
isProblemModelWithDifficultyModel,
1514
isProblemModelWithTimeModel,
1615
ProblemModelWithDifficultyModel,
1716
ProblemModelWithTimeModel,
1817
} from "../../../../interfaces/ProblemModel";
18+
import { ProblemId, UserId } from "../../../../interfaces/Status";
19+
import { clipDifficulty, ordinalSuffixOf } from "../../../../utils";
20+
import { getCurrentUnixtimeInSecond } from "../../../../utils/DateUtil";
1921
import {
2022
calculatePerformances,
2123
makeBotRunners,
2224
} from "../../../../utils/RatingSystem";
23-
import { TweetButton } from "../../../../components/TweetButton";
24-
import { getCurrentUnixtimeInSecond } from "../../../../utils/DateUtil";
25+
import { VirtualContestItem } from "../../types";
26+
import { ContestTableRow } from "./ContestTableRow";
27+
import { FirstAcceptanceRow } from "./FirstAcceptanceRow";
2528
import {
2629
calcUserTotalResult,
2730
compareTotalResult,
2831
ReducedProblemResult,
2932
UserTotalResult,
3033
} from "./ResultCalcUtil";
31-
import { ContestTableRow } from "./ContestTableRow";
32-
import { FirstAcceptanceRow } from "./FirstAcceptanceRow";
33-
import {
34-
compareProblem,
35-
getPointOverrideMap,
36-
getResultsByUserMap,
37-
} from "./util";
34+
import { compareProblem, getResultsByUserMap } from "./util";
35+
36+
interface VirtualContestProblem {
37+
item: VirtualContestItem;
38+
title?: string;
39+
contestId?: string;
40+
}
3841

3942
interface Props {
4043
readonly contestId: string;
4144
readonly contestTitle: string;
4245
readonly showRating: boolean;
4346
readonly showProblems: boolean;
44-
readonly problems: {
45-
item: VirtualContestItem;
46-
title?: string;
47-
contestId?: string;
48-
}[];
47+
readonly problems: VirtualContestProblem[];
4948
readonly enableEstimatedPerformances: boolean;
5049
readonly users: string[];
5150
readonly start: number;
@@ -56,25 +55,65 @@ interface Props {
5655
readonly penaltySecond: number;
5756
}
5857

59-
export const ContestTable = (props: Props) => {
60-
const {
61-
contestId,
62-
contestTitle,
63-
showRating,
64-
showProblems,
65-
problems,
66-
users,
67-
start,
68-
end,
69-
atCoderUserId,
70-
pinMe,
71-
penaltySecond,
72-
} = props;
73-
const query = new URLSearchParams(useLocation().search);
74-
const showBots = !!query.get("bot");
75-
const problemModels = useProblemModelMap();
76-
const { data: problemMap } = useMergedProblemMap();
58+
const getPerformanceByUserId = (
59+
lookForUserId: string,
60+
sortedUserIds: UserId[],
61+
performanceMap: Map<UserId, number>
62+
) => {
63+
const index = sortedUserIds.indexOf(lookForUserId);
64+
if (index < 0) {
65+
return undefined;
66+
}
67+
let upper: number | undefined;
68+
for (let i = index; i < sortedUserIds.length; i++) {
69+
const userId = sortedUserIds[i];
70+
const performance = performanceMap.get(userId);
71+
if (performance !== undefined) {
72+
upper = performance;
73+
break;
74+
}
75+
}
76+
77+
let lower: number | undefined;
78+
for (let i = index; i >= 0; i--) {
79+
const userId = sortedUserIds[i];
80+
const performance = performanceMap.get(userId);
81+
if (performance !== undefined) {
82+
lower = performance;
83+
break;
84+
}
85+
}
7786

87+
if (lower !== undefined && upper !== undefined) {
88+
return (lower + upper) / 2;
89+
} else if (lower !== undefined) {
90+
return lower;
91+
} else if (upper !== undefined) {
92+
return upper;
93+
} else {
94+
return undefined;
95+
}
96+
};
97+
98+
const constructPointOverrideMap = <T extends { item: VirtualContestItem }>(
99+
problems: T[]
100+
) => {
101+
const pointOverrideMap = new Map<ProblemId, number>();
102+
problems.forEach(({ item }) => {
103+
const problemId = item.id;
104+
const point = item.point;
105+
if (point !== null) {
106+
pointOverrideMap.set(problemId, point);
107+
}
108+
});
109+
return pointOverrideMap;
110+
};
111+
112+
const consolidateModels = (
113+
problems: VirtualContestProblem[],
114+
problemMap?: Map<ProblemId, MergedProblem>,
115+
problemModels?: Map<ProblemId, ProblemModel>
116+
) => {
78117
const modelArray = [] as {
79118
problemModel: ProblemModelWithDifficultyModel & ProblemModelWithTimeModel;
80119
problemId: string;
@@ -91,27 +130,54 @@ export const ContestTable = (props: Props) => {
91130
modelArray.push({ problemModel, problemId, point });
92131
}
93132
});
133+
return modelArray;
134+
};
94135

136+
export const ContestTable = (props: Props) => {
137+
const {
138+
contestId,
139+
contestTitle,
140+
showRating,
141+
showProblems,
142+
problems,
143+
users,
144+
start,
145+
end,
146+
atCoderUserId,
147+
pinMe,
148+
penaltySecond,
149+
} = props;
150+
const query = new URLSearchParams(useLocation().search);
151+
const showBots = !!query.get("bot");
152+
const problemModels = useProblemModelMap();
153+
const { data: problemMap } = useMergedProblemMap();
95154
const submissions = useVirtualContestSubmissions(
96155
props.users,
97156
problems.map((p) => p.item.id),
98157
start,
99158
end,
100-
props.enableAutoRefresh ? 60_000 : 1_000_000_000
159+
props.enableAutoRefresh
101160
);
161+
if (!submissions.data && !submissions.error) {
162+
return <Spinner />;
163+
}
164+
if (!submissions.data) {
165+
return <Alert color="danger">Failed to fetch submissions.</Alert>;
166+
}
102167

103-
const pointOverrideMap = getPointOverrideMap(problems);
168+
const modelArray = consolidateModels(problems, problemMap, problemModels);
169+
const pointOverrideMap = constructPointOverrideMap(problems);
104170
const resultsByUser = getResultsByUserMap(
105-
submissions ?? [],
171+
submissions.data,
106172
users,
107173
(problemId) => pointOverrideMap.get(problemId)
108174
);
109175

110-
const currentSecond = Math.floor(new Date().getTime() / 1000);
176+
const now = getCurrentUnixtimeInSecond();
111177
const showEstimatedPerformances =
112178
props.enableEstimatedPerformances &&
113179
modelArray.length === problems.length &&
114-
currentSecond >= start;
180+
now >= start;
115181
const botRunnerIds = new Set<UserId>();
116182
const ratingMap = new Map<UserId, number>();
117183
if (showEstimatedPerformances) {
@@ -157,42 +223,6 @@ export const ContestTable = (props: Props) => {
157223
}
158224
}
159225

160-
const getPerformanceByUserId = (lookForUserId: string) => {
161-
const index = sortedUserIds.indexOf(lookForUserId);
162-
if (index < 0) {
163-
return undefined;
164-
}
165-
let upper: number | undefined;
166-
for (let i = index; i < sortedUserIds.length; i++) {
167-
const userId = sortedUserIds[i];
168-
const performance = performanceMap.get(userId);
169-
if (performance !== undefined) {
170-
upper = performance;
171-
break;
172-
}
173-
}
174-
175-
let lower: number | undefined;
176-
for (let i = index; i >= 0; i--) {
177-
const userId = sortedUserIds[i];
178-
const performance = performanceMap.get(userId);
179-
if (performance !== undefined) {
180-
lower = performance;
181-
break;
182-
}
183-
}
184-
185-
if (lower !== undefined && upper !== undefined) {
186-
return (lower + upper) / 2;
187-
} else if (lower !== undefined) {
188-
return lower;
189-
} else if (upper !== undefined) {
190-
return upper;
191-
} else {
192-
return undefined;
193-
}
194-
};
195-
196226
const showingUserIds = sortedUserIds.filter(
197227
(userId) => !botRunnerIds.has(userId) || showBots
198228
);
@@ -209,21 +239,18 @@ export const ContestTable = (props: Props) => {
209239
}))
210240
.sort(compareProblem);
211241

212-
const now = getCurrentUnixtimeInSecond();
213-
214242
const loginUserRank = loginUserIndex + 1;
215-
const tweetButton =
216-
end < now ? (
217-
<TweetButton
218-
id={contestId}
219-
text={`${atCoderUserId} took ${loginUserRank}${ordinalSuffixOf(
220-
loginUserRank
221-
)} place in ${contestTitle}!`}
222-
color="link"
223-
>
224-
Share it!
225-
</TweetButton>
226-
) : undefined;
243+
const tweetButton = end < now && (
244+
<TweetButton
245+
id={contestId}
246+
text={`${atCoderUserId} took ${loginUserRank}${ordinalSuffixOf(
247+
loginUserRank
248+
)} place in ${contestTitle}!`}
249+
color="link"
250+
>
251+
Share it!
252+
</TweetButton>
253+
);
227254

228255
return (
229256
<Table striped bordered size="sm">
@@ -259,7 +286,11 @@ export const ContestTable = (props: Props) => {
259286
showRating={showRating}
260287
showProblems={showProblems}
261288
start={start}
262-
estimatedPerformance={getPerformanceByUserId(atCoderUserId)}
289+
estimatedPerformance={getPerformanceByUserId(
290+
atCoderUserId,
291+
sortedUserIds,
292+
performanceMap
293+
)}
263294
reducedProblemResults={
264295
resultsByUser.get(atCoderUserId) ??
265296
new Map<ProblemId, ReducedProblemResult>()
@@ -271,15 +302,19 @@ export const ContestTable = (props: Props) => {
271302
{showingUserIds.map((userId, i) => {
272303
return (
273304
<ContestTableRow
274-
tweetButton={atCoderUserId === userId ? tweetButton : undefined}
305+
tweetButton={atCoderUserId === userId && tweetButton}
275306
key={userId}
276307
userId={userId}
277308
rank={i}
278309
sortedItems={sortedItems}
279310
showRating={showRating}
280311
showProblems={showProblems}
281312
start={start}
282-
estimatedPerformance={getPerformanceByUserId(userId)}
313+
estimatedPerformance={getPerformanceByUserId(
314+
userId,
315+
sortedUserIds,
316+
performanceMap
317+
)}
283318
reducedProblemResults={
284319
resultsByUser.get(userId) ??
285320
new Map<ProblemId, ReducedProblemResult>()

0 commit comments

Comments
 (0)