From 4db115ba948e3d28a7646954e84b9ac37f53c957 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Tue, 21 Nov 2023 10:27:59 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20JS=20=EC=9B=B9=20=EC=96=B4?= =?UTF-8?q?=EC=85=88=EB=B8=94=EB=A6=AC=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - quickjs-emscripten 이라는 라이브러리이다. - quickjs를 웹 어셈블리로 로드할 수 있게 하고 일부 간단한 ui를 추가한 라이브러리이다 --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 7 +++++++ 2 files changed, 8 insertions(+) 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: From e722741e896d10e63c09ae10597afc564a8c6c6e Mon Sep 17 00:00:00 2001 From: dev2820 Date: Tue, 21 Nov 2023 12:04:56 +0900 Subject: [PATCH 2/4] =?UTF-8?q?update:=20eval=EC=9D=84=20quickjs=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/modules/evaluator/eval.worker.ts | 4 +- frontend/src/modules/evaluator/evalCode.ts | 12 ----- frontend/src/modules/evaluator/quickjs.ts | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 14 deletions(-) delete mode 100644 frontend/src/modules/evaluator/evalCode.ts create mode 100644 frontend/src/modules/evaluator/quickjs.ts 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..518fd8f --- /dev/null +++ b/frontend/src/modules/evaluator/quickjs.ts @@ -0,0 +1,49 @@ +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 = wrapCodeWithTemplate(code, params); + + const startTime = performance.now(); + const result = vm.unwrapResult(vm.evalCode(script)); + const endTime = performance.now(); + const value = vm.dump(result); + + result.dispose(); + + return { + time: endTime - startTime, + result: value, + }; +}; + +const wrapCodeWithTemplate = (code: string, params: string) => { + return ` + ${code}\n + solution(${params}); + `; +}; From ecb015603f621cef8589923361df45efdec5750c Mon Sep 17 00:00:00 2001 From: dev2820 Date: Tue, 21 Nov 2023 12:15:11 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/modules/evaluator/quickjs.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/modules/evaluator/quickjs.ts b/frontend/src/modules/evaluator/quickjs.ts index 518fd8f..87bc97b 100644 --- a/frontend/src/modules/evaluator/quickjs.ts +++ b/frontend/src/modules/evaluator/quickjs.ts @@ -26,13 +26,12 @@ const createRuntime = (quickjs: QuickJSWASMModule) => { }; const evalCode = (vm: QuickJSContext, code: string, params: string) => { - const script = wrapCodeWithTemplate(code, params); + const script = toRunableScript(code, params); const startTime = performance.now(); const result = vm.unwrapResult(vm.evalCode(script)); const endTime = performance.now(); const value = vm.dump(result); - result.dispose(); return { @@ -41,7 +40,7 @@ const evalCode = (vm: QuickJSContext, code: string, params: string) => { }; }; -const wrapCodeWithTemplate = (code: string, params: string) => { +const toRunableScript = (code: string, params: string) => { return ` ${code}\n solution(${params}); From 973d8cdcfa05e002b602a3c6c62012229bc68573 Mon Sep 17 00:00:00 2001 From: dev2820 Date: Tue, 21 Nov 2023 15:16:09 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20throw=20error=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 유저가 throw Error를 입력하면 모듈 밖에서도 throw하는 문제가 있었음 이 때문에 내부에서 throw를 한번 걸러줄 수 있게 코드를 수정 추가적으로 에러 코드를 출력하도록 변경함 --- .../src/modules/evaluator/EvalTaskManager.ts | 3 ++- frontend/src/modules/evaluator/quickjs.ts | 22 +++++++++++++++++-- frontend/src/modules/evaluator/types.ts | 5 +++++ frontend/src/pages/ContestPage.tsx | 13 ++++++----- 4 files changed, 34 insertions(+), 9 deletions(-) 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/quickjs.ts b/frontend/src/modules/evaluator/quickjs.ts index 87bc97b..b37d6f8 100644 --- a/frontend/src/modules/evaluator/quickjs.ts +++ b/frontend/src/modules/evaluator/quickjs.ts @@ -31,18 +31,36 @@ const evalCode = (vm: QuickJSContext, code: string, params: string) => { const startTime = performance.now(); const result = vm.unwrapResult(vm.evalCode(script)); const endTime = performance.now(); - const value = vm.dump(result); + const { error, value } = vm.dump(result); result.dispose(); return { time: endTime - startTime, result: value, + error, }; }; const toRunableScript = (code: string, params: string) => { return ` ${code}\n - solution(${params}); + + (()=>{ + 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,