mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-21 11:15:36 -05:00
Move pyodide to a web worker (#1333)
This PR adds support for optionally running pyodide in a web worker: - add a new option config.execution_thread, which can be `main` or `worker`. The default is `main` - improve the test machinery so that we run all tests twice, once for `main` and once for `worker` - add a new esbuild target which builds the code for the worker The support for workers is not complete and many features are still missing: there are 71 tests which are marked as `@skip_worker`, but we can fix them in subsequent PRs. The vast majority of tests fail because js.document is unavailable: for it to run transparently, we need the "auto-syncify" feature of synclink. Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com> Co-authored-by: Madhur Tandon <20173739+madhur-tandon@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import './styles/pyscript_base.css';
|
||||
|
||||
import { loadConfigFromElement } from './pyconfig';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { AppConfig, InterpreterConfig } from './pyconfig';
|
||||
import { InterpreterClient } from './interpreter_client';
|
||||
import { PluginManager, Plugin, PythonPlugin } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
@@ -59,16 +59,6 @@ throwHandler.serialize = new_error_transfer_handler;
|
||||
user scripts
|
||||
|
||||
8. initialize the rest of web components such as py-button, py-repl, etc.
|
||||
|
||||
More concretely:
|
||||
|
||||
- Points 1-4 are implemented sequentially in PyScriptApp.main().
|
||||
|
||||
- PyScriptApp.loadInterpreter adds a <script> tag to the document to initiate
|
||||
the download, and then adds an event listener for the 'load' event, which
|
||||
in turns calls PyScriptApp.afterInterpreterLoad().
|
||||
|
||||
- PyScriptApp.afterInterpreterLoad() implements all the points >= 5.
|
||||
*/
|
||||
|
||||
export let interpreter;
|
||||
@@ -173,6 +163,52 @@ export class PyScriptApp {
|
||||
logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2));
|
||||
}
|
||||
|
||||
_get_base_url(): string {
|
||||
// Note that this requires that pyscript is loaded via a <script>
|
||||
// tag. If we want to allow loading via an ES6 module in the future,
|
||||
// we need to think about some other strategy
|
||||
const elem = document.currentScript as HTMLScriptElement;
|
||||
const slash = elem.src.lastIndexOf('/');
|
||||
return elem.src.slice(0, slash);
|
||||
}
|
||||
|
||||
async _startInterpreter_main(interpreter_cfg: InterpreterConfig) {
|
||||
logger.info('Starting the interpreter in the main thread');
|
||||
// this is basically equivalent to worker_initialize()
|
||||
const remote_interpreter = new RemoteInterpreter(interpreter_cfg.src);
|
||||
const { port1, port2 } = new Synclink.FakeMessageChannel() as unknown as MessageChannel;
|
||||
port1.start();
|
||||
port2.start();
|
||||
Synclink.expose(remote_interpreter, port2);
|
||||
const wrapped_remote_interpreter = Synclink.wrap(port1);
|
||||
|
||||
this.logStatus(`Downloading ${interpreter_cfg.name}...`);
|
||||
/* Dynamically download and import pyodide: the import() puts a
|
||||
loadPyodide() function into globalThis, which is later called by
|
||||
RemoteInterpreter.
|
||||
|
||||
This is suboptimal: ideally, we would like to import() a module
|
||||
which exports loadPyodide(), but this plays badly with workers
|
||||
because at the moment of writing (2023-03-24) Firefox does not
|
||||
support ES modules in workers:
|
||||
https://caniuse.com/mdn-api_worker_worker_ecmascript_modules
|
||||
*/
|
||||
const interpreterURL = interpreter_cfg.src;
|
||||
await import(interpreterURL);
|
||||
return { remote_interpreter, wrapped_remote_interpreter };
|
||||
}
|
||||
|
||||
async _startInterpreter_worker(interpreter_cfg: InterpreterConfig) {
|
||||
logger.warn('execution_thread = "worker" is still VERY experimental, use it at your own risk');
|
||||
logger.info('Starting the interpreter in a web worker');
|
||||
const base_url = this._get_base_url();
|
||||
const worker = new Worker(base_url + '/interpreter_worker.js');
|
||||
const worker_initialize: any = Synclink.wrap(worker);
|
||||
const wrapped_remote_interpreter = await worker_initialize(interpreter_cfg);
|
||||
const remote_interpreter = undefined; // this is _unwrapped_remote
|
||||
return { remote_interpreter, wrapped_remote_interpreter };
|
||||
}
|
||||
|
||||
// lifecycle (4)
|
||||
async loadInterpreter() {
|
||||
logger.info('Initializing interpreter');
|
||||
@@ -184,35 +220,21 @@ export class PyScriptApp {
|
||||
showWarning('Multiple interpreters are not supported yet.<br />Only the first will be used', 'html');
|
||||
}
|
||||
|
||||
const interpreter_cfg = this.config.interpreters[0];
|
||||
const cfg = this.config.interpreters[0];
|
||||
let x;
|
||||
if (this.config.execution_thread == 'worker') {
|
||||
x = await this._startInterpreter_worker(cfg);
|
||||
} else {
|
||||
x = await this._startInterpreter_main(cfg);
|
||||
}
|
||||
const { remote_interpreter, wrapped_remote_interpreter } = x;
|
||||
|
||||
const remote_interpreter = new RemoteInterpreter(interpreter_cfg.src);
|
||||
const { port1, port2 } = new Synclink.FakeMessageChannel() as unknown as MessageChannel;
|
||||
port1.start();
|
||||
port2.start();
|
||||
Synclink.expose(remote_interpreter, port2);
|
||||
const wrapped_remote_interpreter = Synclink.wrap(port1);
|
||||
this.interpreter = new InterpreterClient(
|
||||
this.config,
|
||||
this._stdioMultiplexer,
|
||||
wrapped_remote_interpreter as Synclink.Remote<RemoteInterpreter>,
|
||||
remote_interpreter,
|
||||
);
|
||||
|
||||
this.logStatus(`Downloading ${interpreter_cfg.name}...`);
|
||||
|
||||
/* Dynamically download and import pyodide: the import() puts a
|
||||
loadPyodide() function into globalThis, which is later called by
|
||||
RemoteInterpreter.
|
||||
|
||||
This is suboptimal: ideally, we would like to import() a module
|
||||
which exports loadPyodide(), but this plays badly with workers
|
||||
because at the moment of writing (2023-03-24) Firefox does not
|
||||
support ES modules in workers:
|
||||
https://caniuse.com/mdn-api_worker_worker_ecmascript_modules
|
||||
*/
|
||||
const interpreterURL = await this.interpreter._remote.src;
|
||||
await import(interpreterURL);
|
||||
await this.afterInterpreterLoad(this.interpreter);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user