core: performance: userCache&scoreboard.pagination

This commit is contained in:
undefined committed 2021-05-02 18:44:15 +08:00
1 parent 0454acdbf9
commit 0d7c53b927
12 files changed
+244 -166

No files matched your search

+72 -68
View File
@@ -8,7 +8,9 @@ import {
ContestNotAttendedError, ContestScoreboardHiddenError,
} from '../error';
import {
ContestRule, ContestRules, ScoreboardNode, ScoreboardRow, Tdoc, Udict,
ContestRule, ContestRules, Pdict,
ScoreboardNode, ScoreboardRow, Tdoc,
Udict,
} from '../interface';
import * as misc from '../lib/misc';
import ranked from '../lib/rank';
@@ -50,7 +52,10 @@ const acm: ContestRule = {
}
return { accept, time, detail };
},
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardRow = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
@@ -93,7 +98,7 @@ const acm: ContestRule = {
for (const item of tsdoc.detail || []) tsddict[item.pid] = item;
const row: ScoreboardRow = [];
row.push(
{ type: 'string', value: rank },
{ type: 'string', value: rank.toString() },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
{ type: 'string', value: tsdoc.accept || 0 },
);
@@ -134,9 +139,11 @@ const acm: ContestRule = {
row.raw = tsdoc;
rows.push(row);
}
return rows;
return [rows, udict];
},
async ranked(tdoc, cursor) {
return await ranked.all(cursor, (a, b) => a.score === b.score);
},
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
};
const oi: ContestRule = {
@@ -154,7 +161,10 @@ const oi: ContestRule = {
},
showScoreboard: (tdoc, now) => now > tdoc.endAt,
showRecord: (tdoc, now) => now > tdoc.endAt,
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
@@ -195,9 +205,11 @@ const oi: ContestRule = {
}
rows.push(row);
}
return rows;
return [rows, udict];
},
async ranked(tdoc, cursor) {
return await ranked.all(cursor, (a, b) => a.score === b.score);
},
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
};
const ioi: ContestRule = {
@@ -255,7 +267,10 @@ const homework: ContestRule = {
},
showScoreboard: () => true,
showRecord: () => true,
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList.call(this, tdoc.domainId, uids);
const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
@@ -343,9 +358,11 @@ const homework: ContestRule = {
}
rows.push(row);
}
return rows;
return [rows, udict];
},
async ranked(tdoc, cursor) {
return await ranked.all(cursor, (a, b) => a.score === b.score);
},
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
};
export const RULES: ContestRules = {
@@ -483,62 +500,6 @@ export function isExtended(tdoc: Tdoc) {
return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime();
}
export const ContestHandlerMixin = (c) => class extends c {
canViewHiddenScoreboard() {
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
canShowRecord(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
canShowScoreboard(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
async getScoreboard(
domainId: string, tid: ObjectID,
isExport = false, docType: 30 | 60 = document.TYPE_CONTEST,
): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict]> {
const {
tdoc, udict, pdict, rankedTsdocs,
} = await this.getRawStatus(domainId, tid, docType);
const rows = RULES[tdoc.rule].scoreboard(
isExport, this.translate.bind(this),
tdoc, rankedTsdocs, udict, pdict,
);
return [tdoc, rows, udict];
}
async getRawStatus(domainId: string, tid: ObjectID, docType: 30 | 60 = document.TYPE_CONTEST) {
const tdoc = await get(domainId, tid, docType);
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
if (!this.canShowRecord(tdoc)) throw new ContestScoreboardHiddenError(tid);
const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).sort(RULES[tdoc.rule].statusSort).toArray();
const uids = tsdocs.map((tsdoc) => tsdoc.uid);
const [udict, pdict] = await Promise.all([
user.getList(domainId, uids),
problem.getList(domainId, tdoc.pids, true),
]);
const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs);
return {
tdoc, tsdocs, udict, pdict, rankedTsdocs,
};
}
// eslint-disable-next-line class-methods-use-this
async verifyProblems(domainId: string, pids: Array<number | string>) {
const pdict = await problem.getList(domainId, pids, true);
const _pids: Set<number> = new Set();
for (const i in pdict) _pids.add(pdict[i].docId);
return Array.from(_pids);
}
};
export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) {
return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set);
}
@@ -584,6 +545,45 @@ export async function recalcStatus(domainId: string, tid: ObjectID, type: Type =
return await Promise.all(tasks);
}
export function canViewHiddenScoreboard() {
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
export function canShowRecord(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
export function canShowScoreboard(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
export async function getScoreboard(
domainId: string, tid: ObjectID,
isExport = false, page: number, docType: 30 | 60 = document.TYPE_CONTEST,
): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, Pdict]> {
const tdoc = await get(domainId, tid, docType);
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
if (!this.canShowRecord(tdoc)) throw new ContestScoreboardHiddenError(tid);
const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).sort(RULES[tdoc.rule].statusSort);
const pdict = await problem.getList(domainId, tdoc.pids, true);
const [rows, udict] = await RULES[tdoc.rule].scoreboard(
isExport, this.translate.bind(this),
tdoc, pdict, tsdocs, page,
);
return [tdoc, rows, udict, pdict];
}
export async function verifyProblems(domainId: string, pids: Array<number | string>) {
const pdict = await problem.getList(domainId, pids, true);
const _pids: Set<number> = new Set();
for (const i in pdict) _pids.add(pdict[i].docId);
return Array.from(_pids);
}
export const statusText = (tdoc: Tdoc) => (
isNew(tdoc)
? 'New'
@@ -602,7 +602,6 @@ export const getStatusText = (tdoc: Tdoc) => (
global.Hydro.model.contest = {
RULES,
ContestHandlerMixin,
add,
getListStatus,
getMultiStatus,
@@ -617,6 +616,11 @@ global.Hydro.model.contest = {
setStatus,
getAndListStatus,
recalcStatus,
canShowRecord,
canShowScoreboard,
canViewHiddenScoreboard,
getScoreboard,
verifyProblems,
isNew,
isUpcoming,
isNotStarted,
+5 -4
View File
@@ -94,17 +94,18 @@ export async function add(
}
// FIXME typings doesn't work
export function get<T extends Ddoc.Field>(domainId: string, did: ObjectID, projection: T[] = PROJECTION_PUBLIC as any): Promise<Pick<Ddoc, T>> {
return document.get(domainId, document.TYPE_DISCUSSION, did, projection);
export async function get<T extends Ddoc.Field>(domainId: string, did: ObjectID, projection: T[] = PROJECTION_PUBLIC as any): Promise<Pick<Ddoc, T>> {
return await document.get(domainId, document.TYPE_DISCUSSION, did, projection);
}
export function edit(
domainId: string, did: ObjectID,
title: string, content: string, highlight: boolean, pin: boolean,
): Promise<Ddoc | null> {
return document.set(domainId, document.TYPE_DISCUSSION, did, {
const payload = {
title, content, highlight, pin,
});
};
return document.set(domainId, document.TYPE_DISCUSSION, did, payload);
}
export function del(domainId: string, did: ObjectID): Promise<never> {
+37 -6
View File
@@ -1,4 +1,5 @@
import { Collection } from 'mongodb';
import LRU from 'lru-cache';
import { escapeRegExp } from 'lodash';
import * as system from './system';
import token from './token';
@@ -16,6 +17,16 @@ import * as bus from '../service/bus';
const coll: Collection<Udoc> = db.collection('user');
const logger = new Logger('model/user');
const cache = new LRU<string, User>(500);
function deleteUserCache(udoc: User | Udoc) {
if (udoc) {
const id = [udoc._id.toString(), udoc.uname.toLowerCase(), udoc.mail.toLowerCase()];
for (const key of cache.keys().filter((k) => id.includes(k.split('/')[0]))) {
cache.del(key);
}
}
}
class User implements _User {
_id: number;
@@ -125,13 +136,18 @@ class UserModel {
@ArgMethod
static async getById(domainId: string, _id: number, scope: bigint | string = PERM.PERM_ALL): Promise<User | null> {
if (cache.has(`${_id}/${domainId}`)) return cache.get(`${_id}/${domainId}`);
const udoc = _id === 0 || _id === 1
? BUILTIN_USERS[_id]
: await coll.findOne({ _id });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
if (typeof scope === 'string') scope = BigInt(scope);
return new UserModel.User(udoc, dudoc, scope).init();
const res = await new User(udoc, dudoc, scope).init();
cache.set(`${res._id}/${domainId}`, res);
cache.set(`${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`${res.mail.toLowerCase()}/${domainId}`, res);
return res;
}
static async getList(domainId: string, uids: number[]): Promise<Udict> {
@@ -145,6 +161,7 @@ class UserModel {
@ArgMethod
static async getByUname(domainId: string, uname: string): Promise<User | null> {
const unameLower = uname.trim().toLowerCase();
if (cache.has(`${unameLower}/${domainId}`)) return cache.get(`${unameLower}/${domainId}`);
const udoc = (unameLower === 'guest')
? BUILTIN_USERS[0]
: unameLower === 'hydro'
@@ -152,12 +169,17 @@ class UserModel {
: await coll.findOne({ unameLower });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
return new UserModel.User(udoc, dudoc).init();
const res = await new UserModel.User(udoc, dudoc).init();
cache.set(`${res._id}/${domainId}`, res);
cache.set(`${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`${res.mail.toLowerCase()}/${domainId}`, res);
return res;
}
@ArgMethod
static async getByEmail(domainId: string, mail: string): Promise<User | null> {
const mailLower = mail.trim().toLowerCase();
if (cache.has(`${mailLower}/${domainId}`)) return cache.get(`${mailLower}/${domainId}`);
const udoc = (mailLower === 'guest@hydro.local')
? BUILTIN_USERS[0]
: mailLower === 'hydro@hydro.local'
@@ -165,7 +187,11 @@ class UserModel {
: await coll.findOne({ mailLower });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
return new UserModel.User(udoc, dudoc).init();
const res = await new UserModel.User(udoc, dudoc).init();
cache.set(`${res._id}/${domainId}`, res);
cache.set(`${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`${res.mail.toLowerCase()}/${domainId}`, res);
return res;
}
@ArgMethod
@@ -173,7 +199,9 @@ class UserModel {
const op: any = {};
if ($set && Object.keys($set).length) op.$set = $set;
if ($unset && Object.keys($unset).length) op.$unset = $unset;
return await coll.findOneAndUpdate({ _id: uid }, op);
const res = await coll.findOneAndUpdate({ _id: uid }, op, { returnOriginal: false });
deleteUserCache(res.value);
return res;
}
@ArgMethod
@@ -189,6 +217,7 @@ class UserModel {
{ $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } },
{ returnOriginal: false },
);
deleteUserCache(res.value);
return res.value;
}
@@ -198,6 +227,7 @@ class UserModel {
if (!udoc) throw new UserNotFoundError(_id);
udoc[field] = udoc[field] + n || n;
await coll.updateOne({ _id }, { $set: { [field]: udoc[field] } });
deleteUserCache(udoc);
return udoc;
}
@@ -252,12 +282,13 @@ class UserModel {
@ArgMethod
static async setPriv(uid: number, priv: number): Promise<Udoc | null> {
const udoc = await coll.findOneAndUpdate(
const res = await coll.findOneAndUpdate(
{ _id: uid },
{ $set: { priv } },
{ returnOriginal: false },
);
return udoc.value;
deleteUserCache(res.value);
return res.value;
}
@ArgMethod