starboard-python
Version:
Python cells for Starboard Notebook
162 lines (148 loc) • 5.05 kB
text/typescript
/**
* One-way memory, can block a web worker until data from the main thread arrives.
*
* Web Worker Usage:
* 1. Lock "web worker"
* 2. Set "shared memory signal"
* 3. Notify main thread (Main thread does stuff)
* 4. Wait for "shared memory signal"
* 5. Read size buffer
* 6. Read shared memory
* 7. If the size buffer was bigger than the read memory size
* 7.1. Set "shared memory signal"
* 7.2. Notify main thread (Main thread writes remaining data to shared memory)
* 7.3. Wait for "shared memory signal"
* 7.4. Read shared memory
* 7.5. Go back to step 7. (loop)
* 8. Unlock "web worker"
*
* Main Thread Usage:
* 1. Get notification
* 2. Do operations
* 3. Serialize data
* 4. Write size into the size buffer
* 5. Write partial data into shared memory
* 6. Unlock "shared memory signal" (Worker does stuff)
* 7. If not everything has been written to the shared memory yet
* 7.1. Get notification
* 7.2. Write partial data into shared memory
* 7.3. Unlock "shared memory signal" (Worker does stuff)
* 7.4. Go back to step 7. (loop)
*/
export class AsyncMemory {
// Reference: https://v8.dev/features/atomics
static LOCK_WORKER_INDEX = 0;
static LOCK_SIZE_INDEX = 2;
static SIZE_INDEX = 4;
static UNLOCKED = 0;
static LOCKED = 1;
readonly sharedLock: SharedArrayBuffer;
readonly lockAndSize: Int32Array;
readonly sharedMemory: SharedArrayBuffer;
readonly memory: Uint8Array;
constructor(sharedLock?: SharedArrayBuffer, sharedMemory?: SharedArrayBuffer) {
this.sharedLock = sharedLock ?? new SharedArrayBuffer(8 * Int32Array.BYTES_PER_ELEMENT);
this.lockAndSize = new Int32Array(this.sharedLock);
if (this.lockAndSize.length < 8) {
throw new Error("Expected an sharedLock with at least 8x32 bytes");
}
this.sharedMemory = sharedMemory ?? new SharedArrayBuffer(1024);
this.memory = new Uint8Array(this.sharedMemory);
if (this.sharedMemory.byteLength < 1024) {
throw new Error("Expected an sharedMemory with at least 1024 bytes");
}
}
/**
* Should be called from the worker thread
*/
lockWorker() {
const oldValue = Atomics.compareExchange(
this.lockAndSize,
AsyncMemory.LOCK_WORKER_INDEX,
AsyncMemory.UNLOCKED, // old value
AsyncMemory.LOCKED // new value
);
if (oldValue !== AsyncMemory.UNLOCKED) {
throw new Error(`Cannot lock worker, the worker has to be unlocked ${AsyncMemory.UNLOCKED} !== ${oldValue}`);
}
}
/**
* Should be called from the worker thread
*/
lockSize() {
const oldValue = Atomics.compareExchange(
this.lockAndSize,
AsyncMemory.LOCK_SIZE_INDEX,
AsyncMemory.UNLOCKED, // old value
AsyncMemory.LOCKED // new value
);
if (oldValue !== AsyncMemory.UNLOCKED) {
throw new Error(`Cannot set size flag, the size has to be unlocked ${AsyncMemory.UNLOCKED} !== ${oldValue}`);
}
}
/**
* Only legal if the worker is locked
*/
waitForSize() {
Atomics.wait(this.lockAndSize, AsyncMemory.LOCK_SIZE_INDEX, AsyncMemory.LOCKED);
}
/**
* Should be called from the main thread!
* Only legal if the worker is locked and the size is locked
*/
writeSize(value: number) {
return Atomics.store(this.lockAndSize, AsyncMemory.SIZE_INDEX, value);
}
/**
* Only legal if the worker is locked but the size is not
*/
readSize(): number {
return Atomics.load(this.lockAndSize, AsyncMemory.SIZE_INDEX);
}
/**
* Should be called from the main thread!
*/
unlockSize() {
const oldValue = Atomics.compareExchange(
this.lockAndSize,
AsyncMemory.LOCK_SIZE_INDEX,
AsyncMemory.LOCKED, // old value
AsyncMemory.UNLOCKED // new value
);
if (oldValue != AsyncMemory.LOCKED) {
throw new Error("Tried to unlock, but was already unlocked");
}
Atomics.notify(this.lockAndSize, AsyncMemory.LOCK_SIZE_INDEX);
}
/**
* Ensures that the size gets unlocked
*/
forceUnlockSize() {
const oldValue = Atomics.compareExchange(
this.lockAndSize,
AsyncMemory.LOCK_SIZE_INDEX,
AsyncMemory.LOCKED, // old value
AsyncMemory.UNLOCKED // new value
);
if (oldValue != AsyncMemory.LOCKED) {
// And force unlock it
Atomics.store(this.lockAndSize, AsyncMemory.LOCK_SIZE_INDEX, AsyncMemory.UNLOCKED);
}
Atomics.notify(this.lockAndSize, AsyncMemory.LOCK_SIZE_INDEX);
}
/**
* Should be called from the worker thread!
*/
unlockWorker() {
const oldValue = Atomics.compareExchange(
this.lockAndSize,
AsyncMemory.LOCK_WORKER_INDEX,
AsyncMemory.LOCKED, // old value
AsyncMemory.UNLOCKED // new value
);
if (oldValue != AsyncMemory.LOCKED) {
throw new Error("Tried to unlock, but was already unlocked");
}
Atomics.notify(this.lockAndSize, AsyncMemory.LOCK_WORKER_INDEX);
}
}