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,
} 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<ParsedConfig> {
@@ -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;
-3
View File
@@ -14,7 +14,6 @@ export default async function compile(
): Promise<Execute> {
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,
};
}
-1
View File
@@ -6,7 +6,6 @@ export interface Execute {
execute: string;
clean: () => Promise<any>;
copyIn: Record<string, CopyInFile>;
time?: number;
}
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 },
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;
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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;
+2
View File
@@ -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();
+2
View File
@@ -201,6 +201,8 @@ export interface ProblemConfigFile {
subtasks?: SubtaskConfig[];
langs?: string[];
validator?: string;
time_limit_rate?: Record<string, number>;
memory_limit_rate?: Record<string, number>;
}
export interface ProblemConfig {
@@ -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,
};
@@ -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 = [
+5 -4
View File
@@ -343,6 +343,7 @@ export interface NormalizedSubtask extends Required<ParsedSubtask> {
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[],
+2
View File
@@ -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<string, LangConfig> {
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;