core: optimize scoreboard

This commit is contained in:
undefined committed 2022-11-28 23:56:59 +08:00
1 parent f08ac664da
commit af0649f471
13 files changed
+55 -102

No files matched your search

+1 -1
View File
@@ -56,7 +56,7 @@
"latest-version": "7.0.0",
"mocha": "^10.1.0",
"mongodb": "^3.7.3",
"mongodb-memory-server": "^8.10.0",
"mongodb-memory-server": "^8.10.1",
"nyc": "^15.1.0",
"ora": "^6.1.2",
"semver": "^7.3.8",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "4.4.2",
"version": "4.4.3",
"bin": "bin/hydrooj.js",
"main": "src/plugin-api",
"module": "src/plugin-api",
+1 -71
View File
@@ -137,77 +137,7 @@ export const InvalidOperationError = Err('InvalidOperationError', MethodNotAllow
export const NotLaunchedByPM2Error = Err('NotLaunchedByPM2Error', BadRequestError, 'Not launched by PM2.');
export const FileTooLargeError = Err('FileTooLargeError', ValidationError, 'The uploaded file is too long.');
global.Hydro.error = {
HydroError,
BadRequestError,
BlacklistedError,
ForbiddenError,
NotFoundError,
SendMailError,
LoginError,
CsrfTokenError,
MethodNotAllowedError,
InvalidOperationError,
UserAlreadyExistError,
InvalidTokenError,
UserNotFoundError,
VerifyPasswordError,
ProblemDataNotFoundError,
OpcountExceededError,
PermissionError,
PrivilegeError,
RequireProError,
NoProblemError,
ValidationError,
ProblemNotFoundError,
TrainingNotFoundError,
ContestNotFoundError,
ContestNotEndedError,
RecordNotFoundError,
SolutionNotFoundError,
AlreadyVotedError,
ContestNotAttendedError,
ContestNotLiveError,
ContestScoreboardHiddenError,
ContestAlreadyAttendedError,
DomainAlreadyExistsError,
DomainJoinAlreadyMemberError,
DomainJoinForbiddenError,
InvalidJoinInvitationCodeError,
UserFacingError,
SystemError,
TrainingAlreadyEnrollError,
HomeworkNotLiveError,
HomeworkNotAttendedError,
RemoteOnlineJudgeError,
DiscussionNodeNotFoundError,
DocumentNotFoundError,
DiscussionNotFoundError,
RoleAlreadyExistError,
MessageNotFoundError,
FileTooLargeError,
CurrentPasswordError,
DiscussionLockedError,
FileLimitExceededError,
FileUploadError,
FileExistsError,
HackFailedError,
HackRejudgeFailedError,
CannotDeleteSystemDomainError,
OnlyOwnerCanDeleteDomainError,
NotAssignedError,
BuiltinLoginError,
ProblemAlreadyExistError,
ProblemAlreadyUsedByContestError,
ProblemConfigError,
ProblemIsReferencedError,
ProblemNotAllowPretestError,
ProblemNotAllowLanguageError,
TFAOperationError,
CannotEditSuperAdminError,
NotLaunchedByPM2Error,
AccessDeniedError,
};
global.Hydro.error = module.exports;
/*
class FileTypeNotAllowedError(ValidationError):
+3 -3
View File
@@ -345,7 +345,7 @@ class RecordDetailConnectionHandler extends ConnectionHandler {
if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
this.throttleSend = throttle(this.sendUpdate, 1000);
this.throttleSend = throttle(this.sendUpdate, 1000, { trailing: true });
this.rid = rid.toString();
this.cleanup = bus.on('record/change', this.onRecordChange.bind(this));
this.onRecordChange(rdoc);
@@ -368,9 +368,9 @@ class RecordDetailConnectionHandler extends ConnectionHandler {
// TODO: frontend doesn't support incremental update
// if ($set) this.send({ $set, $push });
if (![STATUS.STATUS_WAITING, STATUS.STATUS_JUDGING, STATUS.STATUS_COMPILING, STATUS.STATUS_FETCHED].includes(rdoc.status)) {
this.sendUpdate(rdoc);
this.disconnectTimeout = setTimeout(() => this.close(4001, 'Ended'), 30000);
} else this.throttleSend(rdoc);
}
this.throttleSend(rdoc);
}
}
+3 -7
View File
@@ -4,7 +4,6 @@ import { ProblemNotFoundError, ValidationError } from '../error';
import { Tdoc, TrainingDoc } from '../interface';
import paginate from '../lib/paginate';
import { PERM, PRIV, STATUS } from '../model/builtin';
import DomainModel from '../model/domain';
import problem from '../model/problem';
import * as system from '../model/system';
import * as training from '../model/training';
@@ -108,16 +107,13 @@ class TrainingDetailHandler extends Handler {
}
}
const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id;
const [udoc, udocs, dudocs, pdict, psdict, selfPsdict] = await Promise.all([
const [udoc, udict, pdict, psdict, selfPsdict] = await Promise.all([
user.getById(domainId, tdoc.owner),
user.getMulti({ _id: { $in: enrollUsers } }).project({ _id: 1, uname: 1, avatar: 1 }).toArray(),
DomainModel.getDomainUserMulti(domainId, enrollUsers).project({ uid: 1, displayName: 1 }).toArray(),
user.getListForRender(domainId, enrollUsers),
problem.getList(domainId, pids, canViewHidden, true),
problem.getListStatus(domainId, targetUser, pids),
shouldCompare ? problem.getListStatus(domainId, this.user._id, pids) : {},
]);
const dudict = {};
for (const dudoc of dudocs) dudict[dudoc.uid] = dudoc.displayName;
const donePids = new Set<number>();
const progPids = new Set<number>();
for (const pid in psdict) {
@@ -151,7 +147,7 @@ class TrainingDetailHandler extends Handler {
done: doneNids.size === tdoc.dag.length,
});
this.response.body = {
tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udocs, dudict, selfPsdict,
tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udict, selfPsdict,
};
this.response.pjax = 'partials/training_detail.html';
this.response.template = 'training_detail.html';
+10 -2
View File
@@ -105,7 +105,15 @@ export interface UserPreferenceDoc {
export type ownerInfo = { owner: number, maintainer?: number[] };
export type User = import('./model/user').User;
export type Udict = NumericDictionary<User>;
export type Udict = Record<number, User>;
export interface BaseUser {
_id: number;
uname: string;
avatar: string;
displayName: string;
}
export type BaseUserDict = Record<number, BaseUser>;
export interface FileInfo {
/** storage path */
@@ -507,7 +515,7 @@ export interface ContestRule<T = any> {
scoreboard: (
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<ContestStat & T>,
) => Promise<[board: ScoreboardRow[], udict: Udict]>;
) => Promise<[board: ScoreboardRow[], udict: BaseUserDict]>;
ranked: (tdoc: Tdoc<30>, cursor: Cursor<ContestStat & T>) => Promise<[number, ContestStat & T][]>;
}
+3 -3
View File
@@ -178,7 +178,7 @@ const acm = buildContestRule({
async scoreboard(isExport, _, tdoc, pdict, cursor) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const udict = await user.getListForRender(tdoc.domainId, uids);
// Find first accept
const first = {};
const data = await document.collStatus.aggregate([
@@ -309,7 +309,7 @@ const oi = buildContestRule({
async scoreboard(isExport, _, tdoc, pdict, cursor) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const udict = await user.getListForRender(tdoc.domainId, uids);
const psdict = {};
const first = {};
await Promise.all(tdoc.pids.map(async (pid) => {
@@ -491,7 +491,7 @@ const homework = buildContestRule({
async scoreboard(isExport, _, tdoc, pdict, cursor) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const udict = await user.getListForRender(tdoc.domainId, uids);
const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
const rows: ScoreboardRow[] = [
columns,
+19 -6
View File
@@ -3,14 +3,14 @@ import LRU from 'lru-cache';
import { Collection, FilterQuery, ObjectID } from 'mongodb';
import { LoginError, UserAlreadyExistError, UserNotFoundError } from '../error';
import {
FileInfo, GDoc, ownerInfo,
BaseUserDict, FileInfo, GDoc, ownerInfo,
Udict, Udoc, VUdoc,
} from '../interface';
import pwhash from '../lib/hash.hydro';
import * as bus from '../service/bus';
import db from '../service/db';
import { Value } from '../typeutils';
import { ArgMethod } from '../utils';
import { ArgMethod, buildProjection } from '../utils';
import { PERM, PRIV } from './builtin';
import domain from './domain';
import * as setting from './setting';
@@ -183,8 +183,10 @@ class UserModel {
if (cache.has(`id/${_id}/${domainId}`)) return cache.get(`id/${_id}/${domainId}`) || null;
const udoc = await (_id < -999 ? collV : coll).findOne({ _id });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
const groups = await UserModel.listGroup(domainId, _id);
const [dudoc, groups] = await Promise.all([
domain.getDomainUser(domainId, udoc),
UserModel.listGroup(domainId, _id),
]);
dudoc.group = groups.map((i) => i.name);
if (typeof scope === 'string') scope = BigInt(scope);
const res = await new User(udoc, dudoc, scope).init();
@@ -339,8 +341,19 @@ class UserModel {
return uid;
}
static getMulti(params: FilterQuery<Udoc> = {}) {
return coll.find(params);
static getMulti(params: FilterQuery<Udoc> = {}, projection?: (keyof Udoc)[]) {
return projection ? coll.find(params).project(buildProjection(projection)) : coll.find(params);
}
static async getListForRender(domainId: string, uids: number[]) {
const [udocs, dudocs] = await Promise.all([
UserModel.getMulti({ _id: { $in: uids } }, ['_id', 'uname', 'avatar']).toArray(),
domain.getDomainUserMulti(domainId, uids).project({ uid: 1, displayName: 1 }).toArray(),
]);
const udict = {};
for (const udoc of udocs) udict[udoc._id] = udoc;
for (const dudoc of dudocs) udict[dudoc.uid].displayName = dudoc.displayName;
return udict as BaseUserDict;
}
@ArgMethod
+3
View File
@@ -58,6 +58,9 @@ export interface HydroResponse {
type: string;
status: number;
template?: string;
/** If set, and pjax content was request from client,
* The template will be used for rendering.
*/
pjax?: string;
redirect?: string;
disposition?: string;
+1 -1
View File
@@ -1,4 +1,4 @@
export function buildProjection(fields: string[]): Record<string, 1> {
export function buildProjection(fields: Array<string | number>): Record<string, 1> {
const o = {};
for (const k of fields) o[k] = 1;
return o;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.42.20",
"version": "4.42.21",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "index.ts",
@@ -32,6 +32,9 @@
--code-font-family: "{{ UserContext.codeFontFamily }}", "monaco", "Source Code Pro", "Consolas", "Lucida Console", "monospace";
--font-ligatures: {{ "normal" if UserContext.codeFontLigatures else "none" }} !important;
}
{% if not isIE(handler.request.headers['user-agent']) %}
#panel { display: flex; flex-direction: column; }
{% endif %}
</style>
{% if process.env.DEV %}
<link rel="stylesheet" media="all" href="/default.theme.css">
@@ -1,7 +1,7 @@
{% extends "layout/basic.html" %}
{% block content %}
<div class="row" data-sticky-parent>
{%- if udocs.length -%}
{%- if Object.keys(udict).length -%}
<div class="medium-2 columns mobile--hide">
<div data-sticky="large"><div class="section side">
<ol class="menu">
@@ -11,14 +11,14 @@
</form>
</li>
<ul class="enroll_user_menu">
{%- for udoc in udocs -%}
<li class="menu__item enroll_user_menu_item" data-uid="{{ udoc._id }}" data-uname="{{ udoc.uname }}">
<a href="./{{ tdoc._id }}?uid={{ udoc._id }}" class="menu__link{% if (handler.request.query.uid or handler.user._id) == uid %} active{% endif %}">
{%- for uid, udoc in udict -%}
<li class="menu__item enroll_user_menu_item" data-uid="{{ uid }}" data-uname="{{ udoc.uname }}">
<a href="./{{ tdoc._id }}?uid={{ uid }}" class="menu__link{% if (handler.request.query.uid or handler.user._id) == uid %} active{% endif %}">
<span class="user-profile-link">
<img class="small user-profile-avatar v-center" loading="lazy" src="{{ avatarUrl(udoc.avatar|default('')) }}" width="20" height="20">
<span class="user-profile-name">
{% if handler.user.hasPerm(perm.PREM_VIEW_DISPLAYNAME) and dudict[udoc._id] and dudict[udoc._id] != udoc.uname %}
{{ dudict[udoc._id] }} ({{ udoc.uname }})
{% if handler.user.hasPerm(perm.PREM_VIEW_DISPLAYNAME) and udoc.displayName and udoc.displayName != udoc.uname %}
{{ udoc.displayName }} ({{ udoc.uname }})
{% else %}
{{ udoc.uname }}
{% endif %}