Skip to content

Commit

Permalink
workers - throw on more post failures, configure queue length & timeo…
Browse files Browse the repository at this point in the history
…ut, decouple debugger messages #1046
  • Loading branch information
phoddie committed Mar 16, 2023
1 parent e8441c9 commit dccc1bb
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 47 deletions.
51 changes: 37 additions & 14 deletions documentation/base/worker.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Worker
Copyright 2018-2020 Moddable Tech, Inc.<BR>
Revised: December 30, 2020
Copyright 2018-2023 Moddable Tech, Inc.<BR>
Revised: March 16, 2023

The Moddable runtime integrates with XS to allow a multiple virtual machines to co-exist on a single microcontroller. The majority of projects use only a single virtual machine. However, there are situations where the several independent runtime contexts provided by having several virtual machines is advantageous. This isolation is useful to fully separate a particular set of scripts, for example user installed modules, from the core project functionality for security, privacy, and reliability reasons. Another useful situation is to allow scripts to perform blocking operations in one virtual machine while scripts in another virtual machine remain fully responsive.
The Moddable runtime integrates with XS to allow a multiple virtual machines to co-exist on a single microcontroller. The majority of projects use only a single virtual machine. However, there are situations where the several independent runtime contexts provided by having several virtual machines is advantageous. This isolation is useful to fully separate a particular set of scripts, for example user installed modules, from the core project functionality for security, privacy, and reliability reasons. Another useful situation is to allow scripts to perform blocking operations in one virtual machine while scripts in another virtual machine remain fully responsive. On microcontrollers with multiple CPU cores, workers can execute in parallel to take full advantage of the available CPU power.

Undertake the use of multiple virtual machines in a project with care. Each virtual machine requires additional RAM, and RAM is the most limited resource on most microcontroller deployments. In addition, the asynchronous nature of communication between virtual machines adds complexity to the overall system. Still, having multiple virtual machines is useful, even essential, in some circumstances. The remainder of this document describes how to use multiple virtual machines with the Moddable SDK together with some implementation details.

Expand All @@ -12,10 +12,11 @@ The `Worker` class is an API for working with virtual machines. The implementati
- The implementation is a small subset of the Web Workers API.
- Workers are always launched from a module, never from a script file.
- One addition has been made to specify the memory configuration of a new worker.
- Posting a message to a worker throws in additional situations.

Those familiar with Web Workers are strongly advised to read this document to understand whether the implementation differences are relevant to their use of workers.
Those familiar with Web Workers are strongly advised to read this document to understand the implementation differences that may be relevant to their use of workers.

This document contains a standalone description of the `Worker` class implemented in the Moddable SDK, without reference to the Web Worker specification. The [`worker` example](../../examples/base/worker/) is a simple example of using the `Worker` class.
This document contains a standalone description of the `Worker` class implemented in the Moddable SDK. The [`worker` example](../../examples/base/worker/) is a simple example of using the `Worker` class.

## class Worker
Scripts import the `Worker` class to be able to create a new worker.
Expand All @@ -27,28 +28,32 @@ To launch a worker, create an instance of the `Worker` class, passing the name o

let aWorker = new Worker("simpleworker");

The call to the `Worker` constructor returns only after execution of the specified module completes. If the worker module generates an exception during this step, an exception is propagated so that the call to `new Worker` throws an exception. This behavior means that the invoking virtual machine blocks until the new worker virtual machine has fully completely initialization. Consequently, an operations performed in a newly instantiated virtual machine should be relatively brief.
The call to the `Worker` constructor returns only after execution of the specified module completes. If the worker module generates an exception during this step, an exception is propagated so that the call to `new Worker` throws an exception. This behavior means that the invoking virtual machine blocks until the new worker virtual machine has fully completely initialization. Consequently, any operations performed in a newly instantiated virtual machine should be relatively brief.

### Launching a worker with memory configuration
The previous example launches the worker with the default memory configuration. This may not be large enough for the worker, or may allocate more RAM than needed by the worker. An optional configuration dictionary allows the script instantiating a new virtual machine to set the memory use.
The previous example launches the worker with the default memory configuration. This may not be large enough for the worker, or may allocate more RAM than needed by the worker. An optional configuration object allows the script instantiating a new virtual machine to set the memory use.

let aWorker = new Worker("simpleworker",
{allocation: 8192, stackCount: 64, slotCount: 64});

### Sending a message to a worker
Messages to workers are JavaScript objects and binary data. The JavaScript objects can be considered equivalent to JSON. The binary data is an `ArrayBuffer`. All messages are passed by copy, so the size of the message should be as small as practical.
Messages to workers are JavaScript objects and binary data. The JavaScript objects can be considered equivalent to JSON. The binary data is an `ArrayBuffer`.

aWorker.postMessage({hello: "world", count: 12});
aWorker.postMessage(new ArrayBuffer(12));

Internally, workers use the XS marshalling feature which supports sending some types of data that fail in browser implementations of workers. If an object cannot be serialized, `postMessage` throws an exception.

Messages are passed by copy, so the size of the message should be as small as practical. If the memory allocation fails, `postMessage` throws an exception.

### Receiving a message from a worker
The worker instance has an `onmessage` function which receives all messages from the worker. It is typically assigned immediately after the worker is constructed:

aWorker.onmessage = function(message) {
trace(message, "\n");
}

An alternative approach is to create a subclass of `Worker` which contains the `onmessage` function. This uses less memory and runs somewhat faster.
An alternative approach is to create a subclass of `Worker` which contains the `onmessage` function. This uses less memory and runs somewhat faster.

class MyWorker extends Worker {
onmessage(message) {
Expand All @@ -63,7 +68,7 @@ The script that instantiates a worker may terminate the worker.

aWorker.terminate();

Once a worker is terminated, no further calls should be made to the worker instance.
Once a worker is terminated, no further calls should be made to the worker instance. Attempting to post a message to a terminated work throws an exception.

### Worker script start-up
When the Worker constructor is called, the module at the path specified (`simpleworker` in the preceding examples) is loaded and run. The worker itself typically performs two tasks. The first is initialization and the second is installing a function to receive messages. The receiving function is installed on the global object `self` and is named `onmessage`.
Expand All @@ -85,7 +90,8 @@ A worker script terminates itself by calling `close` on the global object `self`

self.close()

### constructor(modulePath[, dictionary])
### API Reference
#### constructor(modulePath[, dictionary])
The `Worker` constructor takes a path to the module used to initialize the new worker instance.

let aWorker = new Worker("simpleworker");
Expand All @@ -99,14 +105,14 @@ The `allocation` property is the total memory shared by the new virtual machine

If an error occurs or an exception is thrown during execution of the module, the `Worker` constructor also throws an error.

### terminate()
#### terminate()
The `terminate` function immediately ends execution of the worker instance, freeing all resources owned by the worker.

aWorker.terminate();

Once a worker has been terminated, no further calls should be made to it.

### postMessage(msg)
#### postMessage(msg)
The `postMessage` function queues a message for delivery to the worker. Messages are either a JavaScript object, roughly equivalent to JSON objects, or an `ArrayBuffer`.

aWorker.postMessage("hello");
Expand All @@ -115,7 +121,7 @@ The `postMessage` function queues a message for delivery to the worker. Messages

Messages are passed by copy, so they should be in small in size as practical. Messages are delivered in the same order they were sent.

### onmessage property
#### onmessage property
The worker `onmessage` property contains a function which receives messages from the worker.

aWorker.onmessage = function(msg) {
Expand Down Expand Up @@ -150,3 +156,20 @@ The debugger for the XS virtual machine, `xsbug`, supports working with multiple

## Shared Memory and Atomics
The ECMAScript 2016 standard includes support for Shared Memory and Atomics. These are powerful tools for efficient communication between virtual machines. The XS virtual machine fully implements these features. They are supported on some microcontrollers (ESP32) but not all (ESP8266).

## Configuration Options
The Web Workers implementation uses `modMessagePostToMachine()`, the native IPC facility of the Moddable SDK, to pass messages between threads. On ESP32 this is implemented using FreeRTOS queues.

By default the message queue has 10 elements. A message is posted while the queue is full blocks until space become available in the queue. This behavior generally works well as the number of messages being posted is relatively infrequent. If many messages are being sent between the sender and receiver, a deadlock is possible. Two build options are available in the manifest to help if necessary.

```json
"defines": {
"task": {
"queueLength": 20,
"queueWait": 100
}
}
```
The `queueLength` property changes the size of the message queues to the value specified. The `queueWait` property allows posting messages to fail after the specified timeout (given in milliseconds). If a message cannot be enqueued after this timeout period, `postMessage` throws an exception.

By default, a debug build sets `queueWait` to 1000 milliseconds. In a well-balanced system, messages should enqueue instantaneously and certainly shouldn't block for more than a few millisecond. This default allows debugging of potential queue related issues by throwing instead of deadlocking when message sends take unexpectedly long. By default, release and instrumented builds have an infinite wait for `queueWait` and so never time out.
43 changes: 36 additions & 7 deletions modules/base/worker/modWorker.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ static void workerDeliverConnect(xsMachine *the, modWorker worker, uint8_t *mess
static int workerStart(modWorker worker);
static void workerTerminate(xsMachine *the, modWorker worker, uint8_t *message, uint16_t messageLength);

static void doModMessagePostToMachine(xsMachine *the, xsMachine *targetThe, uint8_t *message, modMessageDeliver callback, void *refcon);

static modWorker gWorkers;

static void xs_emptyworker_destructor(void *data) {
Expand Down Expand Up @@ -182,7 +184,7 @@ static void workerConstructor(xsMachine *the, xsBooleanValue shared)
#ifdef INC_FREERTOS_H
worker->task = target->task;
#endif
modMessagePostToMachine(worker->the, NULL, 0, (modMessageDeliver)workerDeliverConnect, worker);
doModMessagePostToMachine(the, worker->the, NULL, (modMessageDeliver)workerDeliverConnect, worker);
goto done;
}
}
Expand Down Expand Up @@ -287,8 +289,7 @@ void xs_worker_postfrominstantiator(xsMachine *the)
xsUnknownError("worker closing");

message = xsMarshall(xsArg(0));
if (modMessagePostToMachine(worker->the, (uint8_t *)&message, sizeof(message), (modMessageDeliver)workerDeliverMarshall, worker))
xsUnknownError("post from instantiator failed");
doModMessagePostToMachine(the, worker->the, message, (modMessageDeliver)workerDeliverMarshall, worker);
}

void xs_worker_postfromworker(xsMachine *the)
Expand All @@ -300,20 +301,21 @@ void xs_worker_postfromworker(xsMachine *the)
xsUnknownError("worker closing");

message = xsMarshall(xsArg(0));
if (modMessagePostToMachine(worker->parent, (uint8_t *)&message, sizeof(message), (modMessageDeliver)workerDeliverMarshall, worker))
xsUnknownError("post from worker failed");
doModMessagePostToMachine(the, worker->parent, message, (modMessageDeliver)workerDeliverMarshall, worker);
}

void xs_worker_close(xsMachine *the)
{
modWorker worker = xsmcGetHostDataValidate(xsThis, (void *)xs_emptyworker_destructor);
modMessagePostToMachine(worker->parent, NULL, 0, (modMessageDeliver)workerTerminate, worker);
doModMessagePostToMachine(the, worker->parent, NULL, (modMessageDeliver)workerTerminate, worker);
}

void workerDeliverMarshall(xsMachine *the, modWorker worker, uint8_t *message, uint16_t messageLength)
{
if (worker->closing)
if (worker->closing) {
c_free(*(char **)message);
return;
}

xsBeginHost(the);

Expand Down Expand Up @@ -419,6 +421,33 @@ void workerTerminate(xsMachine *the, modWorker worker, uint8_t *message, uint16_
xs_worker_destructor(worker);
}

void doModMessagePostToMachine(xsMachine *the, xsMachine *targetThe, uint8_t *message, modMessageDeliver callback, void *refcon)
{
char *error;
int result;

if (message) {
result = modMessagePostToMachine(targetThe, (uint8_t *)&message, sizeof(message), callback, refcon);
if (!result)
return;
c_free(message);
}
else {
result = modMessagePostToMachine(targetThe, NULL, 0, callback, refcon);
if (!result)
return;
}

if (-1 == result)
error = "no memory";
else if (-2 == result)
error = "timeout";
else
error = "unknown";

xsUnknownError(error);
}

#ifdef INC_FREERTOS_H

void workerLoop(void *pvParameter)
Expand Down
3 changes: 3 additions & 0 deletions tests/modules/base/worker/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const worker = new Worker("testworker", minimumOptions);

assert.throws(SyntaxError, () => worker.postMessage(), "postMessage requires 1 argument");
assert.throws(SyntaxError, () => worker.postMessage.call(new $TESTMC.HostObject, 0, 64), "postMessage with non-worker this");
assert.throws(TypeError, () => worker.postMessage({host: new $TESTMC.HostObject}), "postMessage rejects host objects");

let index = 0;
worker.postMessage(messages[index]);
Expand All @@ -52,10 +53,12 @@ worker.onmessage = function(reply) {
}
else if (7 === index) {
assert(actual instanceof ArrayBuffer, "expected ArrayBuffer instance");
assert.sameValue(actual.byteLength, 1, "expected ArrayBuffer.byteLength 1");
assert.sameValue((new Uint8Array(actual))[0], 1, "expected buffer[0] to be 1");
}
else if (8 === index) {
assert(actual instanceof Uint32Array, "expected Uint32Array instance");
assert.sameValue(actual.length, 1, "expected Uint32Array.length 1");
assert.sameValue(actual[0], 1, "expected buffer[0] to be 1");
}
else
Expand Down
Loading

0 comments on commit dccc1bb

Please sign in to comment.