优化rating计算速度

This commit is contained in:
masnn committed 2020-06-09 13:22:04 +08:00
1 parent 45f1184d68
commit 55912b087d
22 files changed
+200 -69

No files matched your search

+4
View File
@@ -28,6 +28,10 @@ jobs:
with: with:
name: full.js name: full.js
path: .build/full.js path: .build/full.js
- uses: actions/upload-artifact@v1
with:
name: install.js
path: .build/install.js
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v1
with: with:
name: module name: module
+2 -2
View File
@@ -282,7 +282,7 @@ class ContestCreateHandler extends ContestHandler {
} }
async post({ async post({
domainId, title, content, rule, beginAtDate, beginAtTime, duration, pids, domainId, title, content, rule, beginAtDate, beginAtTime, duration, pids, rated = false,
}) { }) {
let beginAt; let beginAt;
try { try {
@@ -294,7 +294,7 @@ class ContestCreateHandler extends ContestHandler {
pids = await this.verifyProblems(domainId, pids); pids = await this.verifyProblems(domainId, pids);
const tid = await contest.add( const tid = await contest.add(
domainId, title, content, domainId, title, content,
this.user._id, rule, beginAt, endAt, pids, this.user._id, rule, beginAt, endAt, pids, rated,
); );
this.response.body = { tid }; this.response.body = { tid };
this.response.redirect = `/c/${tid}`; this.response.redirect = `/c/${tid}`;
+7 -2
View File
@@ -95,7 +95,12 @@ class DomainUserHandler extends ManageHandler {
const uids = []; const uids = [];
const rudocs = {}; const rudocs = {};
const [udocs, roles] = await Promise.all([ const [udocs, roles] = await Promise.all([
user.getInDomain(domainId), user.getMultiInDomain(domainId, {
$and: [
{ role: { $ne: 'default' } },
{ role: { $ne: null } },
],
}).toArray(),
user.getRoles(domainId), user.getRoles(domainId),
]); ]);
for (const role of roles) rudocs[role._id] = []; for (const role of roles) rudocs[role._id] = [];
@@ -104,7 +109,7 @@ class DomainUserHandler extends ManageHandler {
// TODO Improve here // TODO Improve here
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const ud = await user.getById(domainId, udoc.uid); const ud = await user.getById(domainId, udoc.uid);
rudocs[udoc.role].push({ ...ud, role: udoc.role }); rudocs[ud.role].push(ud);
} }
const rolesSelect = roles.map((role) => [role._id, role._id]); const rolesSelect = roles.map((role) => [role._id, role._id]);
const udict = await user.getList(domainId, uids); const udict = await user.getList(domainId, uids);
+2 -2
View File
@@ -197,7 +197,7 @@ class HomeworkCreateHandler extends HomeworkHandler {
async post({ async post({
domainId, title, content, beginAtDate, beginAtTime, domainId, title, content, beginAtDate, beginAtTime,
penaltySinceDate, penaltySinceTime, extensionDays, penaltySinceDate, penaltySinceTime, extensionDays,
penaltyRules, pids, penaltyRules, pids, rated = false,
}) { }) {
let beginAt; let beginAt;
let penaltySince; let penaltySince;
@@ -216,7 +216,7 @@ class HomeworkCreateHandler extends HomeworkHandler {
if (penaltySince > endAt) throw new ValidationError('extensionDays'); if (penaltySince > endAt) throw new ValidationError('extensionDays');
await this.verifyProblems(domainId, pids); await this.verifyProblems(domainId, pids);
const tid = await contest.add(domainId, title, content, this.user._id, const tid = await contest.add(domainId, title, content, this.user._id,
'homework', beginAt, endAt, pids, { penaltySince, penaltyRules }, document.TYPE_HOMEWORK); 'homework', beginAt, endAt, pids, rated, { penaltySince, penaltyRules }, document.TYPE_HOMEWORK);
this.response.body = { tid }; this.response.body = { tid };
this.response.redirect = `/homework/${tid}`; this.response.redirect = `/homework/${tid}`;
} }
+2 -2
View File
@@ -8,7 +8,7 @@ const { PermissionError } = require('../error');
class SystemHandler extends Handler { class SystemHandler extends Handler {
async prepare() { async prepare() {
if (!this.user.priv) throw new PermissionError('???'); if (!this.user.priv) throw new PermissionError('???');
this.body = { this.response.body = {
path: [ path: [
['Hydro', '/'], ['Hydro', '/'],
['manage_main', '/manage'], ['manage_main', '/manage'],
@@ -58,7 +58,7 @@ class SystemScriptHandler extends SystemHandler {
async post({ id, args }) { async post({ id, args }) {
// TODO Do not use console.log // TODO Do not use console.log
await global.Hydro.script[id].run(args, console.log); await global.Hydro.script[id].run(JSON.parse(args), console.log);
this.back(); this.back();
} }
} }
+1 -1
View File
@@ -176,7 +176,7 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
}); });
await Promise.all([ await Promise.all([
record.judge(domainId, rid), record.judge(domainId, rid),
user.inc(this.user._id, 'nSubmit'), user.incDomain(domainId, this.user._id, 'nSubmit'),
]); ]);
this.response.body = { rid }; this.response.body = { rid };
this.response.redirect = `/r/${rid}`; this.response.redirect = `/r/${rid}`;
+10
View File
@@ -0,0 +1,10 @@
const crypto = require('crypto');
/**
* @param {any} content
* @return {string}
*/
global.Hydro.lib.sha1 = module.exports = (content) => {
const hash = crypto.createHash('sha1');
hash.update(content);
return hash.digest('hex');
};
+2 -2
View File
@@ -216,7 +216,7 @@ const builtinHandler = [
]; ];
const builtinScript = [ const builtinScript = [
'install', 'uninstall', 'rating', 'recalcRating', 'install', 'uninstall', 'rating', 'recalcRating', 'register',
]; ];
async function loadAsMaster() { async function loadAsMaster() {
@@ -385,7 +385,7 @@ async function load() {
cluster.on('online', (worker) => { cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} is online`); console.log(`Worker ${worker.process.pid} is online`);
}); });
for (let i = 0; i < numCPUs; i++) { for (let i = 1; i < numCPUs; i++) {
cluster.fork(); cluster.fork();
} }
} else { } else {
+3 -2
View File
@@ -207,7 +207,8 @@ const RULES = {
* @returns {ObjectID} tid * @returns {ObjectID} tid
*/ */
function add(domainId, title, content, owner, rule, function add(domainId, title, content, owner, rule,
beginAt = new Date(), endAt = new Date(), pids = [], data = {}, type = document.TYPE_CONTEST) { beginAt = new Date(), endAt = new Date(), pids = [], rated = false,
data = {}, type = document.TYPE_CONTEST) {
validator.checkTitle(title); validator.checkTitle(title);
validator.checkContent(content); validator.checkContent(content);
if (!this.RULES[rule]) throw new ValidationError('rule'); if (!this.RULES[rule]) throw new ValidationError('rule');
@@ -217,7 +218,7 @@ function add(domainId, title, content, owner, rule,
}); });
this.RULES[rule].check(data); this.RULES[rule].check(data);
return document.add(domainId, content, owner, type, null, null, null, { return document.add(domainId, content, owner, type, null, null, null, {
...data, title, rule, beginAt, endAt, pids, attend: 0, ...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
}); });
} }
+16 -8
View File
@@ -29,7 +29,7 @@ class USER {
this.nSubmit = dudoc.nSubmit || 0; this.nSubmit = dudoc.nSubmit || 0;
this.rating = dudoc.rating || 1500; this.rating = dudoc.rating || 1500;
this.perm = dudoc.perm; this.perm = dudoc.perm;
this.role = dudoc.role; this.role = dudoc.role || 'default';
} }
hasPerm(p) { hasPerm(p) {
@@ -39,7 +39,7 @@ class USER {
checkPassword(password) { checkPassword(password) {
const h = global.Hydro.lib[`hash.${this.hashType || 'hydro'}`]; const h = global.Hydro.lib[`hash.${this.hashType || 'hydro'}`];
if (!h) throw new Error('Unknown hash method'); if (!h) throw new Error('Unknown hash method');
return h.check(password, this.salt(), this.hash()); return h.check(password, this.salt(), this.hash(), this);
} }
} }
@@ -120,6 +120,17 @@ async function inc(_id, field, n = 1) {
return udoc; return udoc;
} }
function setInDomain(domainId, uid, params) {
return document.setStatus(domainId, document.TYPE_DOMAIN_USER, 0, uid, params);
}
async function incDomain(domainId, uid, field, n = 1) {
const dudoc = await getInDomain(domainId, { _id: uid });
dudoc[field] = dudoc[field] + n || n;
await setInDomain(domainId, uid, { [field]: dudoc[field] });
return dudoc;
}
async function create({ async function create({
uid, mail, uname, password, regip = '127.0.0.1', priv = perm.PRIV_NONE, uid, mail, uname, password, regip = '127.0.0.1', priv = perm.PRIV_NONE,
}) { }) {
@@ -151,10 +162,6 @@ function getMulti(params) {
return coll.find(params); return coll.find(params);
} }
function setInDomain(domainId, uid, params) {
return document.setStatus(domainId, document.TYPE_DOMAIN_USER, 0, uid, params);
}
function setMultiInDomain(domainId, query, params) { function setMultiInDomain(domainId, query, params) {
return document.setMultiStatus(domainId, document.TYPE_DOMAIN_USER, query, params); return document.setMultiStatus(domainId, document.TYPE_DOMAIN_USER, query, params);
} }
@@ -193,8 +200,8 @@ function getRole(domainId, name) {
return document.get(domainId, document.TYPE_DOMAIN_USER, name); return document.get(domainId, document.TYPE_DOMAIN_USER, name);
} }
function getMultiInDomain(domainId) { function getMultiInDomain(domainId, query) {
return document.getMultiStatus(domainId, document.TYPE_DOMAIN_USER); return document.getMultiStatus(domainId, document.TYPE_DOMAIN_USER, query);
} }
function addRole(domainId, name, permission) { function addRole(domainId, name, permission) {
@@ -223,6 +230,7 @@ global.Hydro.model.user = module.exports = {
getByUname, getByUname,
getMulti, getMulti,
inc, inc,
incDomain,
setById, setById,
setEmail, setEmail,
setPassword, setPassword,
+1 -1
View File
@@ -56,7 +56,7 @@ async function run({ username, password } = {}) {
} }
await Promise.all(tasks); await Promise.all(tasks);
tasks = [ tasks = [
allowFail(domain.add, 'system', 0), domain.add('system', 0, 'Hydro').catch(() => domain.edit('system', { owner: 0, name: 'Hydro' })),
collUser.updateOne({ _id: 0 }, { collUser.updateOne({ _id: 0 }, {
$set: defaults({ $set: defaults({
_id: 0, _id: 0,
+9 -7
View File
@@ -14,28 +14,30 @@ async function run({ domainId, isSub = false }, report) {
} }
return true; return true;
} }
const contests = await contest.getMulti(domainId).sort('endAt', -1).toArray(); const contests = await contest.getMulti(domainId, { rated: true }).sort('endAt', -1).toArray();
await report({ total: contests.length }); await report({ total: contests.length });
await user.setMultiInDomain(domainId, {}, { rating: 1500 }); const udict = {};
for (const i in contests) { for (const i in contests) {
const tdoc = contests[i]; const tdoc = contests[i];
const tsdocs = await contest.getMultiStatus(domainId, { docId: tdoc.docId }) const tsdocs = await contest.getMultiStatus(domainId, { docId: tdoc.docId })
.sort(contest.RULES[tdoc.rule].statusSort).toArray(); .sort(contest.RULES[tdoc.rule].statusSort).toArray();
const udict = await user.getList(domainId, tsdocs.map((tsdoc) => tsdoc.uid));
const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs); const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs);
const users = []; const users = [];
for (const result of rankedTsdocs) { for (const result of rankedTsdocs) {
users.push({ ...result[1], rank: result[0], old: udict[result[1].uid].rating || 1500 }); users.push({ ...result[1], rank: result[0], old: udict[result[1].uid] || 1500 });
} }
const rated = rating(users); const rated = rating(users);
const tasks = [];
for (const udoc of rated) { for (const udoc of rated) {
tasks.push(user.setInDomain(domainId, udoc.uid, { rating: udoc.new })); udict[udoc.uid] = udoc.new;
} }
await Promise.all(tasks);
if (isSub) await report({ subProgress: (i + 1) / contests.length }); if (isSub) await report({ subProgress: (i + 1) / contests.length });
else await report({ current: parseInt(i) + 1 }); else await report({ current: parseInt(i) + 1 });
} }
const tasks = [];
for (const uid in udict) {
tasks.push(user.setInDomain(domainId, uid, { rating: udict[uid] }));
}
await Promise.all(tasks);
return true; return true;
} }
+14
View File
@@ -0,0 +1,14 @@
const user = require('../model/user');
const system = require('../model/system');
async function run({
uname, password, mail, uid,
}) {
if (!uid) uid = await system.inc('user');
await user.create({
uid, uname, password, mail, regip: '127.0.0.1',
});
return uid;
}
global.Hydro.script.register = module.exports = { run };
+2 -2
View File
@@ -302,7 +302,7 @@ function Route(route, RouteHandler, permission = null) {
const method = ctx.method.toLowerCase(); const method = ctx.method.toLowerCase();
// TODO(masnn) domainId // TODO(masnn) domainId
const args = { const args = {
...ctx.params, ...ctx.query, ...ctx.request.body, domainId: 'system', domainId: 'system', ...ctx.params, ...ctx.query, ...ctx.request.body,
}; };
if (h.request.host !== await system.get('server.host')) { if (h.request.host !== await system.get('server.host')) {
args.domainId = h.request.url.split('//')[1].split(`.${await system.get('server.host')}`)[0] || ''; args.domainId = h.request.url.split('//')[1].split(`.${await system.get('server.host')}`)[0] || '';
@@ -399,7 +399,7 @@ function Connection(prefix, RouteConnHandler) {
sock.on('connection', async (conn) => { sock.on('connection', async (conn) => {
const h = new RouteConnHandler(conn); const h = new RouteConnHandler(conn);
try { try {
const args = { ...h.request.params, domainId: 'system' }; const args = { domainId: 'system', ...h.request.params };
if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); if (args.uid) args.uid = parseInt(validator.checkUid(args.uid));
if (args.page) args.page = parseInt(args.page); if (args.page) args.page = parseInt(args.page);
+23
View File
@@ -0,0 +1,23 @@
const { md5, sha1 } = global.Hydro.lib;
global.Hydro.lib['hash.vj2'] = module.exports = {
/**
* @param {string} password
* @param {string} salt
* @param {string} hash
*/
check(password, salt, hash, udoc) {
return hash === this.hash(password, salt, udoc);
},
/**
* @param {string} password
* @param {string} salt
*/
hash(password, salt, udoc) {
const { uname, unameLower } = udoc;
const pmd5 = md5(password);
const mixedSha1 = sha1(md5(unameLower + pmd5) + salt + sha1(pmd5 + salt));
return `${Buffer.from(uname).toString('base64')}|${mixedSha1}`;
},
};
+85 -31
View File
@@ -34,9 +34,7 @@ const tasks = {
gender: doc.gender, gender: doc.gender,
qq: doc.qq, qq: doc.qq,
}), }),
document: async (doc) => { document: {
const res = {};
const mapper = {
_id: '_id', _id: '_id',
doc_id: 'docId', doc_id: 'docId',
doc_type: 'docType', doc_type: 'docType',
@@ -107,23 +105,8 @@ const tasks = {
return r; return r;
}, },
}, },
};
for (const key in doc) {
if (typeof mapper[key] === 'string') {
res[mapper[key]] = doc[key];
} else if (typeof mapper[key] === 'object') {
res[mapper[key].field] = mapper[key].processer(doc[key]);
} else if (mapper[key] === null) {
// Ignore this key
} else {
console.log('Unknown key:', key);
}
}
return res;
}, },
'document.status': async (doc) => { 'document.status': {
const res = {};
const mapper = {
_id: '_id', _id: '_id',
doc_id: 'docId', doc_id: 'docId',
doc_type: 'docType', doc_type: 'docType',
@@ -168,17 +151,6 @@ const tasks = {
field: 'donePids', field: 'donePids',
processer: (pids) => pids.map((id) => pid(id)), processer: (pids) => pids.map((id) => pid(id)),
}, },
};
for (const key in doc) {
if (typeof mapper[key] === 'string') {
res[mapper[key]] = doc[key];
} else if (typeof mapper[key] === 'object') {
res[mapper[key].field] = mapper[key].processer(doc[key]);
} else {
console.log('Unknown key:', key);
}
}
return res;
}, },
record: async (doc) => { record: async (doc) => {
const testCases = []; const testCases = [];
@@ -211,6 +183,16 @@ const tasks = {
testCases, testCases,
}; };
}, },
domain: {
_id: '_id',
doc_id: 'docId',
doc_type: 'docType',
owner_uid: 'owner',
name: 'name',
gravatar: 'gravatar',
bulletin: 'bulletin',
pid_counter: 'pidCounter',
},
'fs.files': async (doc) => doc, 'fs.files': async (doc) => doc,
'fs.chunks': async (doc) => doc, 'fs.chunks': async (doc) => doc,
file: async (doc) => ({ file: async (doc) => ({
@@ -227,6 +209,7 @@ const cursor = {
document: (s) => s.collection('document').find({ doc_type: { $ne: 20 } }), document: (s) => s.collection('document').find({ doc_type: { $ne: 20 } }),
'document.status': (s) => s.collection('document.status').find(), 'document.status': (s) => s.collection('document.status').find(),
record: (s) => s.collection('record').find(), record: (s) => s.collection('record').find(),
domain: (s) => s.collection('domain').find(),
'fs.files': (s) => s.collection('fs.files').find(), 'fs.files': (s) => s.collection('fs.files').find(),
'fs.chunks': (s) => s.collection('fs.chunks').find(), 'fs.chunks': (s) => s.collection('fs.chunks').find(),
file: (s) => s.collection('fs.files').find(), file: (s) => s.collection('fs.files').find(),
@@ -255,6 +238,59 @@ async function discussionNode(src, report) {
} }
} }
async function domainUser(src, report) {
const count = await src.collection('domain.user').find().count();
await report({ progress: 1, message: `domain.user: ${count}` });
const total = Math.floor(count / 50);
for (let i = 0; i <= total; i++) {
const docs = await src.collection('domain.user')
.find().skip(i * 50).limit(50)
.toArray();
for (const doc of docs) {
const mapper = {
_id: '_id',
uid: 'uid',
join_at: 'joinAt',
role: 'role',
num_liked: 'nLike',
num_submit: 'nSubmit',
num_accept: 'nAccept',
num_problems: 'nProblems',
domain_id: 'domainId',
level: 'level',
};
const d = {};
for (const key in doc) {
if (typeof mapper[key] === 'string') {
d[mapper[key]] = doc[key];
} else if (typeof mapper[key] === 'object') {
d[mapper[key].field] = mapper[key].processer(doc[key]);
} else if (mapper[key] === null) {
// Ignore this key
} else {
console.log('Unknown key:', key);
}
}
const docWithoutId = {};
for (const key in d) {
if (key !== '_id') {
docWithoutId[key] = d[key];
}
}
d.push(dst.collection('document').updateOne(
{ _id: d._id },
{
$set: {
...docWithoutId, docType: 0, docId: 1, role: docWithoutId.role || 'default',
},
},
{ upsert: true },
));
}
await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) });
}
}
async function fixPid(report) { async function fixPid(report) {
const count = await dst.collection('document').find({ docType: 10 }).count(); const count = await dst.collection('document').find({ docType: 10 }).count();
await report({ progress: 1, message: `Fix pid: ${count}` }); await report({ progress: 1, message: `Fix pid: ${count}` });
@@ -298,8 +334,25 @@ async function task(name, src, report) {
const docs = await cursor[name](src).skip(i * 50).limit(50).toArray(); const docs = await cursor[name](src).skip(i * 50).limit(50).toArray();
const res = []; const res = [];
for (const doc of docs) { for (const doc of docs) {
const d = await tasks[name](doc); let d = {};
if (typeof tasks[name].call === 'function') {
d = await tasks[name](doc);
} else {
const mapper = tasks[name];
for (const key in doc) {
if (typeof mapper[key] === 'string') {
res[mapper[key]] = doc[key];
} else if (typeof mapper[key] === 'object') {
res[mapper[key].field] = mapper[key].processer(doc[key]);
} else if (mapper[key] === null) {
// Ignore this key
} else {
console.log('Unknown key:', key);
}
}
}
if (d) { if (d) {
if (d.rule) d.rated = true; // Hack contest rated field
const docWithoutId = {}; const docWithoutId = {};
const docWithoutDid = {}; const docWithoutDid = {};
for (const key in d) { for (const key in d) {
@@ -380,6 +433,7 @@ async function migrateVijos({
for (const i of d) await task(i, src, report); for (const i of d) await task(i, src, report);
await fixPid(report); await fixPid(report);
await discussionNode(src, report); await discussionNode(src, report);
await domainUser(src, report);
return true; return true;
} }
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydro", "name": "hydro",
"version": "2.0.0", "version": "2.0.1",
"main": "hydro/development.js", "main": "hydro/development.js",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
"author": "masnn", "author": "masnn",
@@ -35,8 +35,10 @@
"build": "node build/index.js", "build": "node build/index.js",
"build:development": "node build/development.js", "build:development": "node build/development.js",
"build:production": "node build/production.js", "build:production": "node build/production.js",
"build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build:production",
"start": "node hydro/loader.js",
"pack": "pkg .", "pack": "pkg .",
"lint": "eslint build hydro module --fix" "lint": "eslint build hydro module tool --fix"
}, },
"pkg": { "pkg": {
"scripts": [ "scripts": [
+9 -1
View File
@@ -15,7 +15,7 @@
row:false row:false
}) }} }) }}
{{ form.form_text({ {{ form.form_text({
columns:8, columns:4,
label:'Title', label:'Title',
name:'title', name:'title',
placeholder:_('title'), placeholder:_('title'),
@@ -23,6 +23,14 @@
autofocus:true, autofocus:true,
row:false row:false
}) }} }) }}
{{ form.form_checkbox({
columns:4,
label:'Rated',
name:'rated',
placeholder:_('rated'),
value:true,
row:false
}) }}
</div><div class="row"> </div><div class="row">
{{ form.form_text({ {{ form.form_text({
columns:4, columns:4,
+1 -1
View File
@@ -17,7 +17,7 @@
<tr> <tr>
<th class="col--description">{{ _('Permissions') }}</th> <th class="col--description">{{ _('Permissions') }}</th>
{% for role in roles %} {% for role in roles %}
<th class="col--p"><span data-tooltip="{{ role }}">{{ role._id }}</span></th> <th class="col--p"><span data-tooltip="{{ role._id }}">{{ role._id }}</span></th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
+1 -1
View File
@@ -1,4 +1,4 @@
.page--manage_permission .page--domain_permission
.col--p .col--p
width: 80px width: 80px
text-align: center text-align: center
+1 -1
View File
@@ -1,4 +1,4 @@
.page--manage_role .page--domain_role
.col--checkbox .col--checkbox
width: 60px width: 60px
+1 -1
View File
@@ -1,4 +1,4 @@
.page--manage_user .page--domain_user
.col--checkbox .col--checkbox
width: 60px width: 60px