Skip to content

Commit 8e092c4

Browse files
committed
add webworker
1 parent 84919d2 commit 8e092c4

File tree

4 files changed

+124
-49
lines changed

4 files changed

+124
-49
lines changed
620 Bytes
Binary file not shown.

examples/server/webui/src/components/CanvasPyInterpreter.tsx

Lines changed: 119 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,126 @@
11
import { useEffect, useState } from 'react';
22
import { useAppContext } from '../utils/app.context';
3-
import { XCloseButton } from '../utils/common';
4-
import { delay } from '../utils/misc';
5-
import StorageUtils from '../utils/storage';
3+
import { OpenInNewTab, XCloseButton } from '../utils/common';
64
import { CanvasType } from '../utils/types';
7-
import { PlayIcon } from '@heroicons/react/24/outline';
8-
9-
const PyodideWrapper = {
10-
load: async function () {
11-
// load pyodide from CDN
12-
// @ts-expect-error experimental pyodide
13-
if (window.addedScriptPyodide) return;
14-
// @ts-expect-error experimental pyodide
15-
window.addedScriptPyodide = true;
16-
const scriptElem = document.createElement('script');
17-
scriptElem.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js';
18-
document.body.appendChild(scriptElem);
19-
},
20-
21-
run: async function (code: string) {
22-
PyodideWrapper.load();
23-
24-
// wait for pyodide to be loaded
25-
// @ts-expect-error experimental pyodide
26-
while (!window.loadPyodide) {
27-
await delay(100);
28-
}
29-
const stdOutAndErr: string[] = [];
30-
// @ts-expect-error experimental pyodide
31-
const pyodide = await window.loadPyodide({
32-
stdout: (data: string) => stdOutAndErr.push(data),
33-
stderr: (data: string) => stdOutAndErr.push(data),
34-
});
35-
try {
36-
const result = await pyodide.runPythonAsync(code);
37-
if (result) {
38-
stdOutAndErr.push(result.toString());
5+
import { PlayIcon, StopIcon } from '@heroicons/react/24/outline';
6+
7+
const canInterrupt = typeof SharedArrayBuffer === 'function';
8+
9+
// adapted from https://pyodide.org/en/stable/usage/webworker.html
10+
const WORKER_CODE = `
11+
importScripts("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js");
12+
13+
let stdOutAndErr = [];
14+
15+
let pyodideReadyPromise = loadPyodide({
16+
stdout: (data) => stdOutAndErr.push(data),
17+
stderr: (data) => stdOutAndErr.push(data),
18+
});
19+
20+
let alreadySetBuff = false;
21+
22+
self.onmessage = async (event) => {
23+
stdOutAndErr = [];
24+
25+
// make sure loading is done
26+
const pyodide = await pyodideReadyPromise;
27+
const { id, python, context, interruptBuffer } = event.data;
28+
29+
if (interruptBuffer && !alreadySetBuff) {
30+
pyodide.setInterruptBuffer(interruptBuffer);
31+
alreadySetBuff = true;
32+
}
33+
34+
// Now load any packages we need, run the code, and send the result back.
35+
await pyodide.loadPackagesFromImports(python);
36+
37+
// make a Python dictionary with the data from content
38+
const dict = pyodide.globals.get("dict");
39+
const globals = dict(Object.entries(context));
40+
try {
41+
self.postMessage({ id, running: true });
42+
// Execute the python code in this context
43+
const result = pyodide.runPython(python, { globals });
44+
self.postMessage({ result, id, stdOutAndErr });
45+
} catch (error) {
46+
self.postMessage({ error: error.message, id });
47+
}
48+
interruptBuffer[0] = 0;
49+
};
50+
`;
51+
52+
let worker: Worker;
53+
const interruptBuffer = canInterrupt
54+
? new Uint8Array(new SharedArrayBuffer(1))
55+
: null;
56+
57+
const runCodeInWorker = (
58+
pyCode: string,
59+
callbackRunning: () => void
60+
): {
61+
donePromise: Promise<string>;
62+
interrupt: () => void;
63+
} => {
64+
if (!worker) {
65+
worker = new Worker(
66+
URL.createObjectURL(new Blob([WORKER_CODE], { type: 'text/javascript' }))
67+
);
68+
}
69+
const id = Math.random() * 1e8;
70+
const context = {};
71+
if (interruptBuffer) {
72+
interruptBuffer[0] = 0;
73+
}
74+
75+
const donePromise = new Promise<string>((resolve) => {
76+
worker.onmessage = (event) => {
77+
const { error, stdOutAndErr, running } = event.data;
78+
if (id !== event.data.id) return;
79+
if (running) {
80+
callbackRunning();
81+
return;
82+
} else if (error) {
83+
resolve(error.toString());
84+
} else {
85+
resolve(stdOutAndErr.join('\n'));
3986
}
40-
} catch (e) {
41-
console.error(e);
42-
stdOutAndErr.push((e as Error).toString());
87+
};
88+
worker.postMessage({ id, python: pyCode, context, interruptBuffer });
89+
});
90+
91+
const interrupt = () => {
92+
console.log('Interrupting...');
93+
console.trace();
94+
if (interruptBuffer) {
95+
interruptBuffer[0] = 2;
4396
}
44-
return stdOutAndErr.join('\n');
45-
},
46-
};
97+
};
4798

48-
if (StorageUtils.getConfig().pyIntepreterEnabled) {
49-
PyodideWrapper.load();
50-
}
99+
return { donePromise, interrupt };
100+
};
51101

52102
export default function CanvasPyInterpreter() {
53103
const { canvasData, setCanvasData } = useAppContext();
54104

55105
const [code, setCode] = useState(canvasData?.content ?? ''); // copy to avoid direct mutation
56106
const [running, setRunning] = useState(false);
57107
const [output, setOutput] = useState('');
108+
const [interruptFn, setInterruptFn] = useState<() => void>();
109+
const [showStopBtn, setShowStopBtn] = useState(false);
58110

59111
const runCode = async (pycode: string) => {
112+
interruptFn?.();
60113
setRunning(true);
61-
setOutput('Running...');
62-
const out = await PyodideWrapper.run(pycode);
114+
setOutput('Loading Pyodide...');
115+
const { donePromise, interrupt } = runCodeInWorker(pycode, () => {
116+
setOutput('Running...');
117+
setShowStopBtn(canInterrupt);
118+
});
119+
setInterruptFn(() => interrupt);
120+
const out = await donePromise;
63121
setOutput(out);
64122
setRunning(false);
123+
setShowStopBtn(false);
65124
};
66125

67126
// run code on mount
@@ -98,9 +157,21 @@ export default function CanvasPyInterpreter() {
98157
onClick={() => runCode(code)}
99158
disabled={running}
100159
>
101-
<PlayIcon className="h-6 w-6" />{' '}
102-
{running ? 'Running...' : 'Run'}
160+
<PlayIcon className="h-6 w-6" /> Run
103161
</button>
162+
{showStopBtn && (
163+
<button
164+
className="btn btn-sm bg-base-100 ml-2"
165+
onClick={() => interruptFn?.()}
166+
>
167+
<StopIcon className="h-6 w-6" /> Stop
168+
</button>
169+
)}
170+
<span className="grow text-right text-xs">
171+
<OpenInNewTab href="https://github.com/ggerganov/llama.cpp/issues/11762">
172+
Report a bug
173+
</OpenInNewTab>
174+
</span>
104175
</div>
105176
<textarea
106177
className="textarea textarea-bordered h-full dark-color"

examples/server/webui/src/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ import App from './App.tsx';
55

66
createRoot(document.getElementById('root')!).render(
77
<StrictMode>
8-
<App />
8+
<App />
99
</StrictMode>
1010
);

examples/server/webui/vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,9 @@ export default defineConfig({
7272
proxy: {
7373
'/v1': 'http://localhost:8080',
7474
},
75+
headers: {
76+
'Cross-Origin-Embedder-Policy': 'require-corp',
77+
'Cross-Origin-Opener-Policy': 'same-origin',
78+
},
7579
},
7680
});

0 commit comments

Comments
 (0)