From 560e7c627f69fa7006c61b483d059b21088faafa Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 22 Mar 2023 19:09:54 +0800 Subject: [PATCH] judge: support time_limit_rate & memory_limit_rate per problem (#541) Co-authored-by: panda --- packages/hydrojudge/src/cases.ts | 7 ++++++- packages/hydrojudge/src/compile.ts | 3 --- packages/hydrojudge/src/interface.ts | 1 - packages/hydrojudge/src/judge/default.ts | 4 ++-- packages/hydrojudge/src/judge/hack.ts | 6 +++--- packages/hydrojudge/src/judge/interactive.ts | 6 +++--- packages/hydrojudge/src/judge/run.ts | 4 ++-- packages/hydrojudge/src/task.ts | 2 ++ packages/hydrooj/src/interface.ts | 2 ++ .../components/monaco/schema/problemconfig.ts | 10 ++++++++++ .../components/problemconfig/ProblemConfigEditor.tsx | 1 + packages/utils/lib/common.ts | 9 +++++---- packages/utils/lib/lang.ts | 2 ++ 13 files changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index 2187cba0d..33a87fba2 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -4,6 +4,7 @@ import { changeErrorType, fs, normalizeSubtasks, readSubtasksFromFiles, yaml, } from '@hydrooj/utils'; import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases'; +import { LangConfig } from '@hydrooj/utils/lib/lang'; import { ProblemConfigFile } from 'hydrooj'; import { getConfig } from './config'; import { FormatError, SystemError } from './error'; @@ -40,6 +41,8 @@ interface Args { next: NextFunction; key: string; isSelfSubmission: boolean; + lang: string; + langConfig: LangConfig; } export default async function readCases(folder: string, cfg: ProblemConfigFile = {}, args: Args): Promise { @@ -65,6 +68,8 @@ export default async function readCases(folder: string, cfg: ProblemConfigFile = } catch (e) { throw changeErrorType(e, FormatError); } + const timeRate = +(config.time_limit_rate?.[args.lang] || args.langConfig.time_limit_rate) || 1; + const memoryRate = +(config.memory_limit_rate?.[args.lang] || args.langConfig.memory_limit_rate) || 1; const checkFile = ensureFile(folder); const result = await readYamlCases(config, checkFile) .catch((e) => { throw changeErrorType(e, FormatError); }); @@ -78,7 +83,7 @@ export default async function readCases(folder: string, cfg: ProblemConfigFile = throw new SystemError('Cannot parse testdata.', [e.message, ...(e.params || [])]); } } - result.subtasks = normalizeSubtasks(result.subtasks || [], checkFile, config.time, config.memory); + result.subtasks = normalizeSubtasks(result.subtasks || [], checkFile, config.time, config.memory, false, timeRate, memoryRate); if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key'); if (!result.key) isValidConfig(result); return result; diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts index fd2f68f5f..34e3a0840 100644 --- a/packages/hydrojudge/src/compile.ts +++ b/packages/hydrojudge/src/compile.ts @@ -14,7 +14,6 @@ export default async function compile( ): Promise { const target = lang.target || 'foo'; const execute = copyIn['execute.sh'] ? '/bin/bash execute.sh' : lang.execute; - const time = lang.time_limit_rate || 1; if (lang.compile) { const { status, stdout, stderr, fileIds, @@ -33,14 +32,12 @@ export default async function compile( execute, copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } }, clean: () => del(fileIds[target]), - time, }; } return { execute, copyIn: { ...copyIn, [lang.code_file]: code }, clean: () => Promise.resolve(null), - time, }; } diff --git a/packages/hydrojudge/src/interface.ts b/packages/hydrojudge/src/interface.ts index 11cc4eb2a..9e936f88d 100644 --- a/packages/hydrojudge/src/interface.ts +++ b/packages/hydrojudge/src/interface.ts @@ -6,7 +6,6 @@ export interface Execute { execute: string; clean: () => Promise; copyIn: Record; - time?: number; } export type NextFunction = (body: Partial) => Promise | void; diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index d803126c6..35b9698e1 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -20,7 +20,7 @@ function judgeCase(c: NormalizedCase) { stdin: { src: c.input }, copyIn: ctx.execute.copyIn, filename: ctx.config.filename, - time: c.time * ctx.execute.time, + time: c.time, memory: c.memory, cacheStdoutAndStderr: true, }, @@ -32,7 +32,7 @@ function judgeCase(c: NormalizedCase) { let message: any = ''; let score = 0; if (status === STATUS.STATUS_ACCEPTED) { - if (time > c.time * ctx.execute.time) { + if (time > c.time) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; } else if (memory > c.memory * 1024) { status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts index 93867daa8..98ea9911a 100644 --- a/packages/hydrojudge/src/judge/hack.ts +++ b/packages/hydrojudge/src/judge/hack.ts @@ -33,7 +33,7 @@ export async function judge(ctx: Context) { { stdin: { src: input }, copyIn: { ...validator.copyIn, ...judgeExtraFiles }, - time: parseTimeMS(ctx.config.time || '1s') * execute.time, + time: parseTimeMS(ctx.config.time || '1s'), memory: parseMemoryMB(ctx.config.memory || '256m'), }, ); @@ -46,7 +46,7 @@ export async function judge(ctx: Context) { { stdin: { src: input }, copyIn: execute.copyIn, - time: parseTimeMS(ctx.config.time || '1s') * execute.time, + time: parseTimeMS(ctx.config.time || '1s'), memory: parseMemoryMB(ctx.config.memory || '256m'), cacheStdoutAndStderr: true, }, @@ -55,7 +55,7 @@ export async function judge(ctx: Context) { let { status } = res; let message: any = ''; if (status === STATUS.STATUS_ACCEPTED) { - if (time > ctx.config.time * execute.time) { + if (time > ctx.config.time) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; } else if (memory > ctx.config.memory * 1024) { status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index 2e5ec6f01..9a15751ef 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -17,13 +17,13 @@ function judgeCase(c: NormalizedCase) { { execute: ctx.executeUser.execute, copyIn: ctx.executeUser.copyIn, - time: c.time * ctx.executeUser.time, + time: c.time, memory: c.memory, }, { execute: `${ctx.executeInteractor.execute} /w/in /w/tout /w/out`, copyIn: ctx.executeInteractor.copyIn, - time: c.time * 2 * ctx.executeInteractor.time, + time: c.time * 2, memory: c.memory * 2, copyOut: ['/w/tout?'], env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, @@ -33,7 +33,7 @@ function judgeCase(c: NormalizedCase) { let status: number; let score = 0; let message: any = ''; - if (time > c.time * ctx.executeUser.time) { + if (time > c.time) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; } else if (memory > c.memory * 1024) { status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts index 029665ca9..f049902fd 100644 --- a/packages/hydrojudge/src/judge/run.ts +++ b/packages/hydrojudge/src/judge/run.ts @@ -52,14 +52,14 @@ export const judge = async (ctx: Context) => { stdin: { content: ctx.input }, copyIn: ctx.execute.copyIn, // Allow 2x limits for better debugging - time: parseTimeMS(ctx.config.time || '1s') * ctx.execute.time * 2, + time: parseTimeMS(ctx.config.time || '1s') * 2, memory: parseMemoryMB(ctx.config.memory || '128m'), }, ); const { code, time, memory } = res; let { status } = res; const message: string[] = []; - if (time > parseTimeMS(ctx.config.time || '1s') * ctx.execute.time) { + if (time > parseTimeMS(ctx.config.time || '1s')) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; } else if (memory > parseMemoryMB(ctx.config.memory || '128m') * 1024) { status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; diff --git a/packages/hydrojudge/src/task.ts b/packages/hydrojudge/src/task.ts index 4efbead94..9637c4ede 100644 --- a/packages/hydrojudge/src/task.ts +++ b/packages/hydrojudge/src/task.ts @@ -117,6 +117,8 @@ export class JudgeTask { next: this.next, isSelfSubmission: this.meta.problemOwner === this.request.uid, key: md5(`${this.source}/${getConfig('secret')}`), + lang: this.lang, + langConfig: this.session.getLang(this.lang), }, ); this.stat.judge = new Date(); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 3e108da16..010e1cbe0 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -201,6 +201,8 @@ export interface ProblemConfigFile { subtasks?: SubtaskConfig[]; langs?: string[]; validator?: string; + time_limit_rate?: Record; + memory_limit_rate?: Record; } export interface ProblemConfig { diff --git a/packages/ui-default/components/monaco/schema/problemconfig.ts b/packages/ui-default/components/monaco/schema/problemconfig.ts index 86c9bcb70..90682fef2 100644 --- a/packages/ui-default/components/monaco/schema/problemconfig.ts +++ b/packages/ui-default/components/monaco/schema/problemconfig.ts @@ -40,6 +40,14 @@ const problemConfigSchema: JSONSchema7 = { time: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([mu]?)s?$' }, memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kKmMgG])[bB]?$' }, score: { type: 'integer', maximum: 100, minimum: 1 }, + rateConfig: { + type: 'object', + patternProperties: { + '^.+$': { + type: 'number', + }, + }, + }, }, properties: { redirect: { type: 'string', pattern: '[0-9a-zA-Z_-]+\\/[0-9]+' }, @@ -89,6 +97,8 @@ const problemConfigSchema: JSONSchema7 = { }, additionalProperties: false, }, + time_limit_rate: { $ref: '#/definitions/rateConfig' }, + memory_limit_rate: { $ref: '#/definitions/rateConfig' }, }, additionalProperties: false, }; diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx index da1bc8dea..f3c15721a 100644 --- a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx +++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx @@ -31,6 +31,7 @@ const configKey = [ 'memory', 'filename', 'checker_type', 'checker', 'interactor', 'validator', 'user_extra_files', 'judge_extra_files', 'detail', 'outputs', 'redirect', 'cases', 'subtasks', 'langs', 'key', + 'time_limit_rate', 'memory_limit_rate', ]; const subtasksKey = [ diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts index a25f44bd6..ec3435fa5 100644 --- a/packages/utils/lib/common.ts +++ b/packages/utils/lib/common.ts @@ -343,6 +343,7 @@ export interface NormalizedSubtask extends Required { export function normalizeSubtasks( subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string, time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false, + timeRate = 1, memoryRate = 1, ): NormalizedSubtask[] { subtasks.sort((a, b) => (a.id - b.id)); const subtaskScore = getScore( @@ -362,14 +363,14 @@ export function normalizeSubtasks( if: [], ...s, score, - time: parseTimeMS(s.time || time, !ignoreParseError), - memory: parseMemoryMB(s.memory || memory, !ignoreParseError), + time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate, + memory: parseMemoryMB(s.memory || memory, !ignoreParseError) * memoryRate, cases: s.cases.map((c, index) => ({ id: index + 1, ...c, score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score), - time: parseTimeMS(c.time || s.time || time, !ignoreParseError), - memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError), + time: parseTimeMS(c.time || s.time || time, !ignoreParseError) * timeRate, + memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError) * memoryRate, input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null', output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null', })) as NormalizedCase[], diff --git a/packages/utils/lib/lang.ts b/packages/utils/lib/lang.ts index 035a5c76c..1a579063d 100644 --- a/packages/utils/lib/lang.ts +++ b/packages/utils/lib/lang.ts @@ -8,6 +8,7 @@ export interface LangConfig { highlight: string; monaco: string; time_limit_rate: number; + memory_limit_rate: number; domain: string[]; display: string; target?: string; @@ -38,6 +39,7 @@ export function parseLang(config: string): Record { entry.highlight ||= key; entry.monaco ||= entry.highlight; entry.time_limit_rate ||= 1; + entry.memory_limit_rate ||= 1; entry.code_file ||= `foo.${key}`; entry.execute ||= '/w/foo'; entry.key = key;