From c764fa50105d4e0587e39aaff0a5c8899de20ac9 Mon Sep 17 00:00:00 2001 From: Menci Date: Mon, 10 Dec 2018 19:16:22 +0800 Subject: [PATCH] Better progress feedback when judging --- libs/judger.js | 40 ++++++++++++++++++++++++++++++++----- libs/submissions_process.js | 14 +++++++++++-- modules/submission.js | 4 ++-- static/style.css | 8 +++++++- views/header.ejs | 2 +- views/status_label.ejs | 25 +++++++++++++++++------ views/submission.ejs | 20 +++++++++---------- views/submissions.ejs | 4 ++-- views/submissions_item.ejs | 3 ++- views/util.ejs | 9 ++++++++- 10 files changed, 98 insertions(+), 31 deletions(-) diff --git a/libs/judger.js b/libs/judger.js index 5c308f6..7a8bbc1 100644 --- a/libs/judger.js +++ b/libs/judger.js @@ -13,6 +13,21 @@ let amqpConnection; let amqpSendChannel; let amqpConsumeChannel; +const judgeStateCache = new Map(); + +function getRunningTaskStatusString(result) { + let isPending = status => [0, 1].includes(status); + let allFinished = 0, allTotal = 0; + for (let subtask of result.judge.subtasks) { + for (let curr of subtask.cases) { + allTotal++; + if (!isPending(curr.status)) allFinished++; + } + } + + return `Running ${allFinished}/${allTotal}`; +} + async function connect () { amqpConnection = await amqp.connect(syzoj.config.rabbitMQ); amqpSendChannel = await amqpConnection.createChannel(); @@ -48,11 +63,6 @@ async function connect () { judge_state.result = convertedResult.result; await judge_state.save(); await judge_state.updateRelatedInfo(); - } else if(data.type === interface.ProgressReportType.Progress) { - if(!judge_state) return; - judge_state.score = convertedResult.score; - judge_state.total_time = convertedResult.time; - judge_state.max_memory = convertedResult.memory; } else if(data.type == interface.ProgressReportType.Compiled) { if(!judge_state) return; judge_state.compilation = data.progress; @@ -60,6 +70,7 @@ async function connect () { } else { winston.error("Unsupported result type: " + data.type); } + })(msg).then(async() => { amqpConsumeChannel.ack(msg) }, async(err) => { @@ -78,18 +89,35 @@ async function connect () { (async (result) => { if (result.type === interface.ProgressReportType.Started) { socketio.createTask(result.taskId); + judgeStateCache.set(data.taskId, { + result: 'Compiling', + score: 0, + time: 0, + memory: 0 + }) } else if (result.type === interface.ProgressReportType.Compiled) { socketio.updateCompileStatus(result.taskId, result.progress); } else if (result.type === interface.ProgressReportType.Progress) { + const convertedResult = judgeResult.convertResult(data.taskId, data.progress); + judgeStateCache.set(data.taskId, { + result: getRunningTaskStatusString(data.progress), + score: convertedResult.score, + time: convertedResult.time, + memory: convertedResult.memory + }); socketio.updateProgress(result.taskId, result.progress); } else if (result.type === interface.ProgressReportType.Finished) { socketio.updateResult(result.taskId, result.progress); + setTimeout(() => { + judgeStateCache.delete(result.taskId); + }, 5000); } else if (result.type === interface.ProgressReportType.Reported) { socketio.cleanupProgress(result.taskId); } })(data).then(async() => { progressChannel.ack(msg) }, async(err) => { + console.log(err); winston.error('Error handling progress', err); progressChannel.nack(msg, false, false); }); @@ -144,3 +172,5 @@ module.exports.judge = async function (judge_state, problem, priority) { amqpSendChannel.sendToQueue('judge', msgPack.encode({ content: content, extraData: extraData }), { priority: priority }); } + +module.exports.getCachedJudgeState = taskId => judgeStateCache.get(taskId); diff --git a/libs/submissions_process.js b/libs/submissions_process.js index 61d4d4c..81bff09 100644 --- a/libs/submissions_process.js +++ b/libs/submissions_process.js @@ -1,3 +1,5 @@ +const { getCachedJudgeState } = require('./judger'); + const getSubmissionInfo = (s, displayConfig) => ({ submissionId: s.id, taskId: s.task_id, @@ -10,10 +12,18 @@ const getSubmissionInfo = (s, displayConfig) => ({ submitTime: syzoj.utils.formatDate(s.submit_time), }); -const getRoughResult = (x, displayConfig) => { +const getRoughResult = (x, displayConfig, roughOnly) => { if (displayConfig.showResult) { if (x.pending) { - return null; + let res = getCachedJudgeState(x.task_id) || null + if (!res) return null; + + if (roughOnly) { + res.result = 'Judging'; + res.time = res.memory = res.score = 0; + } + + return res; } else { return { result: x.status, diff --git a/modules/submission.js b/modules/submission.js index fe5601d..3a64702 100644 --- a/modules/submission.js +++ b/modules/submission.js @@ -111,7 +111,7 @@ app.get('/submissions', async (req, res) => { type: 'rough', displayConfig: displayConfig }, syzoj.config.session_secret) : null, - result: getRoughResult(x, displayConfig), + result: getRoughResult(x, displayConfig, true), running: false, })), paginate: paginate, @@ -170,7 +170,7 @@ app.get('/submission/:id', async (req, res) => { displayConfig.showRejudge = await judge.problem.isAllowedEditBy(res.locals.user); res.render('submission', { info: getSubmissionInfo(judge, displayConfig), - roughResult: getRoughResult(judge, displayConfig), + roughResult: getRoughResult(judge, displayConfig, false), code: (judge.problem.type !== 'submit-answer') ? judge.code.toString("utf8") : '', formattedCode: judge.formattedCode ? judge.formattedCode.toString("utf8") : null, preferFormattedCode: res.locals.user ? res.locals.user.prefer_formatted_code : false, diff --git a/static/style.css b/static/style.css index b0d3ce9..e5554cf 100644 --- a/static/style.css +++ b/static/style.css @@ -147,7 +147,13 @@ body > .ui.page.dimmer { :not(.status_detail).status.running, .title:hover .status_detail.status.running, -.title.active .status_detail.status.running { +.title.active .status_detail.status.running, +:not(.status_detail).status.pending, +.title:hover .status_detail.status.pending, +.title.active .status_detail.status.pending, +:not(.status_detail).status.judging, +.title:hover .status_detail.status.judging, +.title.active .status_detail.status.judging { color: #6cf; } diff --git a/views/header.ejs b/views/header.ejs index 111c690..f608d1f 100644 --- a/views/header.ejs +++ b/views/header.ejs @@ -11,7 +11,7 @@ - + diff --git a/views/status_label.ejs b/views/status_label.ejs index 5184c4b..93f2d3b 100644 --- a/views/status_label.ejs +++ b/views/status_label.ejs @@ -11,6 +11,7 @@ const iconList = { 'Waiting': 'hourglass half', 'Running': 'spinner', 'Compiling': 'spinner', + 'Judging': 'spinner', 'Compile Error': 'code', 'Submitted': 'checkmark', // NOI contests 'System Error': 'server', @@ -24,14 +25,26 @@ Vue.component('status-label', { props: ['status', 'indetail', 'progress'], computed: { icon() { - if (this.status in iconList) { - return iconList[this.status]; + var x = this.status.startsWith('Running') ? 'Running' : this.status; + if (x in iconList) { + return iconList[x]; } else { return 'man'; } }, colorClass() { - return (this.indetail ? 'status_detail ' : '') + this.status.toLowerCase().split(' ').join('_'); + var x = this.status.startsWith('Running') ? 'Running' : this.status; + return (this.indetail ? 'status_detail ' : '') + x.toLowerCase().split(' ').join('_'); + }, + displayStatus() { + return this.status.startsWith('Running') ? 'Running' : this.status; + }, + getProgress() { + if (this.status.startsWith('Running ')) { + var tmp = this.status.split(' ')[1].trim().split('/'); + return { total: tmp[1], finished: tmp[0] }; + } + return this.progress; } } }) @@ -39,11 +52,11 @@ Vue.component('status-label', { diff --git a/views/submission.ejs b/views/submission.ejs index 07471c5..c8542b3 100644 --- a/views/submission.ejs +++ b/views/submission.ejs @@ -261,17 +261,17 @@ const vueApp = new Vue({ checkTestcaseOK(c) { return c.status === TaskStatus.Done; }, - getProgress(i) { + getProgress(index) { if (!this.detailResult || !this.detailResult.judge || !this.detailResult.judge.subtasks) return { finished: 0, total: 0 }; - let isPending = status => [TaskStatus.Waiting, TaskStatus.Running].includes(status); - let subtaskProgress = [], allFinished = 0, allTotal = 0; - for (let i in this.detailResult.judge.subtasks) { - let subtaskFinished = 0, subtaskTotal = 0; - for (let j in this.detailResult.judge.subtasks[i].cases) { + var isPending = status => [TaskStatus.Waiting, TaskStatus.Running].includes(status); + var subtaskProgress = [], allFinished = 0, allTotal = 0; + for (var i in this.detailResult.judge.subtasks) { + var subtaskFinished = 0, subtaskTotal = 0; + for (var j in this.detailResult.judge.subtasks[i].cases) { subtaskTotal++, allTotal++; if (!isPending(this.detailResult.judge.subtasks[i].cases[j].status)) subtaskFinished++, allFinished++; } @@ -282,12 +282,12 @@ const vueApp = new Vue({ }); } - let allProgress = { + var allProgress = { finished: allFinished, total: allTotal }; - return typeof i === 'undefined' ? allProgress : subtaskProgress[i]; + return typeof index === 'undefined' ? allProgress : subtaskProgress[index]; } }, mounted() { @@ -336,9 +336,9 @@ if (token != null) { socket.close(); } else { if (data.running) { - vueApp.roughData.running = true; + // vueApp.roughData.running = true; vueApp.detailResult = data.current.content; - vueApp.roughData.result = data.roughResult; + // vueApp.roughData.result = data.roughResult; currentVersion = data.current.version; } } diff --git a/views/submissions.ejs b/views/submissions.ejs index b237ce9..9d37ffe 100644 --- a/views/submissions.ejs +++ b/views/submissions.ejs @@ -55,7 +55,7 @@
不限
<% for (let status in this.icon) { %> <% if (this.iconHidden.includes(status)) continue; %> -
<%= status %>
+
<%= status %>
<% } %> @@ -96,7 +96,7 @@ - + diff --git a/views/submissions_item.ejs b/views/submissions_item.ejs index b5e0cd1..66c11f8 100644 --- a/views/submissions_item.ejs +++ b/views/submissions_item.ejs @@ -20,7 +20,7 @@ const userUrl = <%- JSON.stringify(syzoj.utils.makeUrl(['user', '20000528'])) %> Vue.component('submission-item', { template: '#submissionItemTemplate', - props: ['data', 'config', 'showRejudge', 'progress', 'compiling'], + props: ['data', 'config', 'showRejudge', 'progress', 'compiling', 'rough'], computed: { statusString() { const data = this.data; @@ -28,6 +28,7 @@ Vue.component('submission-item', { if (data.result) { return data.result.result; } else if (data.running) { + if (this.rough) return 'Judging'; if (this.compiling) return 'Compiling'; return 'Running'; } else return 'Waiting'; diff --git a/views/util.ejs b/views/util.ejs index 181a352..ddd207b 100644 --- a/views/util.ejs +++ b/views/util.ejs @@ -44,6 +44,8 @@ this.icon = { 'Waiting': 'hourglass half', 'Running': 'spinner', 'Compiling': 'spinner', + 'Judging': 'spinner', + 'Pending': 'hourglass half', 'Compile Error': 'code', 'Submitted': 'checkmark', // NOI contests 'System Error': 'server', @@ -55,7 +57,12 @@ this.icon = { this.iconHidden = [ 'Success', - 'Submitted' + 'Submitted', + 'Compiling', + 'Running', + 'Waiting', + 'Judging', + 'Skipped' ]; %>