diff --git a/frontend/package.json b/frontend/package.json index 84c6426..16884df 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "codemirror": "^6.0.1", "hast-util-to-jsx-runtime": "^2.2.0", "marked": "^2.1.3", + "quickjs-emscripten": "^0.23.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ebf1f77..fe61381 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: marked: specifier: ^2.1.3 version: 2.1.3 + quickjs-emscripten: + specifier: ^0.23.0 + version: 0.23.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -5967,6 +5970,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /quickjs-emscripten@0.23.0: + resolution: {integrity: sha512-CIP+NDRYDDqbT3cTiN8Bon1wsZ7IgISVYCJHYsPc86oxszpepVMPXFfttyQgn1u1okg1HPnCnM7Xv1LrCO/VmQ==} + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: diff --git a/frontend/src/modules/evaluator/EvalTaskManager.ts b/frontend/src/modules/evaluator/EvalTaskManager.ts index f3d6862..2272ec0 100644 --- a/frontend/src/modules/evaluator/EvalTaskManager.ts +++ b/frontend/src/modules/evaluator/EvalTaskManager.ts @@ -45,9 +45,10 @@ export default class EvalTaskManager { this.deployTask(); } } - receiveTaskEnd({ result }: EvalResult, evaluator: Evaluator) { + receiveTaskEnd({ result, error }: EvalResult, evaluator: Evaluator) { this.taskEndNotifier.notify({ result, + error, task: evaluator.currentTask, }); diff --git a/frontend/src/modules/evaluator/eval.worker.ts b/frontend/src/modules/evaluator/eval.worker.ts index 40f1d1d..3232270 100644 --- a/frontend/src/modules/evaluator/eval.worker.ts +++ b/frontend/src/modules/evaluator/eval.worker.ts @@ -1,4 +1,4 @@ -import evalCode from './evalCode'; +import * as QuickJS from './quickjs'; import type { EvalMessage } from './types'; type MessageEvent = { @@ -10,7 +10,7 @@ self.addEventListener('message', async function (e: MessageEvent) { try { const { code, param } = message; - const result = evalCode(code, param); + const result = await QuickJS.evaluate(code, param); self.postMessage(result); } catch (err) { diff --git a/frontend/src/modules/evaluator/evalCode.ts b/frontend/src/modules/evaluator/evalCode.ts deleted file mode 100644 index ffac21d..0000000 --- a/frontend/src/modules/evaluator/evalCode.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EvalResult } from './types'; - -export default function evalCode(code: string, params: string): EvalResult { - /** - * @notice 단순하게 코드 실행을 구현하기 위해 임시적으로 eval을 사용하며 다음 테스크에서 eval을 제거하고 quickJS로 대체할 예정입니다. - */ - const result = eval(`${code}\nsolution(${params})`); - - return { - result, - }; -} diff --git a/frontend/src/modules/evaluator/quickjs.ts b/frontend/src/modules/evaluator/quickjs.ts new file mode 100644 index 0000000..b37d6f8 --- /dev/null +++ b/frontend/src/modules/evaluator/quickjs.ts @@ -0,0 +1,66 @@ +import type { QuickJSContext, QuickJSWASMModule } from 'quickjs-emscripten'; +import { getQuickJS } from 'quickjs-emscripten'; + +const MEM_LIMIT = 1024 * 1024; // 1GB +const STACK_LIMIT = 1024 * 512; // 500MB + +export async function evaluate(code: string, params: string) { + const QuickJS = await getQuickJS(); + const runtime = createRuntime(QuickJS); + const vm = runtime.newContext(); + + try { + return evalCode(vm, code, params); + } finally { + vm.dispose(); + runtime.dispose(); + } +} + +const createRuntime = (quickjs: QuickJSWASMModule) => { + const runtime = quickjs.newRuntime(); + runtime.setMemoryLimit(MEM_LIMIT); + runtime.setMaxStackSize(STACK_LIMIT); + + return runtime; +}; + +const evalCode = (vm: QuickJSContext, code: string, params: string) => { + const script = toRunableScript(code, params); + + const startTime = performance.now(); + const result = vm.unwrapResult(vm.evalCode(script)); + const endTime = performance.now(); + const { error, value } = vm.dump(result); + result.dispose(); + + return { + time: endTime - startTime, + result: value, + error, + }; +}; + +const toRunableScript = (code: string, params: string) => { + return ` + ${code}\n + + (()=>{ + try { + const result = solution(${params}); + return { + value: result, + error: undefined + } + } catch(err) { + return { + error: { + name: err.name, + message: err.message, + stack: err.stack + } + } + } + })() + `; +}; diff --git a/frontend/src/modules/evaluator/types.ts b/frontend/src/modules/evaluator/types.ts index 226f1bb..abc3296 100644 --- a/frontend/src/modules/evaluator/types.ts +++ b/frontend/src/modules/evaluator/types.ts @@ -6,6 +6,11 @@ export type EvalMessage = { }; export type EvalResult = { + error?: { + name: string; + message: string; + stack: string; + }; result: unknown; }; diff --git a/frontend/src/pages/ContestPage.tsx b/frontend/src/pages/ContestPage.tsx index 7544cad..ec30089 100644 --- a/frontend/src/pages/ContestPage.tsx +++ b/frontend/src/pages/ContestPage.tsx @@ -44,20 +44,21 @@ export default function ContestPage() { }; useEffect(() => { - return evaluator.subscribe(({ result, task }) => { + return evaluator.subscribe(({ result, error, task }) => { if (!task) return; const taskId = task.clientId; - const evaluatedTestcase = testCases.find((_, index) => index === taskId); - if (evaluatedTestcase) { - evaluatedTestcase.result = String(result); - } - setTestCases((oldTestCases) => { return oldTestCases.map((tc, index) => { if (index !== taskId) return tc; + if (error) { + return { + ...tc, + result: `${error.name}: ${error.message} \n${error.stack}`, + }; + } return { ...tc, result,