Skip to content

Commit 01a4405

Browse files
authored
Cache pyodide modules (#1113)
Refactored the `PyodideWorker` so that `pyodide` is not completely reloaded on each code run, but rather the globals representing user-defined variables and user-imported modules are cleared. This allows the modules that have already been loaded into `pyodide` to be cached, massively reducing the latency on subsequent code runs.
1 parent 9be0e5d commit 01a4405

File tree

4 files changed

+74
-22
lines changed

4 files changed

+74
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1919
- Tests for running simple programs in `pyodide` and `skulpt` (#1100)
2020
- Fall back to `skulpt` if the host is not `crossOriginIsolated` (#1107)
2121
- `Pyodide` `seaborn` support (#1106)
22+
- `Pyodide` module caching (#1113)
2223

2324
### Changed
2425

cypress/e2e/spec-wc-pyodide.cy.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,43 @@ describe("Running the code with pyodide", () => {
143143
"ModuleNotFoundError: No module named 'i_do_not_exist' on line 1 of main.py",
144144
);
145145
});
146+
147+
it("clears user-defined variables between code runs", () => {
148+
runCode("a = 1\nprint(a)");
149+
cy.get("editor-wc")
150+
.shadow()
151+
.find(".pythonrunner-console-output-line")
152+
.should("contain", "1");
153+
runCode("print(a)");
154+
cy.get("editor-wc")
155+
.shadow()
156+
.find(".error-message__content")
157+
.should("contain", "NameError: name 'a' is not defined");
158+
});
159+
160+
it("clears user-defined functions between code runs", () => {
161+
runCode("def my_function():\n\treturn 1\nprint(my_function())");
162+
cy.get("editor-wc")
163+
.shadow()
164+
.find(".pythonrunner-console-output-line")
165+
.should("contain", "1");
166+
runCode("print(my_function())");
167+
cy.get("editor-wc")
168+
.shadow()
169+
.find(".error-message__content")
170+
.should("contain", "NameError: name 'my_function' is not defined");
171+
});
172+
173+
it("clears user-imported modules between code runs", () => {
174+
runCode("import math\nprint(math.floor(math.pi))");
175+
cy.get("editor-wc")
176+
.shadow()
177+
.find(".pythonrunner-console-output-line")
178+
.should("contain", "3");
179+
runCode("print(math.floor(math.pi))");
180+
cy.get("editor-wc")
181+
.shadow()
182+
.find(".error-message__content")
183+
.should("contain", "NameError: name 'math' is not defined");
184+
});
146185
});

src/PyodideWorker.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,7 @@ const PyodideWorker = () => {
6565
await pyodide.runPythonAsync(`
6666
import pyodide_http
6767
pyodide_http.patch_all()
68-
69-
old_input = input
70-
71-
def patched_input(prompt=False):
72-
if (prompt):
73-
print(prompt)
74-
return old_input()
75-
76-
__builtins__.input = patched_input
77-
`);
68+
`);
7869

7970
try {
8071
await withSupportForPackages(python, async () => {
@@ -87,7 +78,7 @@ const PyodideWorker = () => {
8778
postMessage({ method: "handleError", ...parsePythonError(error) });
8879
}
8980

90-
await reloadPyodideToClearState();
81+
await clearPyodideData();
9182
};
9283

9384
const checkIfStopped = () => {
@@ -241,12 +232,12 @@ const PyodideWorker = () => {
241232
pyodide.runPython(`
242233
import js
243234
244-
class DummyDocument:
235+
class __DummyDocument__:
245236
def __init__(self, *args, **kwargs) -> None:
246237
return
247238
def __getattr__(self, __name: str):
248-
return DummyDocument
249-
js.document = DummyDocument()
239+
return __DummyDocument__
240+
js.document = __DummyDocument__()
250241
`);
251242
await pyodide.loadPackage("matplotlib")?.catch(() => {});
252243
let pyodidePackage;
@@ -334,7 +325,18 @@ const PyodideWorker = () => {
334325
},
335326
};
336327

337-
const reloadPyodideToClearState = async () => {
328+
const clearPyodideData = async () => {
329+
postMessage({ method: "handleLoading" });
330+
await pyodide.runPythonAsync(`
331+
# Clear all user-defined variables and modules
332+
for name in dir():
333+
if not name.startswith('_'):
334+
del globals()[name]
335+
`);
336+
postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
337+
};
338+
339+
const initialisePyodide = async () => {
338340
postMessage({ method: "handleLoading" });
339341

340342
pyodidePromise = loadPyodide({
@@ -346,6 +348,15 @@ const PyodideWorker = () => {
346348

347349
pyodide = await pyodidePromise;
348350

351+
await pyodide.runPythonAsync(`
352+
__old_input__ = input
353+
def __patched_input__(prompt=False):
354+
if (prompt):
355+
print(prompt)
356+
return __old_input__()
357+
__builtins__.input = __patched_input__
358+
`);
359+
349360
if (supportsAllFeatures) {
350361
stdinBuffer =
351362
stdinBuffer || new Int32Array(new SharedArrayBuffer(1024 * 1024)); // 1 MiB
@@ -409,7 +420,7 @@ const PyodideWorker = () => {
409420
return { file, line, mistake, type, info };
410421
};
411422

412-
reloadPyodideToClearState();
423+
initialisePyodide();
413424

414425
return {
415426
postMessage,

src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideWorker.test.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe("PyodideWorker", () => {
9696
},
9797
});
9898
expect(pyodide.runPythonAsync).toHaveBeenCalledWith(
99-
expect.stringMatching(/__builtins__.input = patched_input/),
99+
expect.stringMatching(/__builtins__.input = __patched_input__/),
100100
);
101101
});
102102

@@ -190,17 +190,18 @@ describe("PyodideWorker", () => {
190190
});
191191
});
192192

193-
test("it reloads pyodide after running the code", async () => {
194-
global.loadPyodide.mockClear();
193+
test("it clears the pyodide variables after running the code", async () => {
195194
await worker.onmessage({
196195
data: {
197196
method: "runPython",
198197
python: "print('hello')",
199198
},
200199
});
201-
await waitFor(() => {
202-
expect(global.loadPyodide).toHaveBeenCalled();
203-
});
200+
await waitFor(() =>
201+
expect(pyodide.runPythonAsync).toHaveBeenCalledWith(
202+
expect.stringContaining("del globals()[name]"),
203+
),
204+
);
204205
});
205206

206207
test("it handles stopping by notifying component of an error", async () => {

0 commit comments

Comments
 (0)