add webworker

This commit is contained in:
Xuan Son Nguyen 2025-02-08 17:54:54 +01:00
parent 84919d2fbf
commit 8e092c4a15
4 changed files with 122 additions and 47 deletions

Binary file not shown.

View file

@ -1,53 +1,103 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAppContext } from '../utils/app.context'; import { useAppContext } from '../utils/app.context';
import { XCloseButton } from '../utils/common'; import { OpenInNewTab, XCloseButton } from '../utils/common';
import { delay } from '../utils/misc';
import StorageUtils from '../utils/storage';
import { CanvasType } from '../utils/types'; import { CanvasType } from '../utils/types';
import { PlayIcon } from '@heroicons/react/24/outline'; import { PlayIcon, StopIcon } from '@heroicons/react/24/outline';
const PyodideWrapper = { const canInterrupt = typeof SharedArrayBuffer === 'function';
load: async function () {
// load pyodide from CDN
// @ts-expect-error experimental pyodide
if (window.addedScriptPyodide) return;
// @ts-expect-error experimental pyodide
window.addedScriptPyodide = true;
const scriptElem = document.createElement('script');
scriptElem.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js';
document.body.appendChild(scriptElem);
},
run: async function (code: string) { // adapted from https://pyodide.org/en/stable/usage/webworker.html
PyodideWrapper.load(); const WORKER_CODE = `
importScripts("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js");
// wait for pyodide to be loaded let stdOutAndErr = [];
// @ts-expect-error experimental pyodide
while (!window.loadPyodide) { let pyodideReadyPromise = loadPyodide({
await delay(100); stdout: (data) => stdOutAndErr.push(data),
} stderr: (data) => stdOutAndErr.push(data),
const stdOutAndErr: string[] = []; });
// @ts-expect-error experimental pyodide
const pyodide = await window.loadPyodide({ let alreadySetBuff = false;
stdout: (data: string) => stdOutAndErr.push(data),
stderr: (data: string) => stdOutAndErr.push(data), self.onmessage = async (event) => {
}); stdOutAndErr = [];
try {
const result = await pyodide.runPythonAsync(code); // make sure loading is done
if (result) { const pyodide = await pyodideReadyPromise;
stdOutAndErr.push(result.toString()); const { id, python, context, interruptBuffer } = event.data;
}
} catch (e) { if (interruptBuffer && !alreadySetBuff) {
console.error(e); pyodide.setInterruptBuffer(interruptBuffer);
stdOutAndErr.push((e as Error).toString()); alreadySetBuff = true;
} }
return stdOutAndErr.join('\n');
}, // Now load any packages we need, run the code, and send the result back.
await pyodide.loadPackagesFromImports(python);
// make a Python dictionary with the data from content
const dict = pyodide.globals.get("dict");
const globals = dict(Object.entries(context));
try {
self.postMessage({ id, running: true });
// Execute the python code in this context
const result = pyodide.runPython(python, { globals });
self.postMessage({ result, id, stdOutAndErr });
} catch (error) {
self.postMessage({ error: error.message, id });
}
interruptBuffer[0] = 0;
}; };
`;
if (StorageUtils.getConfig().pyIntepreterEnabled) { let worker: Worker;
PyodideWrapper.load(); const interruptBuffer = canInterrupt
} ? new Uint8Array(new SharedArrayBuffer(1))
: null;
const runCodeInWorker = (
pyCode: string,
callbackRunning: () => void
): {
donePromise: Promise<string>;
interrupt: () => void;
} => {
if (!worker) {
worker = new Worker(
URL.createObjectURL(new Blob([WORKER_CODE], { type: 'text/javascript' }))
);
}
const id = Math.random() * 1e8;
const context = {};
if (interruptBuffer) {
interruptBuffer[0] = 0;
}
const donePromise = new Promise<string>((resolve) => {
worker.onmessage = (event) => {
const { error, stdOutAndErr, running } = event.data;
if (id !== event.data.id) return;
if (running) {
callbackRunning();
return;
} else if (error) {
resolve(error.toString());
} else {
resolve(stdOutAndErr.join('\n'));
}
};
worker.postMessage({ id, python: pyCode, context, interruptBuffer });
});
const interrupt = () => {
console.log('Interrupting...');
console.trace();
if (interruptBuffer) {
interruptBuffer[0] = 2;
}
};
return { donePromise, interrupt };
};
export default function CanvasPyInterpreter() { export default function CanvasPyInterpreter() {
const { canvasData, setCanvasData } = useAppContext(); const { canvasData, setCanvasData } = useAppContext();
@ -55,13 +105,22 @@ export default function CanvasPyInterpreter() {
const [code, setCode] = useState(canvasData?.content ?? ''); // copy to avoid direct mutation const [code, setCode] = useState(canvasData?.content ?? ''); // copy to avoid direct mutation
const [running, setRunning] = useState(false); const [running, setRunning] = useState(false);
const [output, setOutput] = useState(''); const [output, setOutput] = useState('');
const [interruptFn, setInterruptFn] = useState<() => void>();
const [showStopBtn, setShowStopBtn] = useState(false);
const runCode = async (pycode: string) => { const runCode = async (pycode: string) => {
interruptFn?.();
setRunning(true); setRunning(true);
setOutput('Running...'); setOutput('Loading Pyodide...');
const out = await PyodideWrapper.run(pycode); const { donePromise, interrupt } = runCodeInWorker(pycode, () => {
setOutput('Running...');
setShowStopBtn(canInterrupt);
});
setInterruptFn(() => interrupt);
const out = await donePromise;
setOutput(out); setOutput(out);
setRunning(false); setRunning(false);
setShowStopBtn(false);
}; };
// run code on mount // run code on mount
@ -98,9 +157,21 @@ export default function CanvasPyInterpreter() {
onClick={() => runCode(code)} onClick={() => runCode(code)}
disabled={running} disabled={running}
> >
<PlayIcon className="h-6 w-6" />{' '} <PlayIcon className="h-6 w-6" /> Run
{running ? 'Running...' : 'Run'}
</button> </button>
{showStopBtn && (
<button
className="btn btn-sm bg-base-100 ml-2"
onClick={() => interruptFn?.()}
>
<StopIcon className="h-6 w-6" /> Stop
</button>
)}
<span className="grow text-right text-xs">
<OpenInNewTab href="https://github.com/ggerganov/llama.cpp/issues/11762">
Report a bug
</OpenInNewTab>
</span>
</div> </div>
<textarea <textarea
className="textarea textarea-bordered h-full dark-color" className="textarea textarea-bordered h-full dark-color"

View file

@ -5,6 +5,6 @@ import App from './App.tsx';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode> </StrictMode>
); );

View file

@ -72,5 +72,9 @@ export default defineConfig({
proxy: { proxy: {
'/v1': 'http://localhost:8080', '/v1': 'http://localhost:8080',
}, },
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
}, },
}); });