mirror of https://github.com/pyodide/pyodide.git
190 lines
7.3 KiB
Markdown
190 lines
7.3 KiB
Markdown
(using_from_webworker)=
|
|
# Using Pyodide from a web worker
|
|
|
|
This document describes how to use pyodide to execute python scripts
|
|
asynchronously in a web worker.
|
|
|
|
## Startup
|
|
|
|
Setup your project to serve `webworker.js`. You should also serve
|
|
`pyodide.js`, and all its associated `.asm.js`, `.data`, `.json`, and `.wasm`
|
|
files as well, though this is not strictly required if `pyodide.js` is pointing
|
|
to a site serving current versions of these files.
|
|
The simplest way to serve the required files is to use a CDN,
|
|
such as `https://cdn.jsdelivr.net/pyodide`. This is the solution
|
|
presented here.
|
|
|
|
Update the `webworker.js` sample so that it has as valid URL for `pyodide.js`, and sets
|
|
`self.languagePluginUrl` to the location of the supporting files.
|
|
|
|
In your application code create a web worker `new Worker(...)`,
|
|
and attach listeners to it using its `.onerror` and [`.onmessage`][onmessage]
|
|
methods (listeners).
|
|
|
|
Communication from the worker to the main thread is done via the `Worker.postMessage()`
|
|
method (and vice versa).
|
|
|
|
[worker onmessage]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Sending_messages_to_and_from_a_dedicated_worker
|
|
[worker onerror]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Handling_errors
|
|
|
|
## Detailed example
|
|
|
|
In this example process we will have three parties involved:
|
|
|
|
* The **service worker** is responsible for running scripts in its own thread separate thread.
|
|
* The **worker API** exposes a consumer-to-provider communication interface.
|
|
* The **consumer**s want to run some scripts outside the main thread so they don't block the main thread.
|
|
|
|
### Consumers
|
|
|
|
Our goal is to run some javascript code in another thread, this other thread will
|
|
not have access to the main thread objects. Therefore we will need an API that takes
|
|
as input not only the python `script` we wan to run, but also the `context` on which
|
|
it relies (some javascript variables that we would normally get access to if we
|
|
were running the python script in the main thread). Let's first describe what API
|
|
we would like to have.
|
|
|
|
Here is an example of consumer that will exchange with the , via the worker
|
|
interface/API `py-worker.js` to run the following python `script` using the provided `context`
|
|
using a function called `asyncRun()`.
|
|
|
|
```js
|
|
import { asyncRun } from './py-worker';
|
|
|
|
const script = `
|
|
import statistics
|
|
from js import A_rank
|
|
statistics.stdev(A_rank)
|
|
`;
|
|
|
|
const context = {
|
|
A_rank: [0.8, 0.4, 1.2, 3.7, 2.6, 5.8],
|
|
}
|
|
|
|
async function main(){
|
|
try {
|
|
const {results, error} = await asyncRun(script, context);
|
|
if (results) {
|
|
console.log('pyodideWorker return results: ', results);
|
|
} else if (error) {
|
|
console.log('pyodideWorker error: ', error);
|
|
}
|
|
}
|
|
catch (e){
|
|
console.log(`Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}`)
|
|
}
|
|
}
|
|
|
|
main();
|
|
```
|
|
|
|
Before writing the API, lets first have a look at how the worker operates.
|
|
How does our service worker will run the `script` using a given `context`.
|
|
|
|
### Service worker
|
|
|
|
[A worker][worker API] is ...
|
|
|
|
> A worker is an object created using a constructor (e.g. [Worker()][Worker constructor]) that runs a named JavaScript file — this file contains the code that will run in the worker thread; workers run in another global context that is different from the current window. This context is represented by either a DedicatedWorkerGlobalScope object (in the case of dedicated workers - workers that are utilized by a single script), or a SharedWorkerGlobalScope (in the case of shared workers - workers that are shared between multiple scripts).
|
|
|
|
In our case we will use a single worker to execute python code without interfering with
|
|
client side rendering (which is done by the main javascript thread). The worker does
|
|
two things:
|
|
|
|
1. Listen on new messages from the main thread
|
|
2. Respond back once it finished executing the python script
|
|
|
|
These are the required tasks it should fulfill, but it can do other things.
|
|
For example, to always load packages `numpy` and `pytz`, you would insert the
|
|
lines `pythonLoading = self.pyodide.loadPackage(['numpy', 'pytz'])` and
|
|
`await pythonLoading;` as shown below:
|
|
|
|
```js
|
|
// webworker.js
|
|
|
|
// Setup your project to serve `py-worker.js`. You should also serve
|
|
// `pyodide.js`, and all its associated `.asm.js`, `.data`, `.json`,
|
|
// and `.wasm` files as well:
|
|
self.languagePluginUrl = 'https://cdn.jsdelivr.net/pyodide/v0.15.0/full/';
|
|
importScripts('https://cdn.jsdelivr.net/pyodide/v0.15.0/full/pyodide.js');
|
|
|
|
let pythonLoading;
|
|
async function loadPythonPackages(){
|
|
await languagePluginLoader;
|
|
pythonLoading = self.pyodide.loadPackage(['numpy', 'pytz']);
|
|
}
|
|
|
|
var onmessage = async(event) => {
|
|
await languagePluginLoader;
|
|
// since loading package is asynchronous, we need to make sure loading is done:
|
|
await pythonLoading;
|
|
// Don't bother yet with this line, suppose our API is built in such a way:
|
|
const {python, ...context} = event.data;
|
|
// The worker copies the context in its own "memory" (an object mapping name to values)
|
|
for (const key of Object.keys(context)){
|
|
self[key] = context[key];
|
|
}
|
|
// Now is the easy part, the one that is similar to working in the main thread:
|
|
try {
|
|
self.postMessage({
|
|
results: await self.pyodide.runPythonAsync(python)
|
|
});
|
|
}
|
|
catch (error){
|
|
self.postMessage(
|
|
{error : error.message}
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### The worker API
|
|
|
|
Now that we established what the two sides need and how they operate,
|
|
lets connect them using this simple API (`py-worker.js`). This part is
|
|
optional and only a design choice, you could achieve similar results
|
|
by exchanging message directly between your main thread and the webworker.
|
|
You would just need to call `.postMessages()` with the right arguments as
|
|
this API does.
|
|
|
|
```js
|
|
const pyodideWorker = new Worker('./build/webworker.js')
|
|
|
|
export function run(script, context, onSuccess, onError){
|
|
pyodideWorker.onerror = onError;
|
|
pyodideWorker.onmessage = (e) => onSuccess(e.data);
|
|
pyodideWorker.postMessage({
|
|
...context,
|
|
python: script,
|
|
});
|
|
}
|
|
|
|
// Transform the run (callback) form to a more modern async form.
|
|
// This is what allows to write:
|
|
// const {results, error} = await asyncRun(script, context);
|
|
// Instead of:
|
|
// run(script, context, successCallback, errorCallback);
|
|
export function asyncRun(script, context) {
|
|
return new Promise(function(onSuccess, onError) {
|
|
run(script, context, onSuccess, onError);
|
|
});
|
|
}
|
|
```
|
|
|
|
[worker API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
|
[Worker constructor]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker
|
|
|
|
## Caveats
|
|
|
|
Using a web worker is advantageous because the python code is run in a separate
|
|
thread from your main UI, and hence does not impact your application's
|
|
responsiveness.
|
|
There are some limitations, however.
|
|
At present, Pyodide does not support sharing the Python interpreter and
|
|
packages between multiple web workers or with your main thread.
|
|
Since web workers are each in their own virtual machine, you also cannot share
|
|
globals between a web worker and your main thread.
|
|
Finally, although the web worker is separate from your main thread,
|
|
the web worker is itself single threaded, so only one python script will
|
|
execute at a time.
|