judge: support time_limit_rate & memory_limit_rate per problem (#541)

Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
undefinedandpanda authored and GitHub committed 2023-03-22 19:09:54 +08:00
1 parent f01a8b6671
commit 560e7c627f
13 files changed
+38 -19

No files matched your search

+6 -1
View File
@@ -4,6 +4,7 @@ import {
changeErrorType, fs, normalizeSubtasks, readSubtasksFromFiles, yaml, changeErrorType, fs, normalizeSubtasks, readSubtasksFromFiles, yaml,
} from '@hydrooj/utils'; } from '@hydrooj/utils';
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases'; import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { ProblemConfigFile } from 'hydrooj'; import { ProblemConfigFile } from 'hydrooj';
import { getConfig } from './config'; import { getConfig } from './config';
import { FormatError, SystemError } from './error'; import { FormatError, SystemError } from './error';
@@ -40,6 +41,8 @@ interface Args {
next: NextFunction; next: NextFunction;
key: string; key: string;
isSelfSubmission: boolean; isSelfSubmission: boolean;
lang: string;
langConfig: LangConfig;
} }
export default async function readCases(folder: string, cfg: ProblemConfigFile = {}, args: Args): Promise<ParsedConfig> { export default async function readCases(folder: string, cfg: ProblemConfigFile = {}, args: Args): Promise<ParsedConfig> {
@@ -65,6 +68,8 @@ export default async function readCases(folder: string, cfg: ProblemConfigFile =
} catch (e) { } catch (e) {
throw changeErrorType(e, FormatError); 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 checkFile = ensureFile(folder);
const result = await readYamlCases(config, checkFile) const result = await readYamlCases(config, checkFile)
.catch((e) => { throw changeErrorType(e, FormatError); }); .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 || [])]); 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 && args.key !== result.key) throw new FormatError('Incorrect secret key');
if (!result.key) isValidConfig(result); if (!result.key) isValidConfig(result);
return result; return result;
-3
View File
@@ -14,7 +14,6 @@ export default async function compile(
): Promise<Execute> { ): Promise<Execute> {
const target = lang.target || 'foo'; const target = lang.target || 'foo';
const execute = copyIn['execute.sh'] ? '/bin/bash execute.sh' : lang.execute; const execute = copyIn['execute.sh'] ? '/bin/bash execute.sh' : lang.execute;
const time = lang.time_limit_rate || 1;
if (lang.compile) { if (lang.compile) {
const { const {
status, stdout, stderr, fileIds, status, stdout, stderr, fileIds,
@@ -33,14 +32,12 @@ export default async function compile(
execute, execute,
copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } }, copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } },
clean: () => del(fileIds[target]), clean: () => del(fileIds[target]),
time,
}; };
} }
return { return {
execute, execute,
copyIn: { ...copyIn, [lang.code_file]: code }, copyIn: { ...copyIn, [lang.code_file]: code },
clean: () => Promise.resolve(null), clean: () => Promise.resolve(null),
time,
}; };
} }
-1
View File
@@ -6,7 +6,6 @@ export interface Execute {
execute: string; execute: string;
clean: () => Promise<any>; clean: () => Promise<any>;
copyIn: Record<string, CopyInFile>; copyIn: Record<string, CopyInFile>;
time?: number;
} }
export type NextFunction = (body: Partial<JudgeResultBody>) => Promise<void> | void; export type NextFunction = (body: Partial<JudgeResultBody>) => Promise<void> | void;
+2 -2
View File
@@ -20,7 +20,7 @@ function judgeCase(c: NormalizedCase) {
stdin: { src: c.input }, stdin: { src: c.input },
copyIn: ctx.execute.copyIn, copyIn: ctx.execute.copyIn,
filename: ctx.config.filename, filename: ctx.config.filename,
time: c.time * ctx.execute.time, time: c.time,
memory: c.memory, memory: c.memory,
cacheStdoutAndStderr: true, cacheStdoutAndStderr: true,
}, },
@@ -32,7 +32,7 @@ function judgeCase(c: NormalizedCase) {
let message: any = ''; let message: any = '';
let score = 0; let score = 0;
if (status === STATUS.STATUS_ACCEPTED) { if (status === STATUS.STATUS_ACCEPTED) {
if (time > c.time * ctx.execute.time) { if (time > c.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory > c.memory * 1024) { } else if (memory > c.memory * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
+3 -3
View File
@@ -33,7 +33,7 @@ export async function judge(ctx: Context) {
{ {
stdin: { src: input }, stdin: { src: input },
copyIn: { ...validator.copyIn, ...judgeExtraFiles }, copyIn: { ...validator.copyIn, ...judgeExtraFiles },
time: parseTimeMS(ctx.config.time || '1s') * execute.time, time: parseTimeMS(ctx.config.time || '1s'),
memory: parseMemoryMB(ctx.config.memory || '256m'), memory: parseMemoryMB(ctx.config.memory || '256m'),
}, },
); );
@@ -46,7 +46,7 @@ export async function judge(ctx: Context) {
{ {
stdin: { src: input }, stdin: { src: input },
copyIn: execute.copyIn, copyIn: execute.copyIn,
time: parseTimeMS(ctx.config.time || '1s') * execute.time, time: parseTimeMS(ctx.config.time || '1s'),
memory: parseMemoryMB(ctx.config.memory || '256m'), memory: parseMemoryMB(ctx.config.memory || '256m'),
cacheStdoutAndStderr: true, cacheStdoutAndStderr: true,
}, },
@@ -55,7 +55,7 @@ export async function judge(ctx: Context) {
let { status } = res; let { status } = res;
let message: any = ''; let message: any = '';
if (status === STATUS.STATUS_ACCEPTED) { if (status === STATUS.STATUS_ACCEPTED) {
if (time > ctx.config.time * execute.time) { if (time > ctx.config.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory > ctx.config.memory * 1024) { } else if (memory > ctx.config.memory * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
+3 -3
View File
@@ -17,13 +17,13 @@ function judgeCase(c: NormalizedCase) {
{ {
execute: ctx.executeUser.execute, execute: ctx.executeUser.execute,
copyIn: ctx.executeUser.copyIn, copyIn: ctx.executeUser.copyIn,
time: c.time * ctx.executeUser.time, time: c.time,
memory: c.memory, memory: c.memory,
}, },
{ {
execute: `${ctx.executeInteractor.execute} /w/in /w/tout /w/out`, execute: `${ctx.executeInteractor.execute} /w/in /w/tout /w/out`,
copyIn: ctx.executeInteractor.copyIn, copyIn: ctx.executeInteractor.copyIn,
time: c.time * 2 * ctx.executeInteractor.time, time: c.time * 2,
memory: c.memory * 2, memory: c.memory * 2,
copyOut: ['/w/tout?'], copyOut: ['/w/tout?'],
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
@@ -33,7 +33,7 @@ function judgeCase(c: NormalizedCase) {
let status: number; let status: number;
let score = 0; let score = 0;
let message: any = ''; let message: any = '';
if (time > c.time * ctx.executeUser.time) { if (time > c.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory > c.memory * 1024) { } else if (memory > c.memory * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
+2 -2
View File
@@ -52,14 +52,14 @@ export const judge = async (ctx: Context) => {
stdin: { content: ctx.input }, stdin: { content: ctx.input },
copyIn: ctx.execute.copyIn, copyIn: ctx.execute.copyIn,
// Allow 2x limits for better debugging // 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'), memory: parseMemoryMB(ctx.config.memory || '128m'),
}, },
); );
const { code, time, memory } = res; const { code, time, memory } = res;
let { status } = res; let { status } = res;
const message: string[] = []; 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; status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} else if (memory > parseMemoryMB(ctx.config.memory || '128m') * 1024) { } else if (memory > parseMemoryMB(ctx.config.memory || '128m') * 1024) {
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
+2
View File
@@ -117,6 +117,8 @@ export class JudgeTask {
next: this.next, next: this.next,
isSelfSubmission: this.meta.problemOwner === this.request.uid, isSelfSubmission: this.meta.problemOwner === this.request.uid,
key: md5(`${this.source}/${getConfig('secret')}`), key: md5(`${this.source}/${getConfig('secret')}`),
lang: this.lang,
langConfig: this.session.getLang(this.lang),
}, },
); );
this.stat.judge = new Date(); this.stat.judge = new Date();
+2
View File
@@ -201,6 +201,8 @@ export interface ProblemConfigFile {
subtasks?: SubtaskConfig[]; subtasks?: SubtaskConfig[];
langs?: string[]; langs?: string[];
validator?: string; validator?: string;
time_limit_rate?: Record<string, number>;
memory_limit_rate?: Record<string, number>;
} }
export interface ProblemConfig { export interface ProblemConfig {
@@ -40,6 +40,14 @@ const problemConfigSchema: JSONSchema7 = {
time: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([mu]?)s?$' }, time: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([mu]?)s?$' },
memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kKmMgG])[bB]?$' }, memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kKmMgG])[bB]?$' },
score: { type: 'integer', maximum: 100, minimum: 1 }, score: { type: 'integer', maximum: 100, minimum: 1 },
rateConfig: {
type: 'object',
patternProperties: {
'^.+$': {
type: 'number',
},
},
},
}, },
properties: { properties: {
redirect: { type: 'string', pattern: '[0-9a-zA-Z_-]+\\/[0-9]+' }, redirect: { type: 'string', pattern: '[0-9a-zA-Z_-]+\\/[0-9]+' },
@@ -89,6 +97,8 @@ const problemConfigSchema: JSONSchema7 = {
}, },
additionalProperties: false, additionalProperties: false,
}, },
time_limit_rate: { $ref: '#/definitions/rateConfig' },
memory_limit_rate: { $ref: '#/definitions/rateConfig' },
}, },
additionalProperties: false, additionalProperties: false,
}; };
@@ -31,6 +31,7 @@ const configKey = [
'memory', 'filename', 'checker_type', 'checker', 'interactor', 'memory', 'filename', 'checker_type', 'checker', 'interactor',
'validator', 'user_extra_files', 'judge_extra_files', 'detail', 'outputs', 'validator', 'user_extra_files', 'judge_extra_files', 'detail', 'outputs',
'redirect', 'cases', 'subtasks', 'langs', 'key', 'redirect', 'cases', 'subtasks', 'langs', 'key',
'time_limit_rate', 'memory_limit_rate',
]; ];
const subtasksKey = [ const subtasksKey = [
+5 -4
View File
@@ -343,6 +343,7 @@ export interface NormalizedSubtask extends Required<ParsedSubtask> {
export function normalizeSubtasks( export function normalizeSubtasks(
subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string, subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string,
time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false, time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false,
timeRate = 1, memoryRate = 1,
): NormalizedSubtask[] { ): NormalizedSubtask[] {
subtasks.sort((a, b) => (a.id - b.id)); subtasks.sort((a, b) => (a.id - b.id));
const subtaskScore = getScore( const subtaskScore = getScore(
@@ -362,14 +363,14 @@ export function normalizeSubtasks(
if: [], if: [],
...s, ...s,
score, score,
time: parseTimeMS(s.time || time, !ignoreParseError), time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate,
memory: parseMemoryMB(s.memory || memory, !ignoreParseError), memory: parseMemoryMB(s.memory || memory, !ignoreParseError) * memoryRate,
cases: s.cases.map((c, index) => ({ cases: s.cases.map((c, index) => ({
id: index + 1, id: index + 1,
...c, ...c,
score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score), score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score),
time: parseTimeMS(c.time || s.time || time, !ignoreParseError), time: parseTimeMS(c.time || s.time || time, !ignoreParseError) * timeRate,
memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError), memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError) * memoryRate,
input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null', 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', output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null',
})) as NormalizedCase[], })) as NormalizedCase[],
+2
View File
@@ -8,6 +8,7 @@ export interface LangConfig {
highlight: string; highlight: string;
monaco: string; monaco: string;
time_limit_rate: number; time_limit_rate: number;
memory_limit_rate: number;
domain: string[]; domain: string[];
display: string; display: string;
target?: string; target?: string;
@@ -38,6 +39,7 @@ export function parseLang(config: string): Record<string, LangConfig> {
entry.highlight ||= key; entry.highlight ||= key;
entry.monaco ||= entry.highlight; entry.monaco ||= entry.highlight;
entry.time_limit_rate ||= 1; entry.time_limit_rate ||= 1;
entry.memory_limit_rate ||= 1;
entry.code_file ||= `foo.${key}`; entry.code_file ||= `foo.${key}`;
entry.execute ||= '/w/foo'; entry.execute ||= '/w/foo';
entry.key = key; entry.key = key;