Skip to content

Commit

Permalink
Decorate web-renderer and generic render function with promises
Browse files Browse the repository at this point in the history
This allows guaranteed ordering of calls against the web-renderer
interface
  • Loading branch information
nick-thompson committed Feb 10, 2024
1 parent 0d2693e commit 4c19c2e
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 103 deletions.
42 changes: 32 additions & 10 deletions js/packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,23 @@ class Renderer {
updateNodeProps(this._delegate, node.hash, nodeMapCopy.props, newProps);
this._delegate.commitUpdates();

this._sendMessage(this._delegate.getPackedInstructions());
// Invoke message passing
const instructions = this._delegate.getPackedInstructions();

return Promise.resolve(this._sendMessage(instructions)).then((result) => {
if (result.success) {
// Pack render stats with result object
return Promise.resolve({
...result,
nodesAdded: this._delegate.nodesAdded,
edgesAdded: this._delegate.edgesAdded,
propsWritten: this._delegate.propsWritten,
elapsedTimeMs: t1 - t0,
});
}

return Promise.reject(result);
});
};

return [node, setter];
Expand All @@ -211,15 +227,21 @@ class Renderer {
const t1 = now();

// Invoke message passing
this._sendMessage(this._delegate.getPackedInstructions());

// Return render stats
return {
nodesAdded: this._delegate.nodesAdded,
edgesAdded: this._delegate.edgesAdded,
propsWritten: this._delegate.propsWritten,
elapsedTimeMs: t1 - t0,
};
const instructions = this._delegate.getPackedInstructions();
return Promise.resolve(this._sendMessage(instructions)).then((result) => {
if (result.success) {
// Pack render stats with result object
return Promise.resolve({
...result,
nodesAdded: this._delegate.nodesAdded,
edgesAdded: this._delegate.edgesAdded,
propsWritten: this._delegate.propsWritten,
elapsedTimeMs: t1 - t0,
});
}

return Promise.reject(result);
});
}
}

Expand Down
26 changes: 26 additions & 0 deletions js/packages/offline-renderer/__tests__/offline-renderer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,29 @@ test('child limit', async function() {
// Once this settles, we should see 100s everywhere
expect(outs[0].slice(512 * 8, 512 * 8 + 32)).toMatchSnapshot();
});

test('render stats', async function() {
let core = new OfflineRenderer();

await core.initialize({
numInputChannels: 0,
numOutputChannels: 1,
});

// Render a graph and get some valid stats back
let stats = await core.render(el.mul(2, 3));

expect(stats).toMatchObject({
success: true,
message: "Ok",
edgesAdded: 3,
nodesAdded: 4,
propsWritten: 3,
});

// Render with an invalid property and get a failure, rejecting the
// promise returned from core.render
await expect(core.render(el.const({value: 'hi'}))).rejects.toMatchObject({
success: false,
});
});
14 changes: 7 additions & 7 deletions js/packages/offline-renderer/elementary-wasm.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions js/packages/offline-renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ export default class OfflineRenderer extends EventEmitter {
}

this._renderer = new Renderer((batch) => {
this._native.postMessageBatch(batch, (type, message) => {
this.emit('error', new Error(`${type}: ${message}`));
});
return this._native.postMessageBatch(batch);
});
}

Expand Down
90 changes: 43 additions & 47 deletions js/packages/web-renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import WasmModule from './raw/elementary-wasm';
export default class WebAudioRenderer extends EventEmitter {
private _worklet: any;
private _promiseMap: any;
private _nextPromiseKey: number;
private _nextRequestId: number;
private _renderer: Renderer;
private _timer: any;

Expand Down Expand Up @@ -54,7 +54,7 @@ export default class WebAudioRenderer extends EventEmitter {
}

this._promiseMap = new Map();
this._nextPromiseKey = 0;
this._nextRequestId = 0;

this._worklet = new AudioWorkletNode(audioContext, `ElementaryAudioWorkletProcessor@${pkgVersion}`, Object.assign({
numberOfInputs: 0,
Expand All @@ -68,57 +68,69 @@ export default class WebAudioRenderer extends EventEmitter {
// actually created our Renderer instance here in time.
return await new Promise((resolve, reject) => {
this._worklet.port.onmessage = (e) => {
const [type, evt] = e.data;
const [type, payload] = e.data;

if (type === 'load') {
this._renderer = new Renderer((batch) => {
this._worklet.port.postMessage({
type: 'renderInstructions',
this._renderer = new Renderer(async (batch) => {
return await this._sendWorkletRequest('renderInstructions', {
batch,
});
});

resolve(this._worklet);
return this.emit(type, payload);
}

if (type === 'resolvePromise') {
const {promiseKey, result} = evt;
this._promiseMap.get(promiseKey).resolve(result);
this._promiseMap.delete(promiseKey);
return;
if (type === 'events') {
return payload.batch.forEach(({eventType, data}) => {
this.emit(eventType, data);
});
}

if (type === 'error') {
return this.emit(type, new Error(evt));
}
if (type === 'reply') {
const {requestId, result} = payload;
const {resolve, reject} = this._promiseMap.get(requestId);

if (type === 'eventBatch') {
return evt.forEach(({type, event}) => {
this.emit(type, event);
});
}
this._promiseMap.delete(requestId);

this.emit(type, evt);
return resolve(result);
}
};

// TODO: Clean up? Unsubscribe option?
this._timer = window.setInterval(() => {
this._worklet.port.postMessage({
type: 'processQueuedEvents',
requestType: 'processQueuedEvents',
});
}, eventInterval);
});
}

_sendWorkletRequest(requestType, payload) {
invariant(this._worklet, 'Can\'t send request before worklet is ready. Have you initialized your WebRenderer instance?');

let requestId = this._nextRequestId++;

this._worklet.port.postMessage({
requestId,
requestType,
payload,
});

return new Promise((resolve, reject) => {
this._promiseMap.set(requestId, { resolve, reject });
});
}

createRef(kind, props, children) {
return this._renderer.createRef(kind, props, children);
}

render(...args) {
return this._renderer.render(...args);
async render(...args) {
return await this._renderer.render(...args);
}

updateVirtualFileSystem(vfs) {
async updateVirtualFileSystem(vfs) {
const valid = typeof vfs === 'object' && vfs !== null;

invariant(valid, "Virtual file system must be an object mapping string type keys to Array|Float32Array type values");
Expand All @@ -130,36 +142,20 @@ export default class WebAudioRenderer extends EventEmitter {
invariant(validValue, "Virtual file system must be an object mapping string type keys to Array|Float32Array type values");
});

this._worklet.port.postMessage({
type: 'updateSharedResourceMap',
return await this._sendWorkletRequest('updateSharedResourceMap', {
resources: vfs,
});
}

pruneVirtualFileSystem() {
this._worklet.port.postMessage({
type: 'pruneVirtualFileSystem',
});
async pruneVirtualFileSystem() {
return await this._sendWorkletRequest('pruneVirtualFileSystem', {});
}

listVirtualFileSystem() {
const promiseKey = this._nextPromiseKey++;

this._worklet.port.postMessage({
type: 'listVirtualFileSystem',
promiseKey,
});

return new Promise((resolve, reject) => {
this._promiseMap.set(promiseKey, { resolve, reject });
});
async listVirtualFileSystem() {
return await this._sendWorkletRequest('listVirtualFileSystem', {});
}

reset() {
if (this._worklet) {
this._worklet.port.postMessage({
type: 'reset',
});
}
async reset() {
return await this._sendWorkletRequest('reset', {});
}
}
43 changes: 26 additions & 17 deletions js/packages/web-renderer/raw/WorkletProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,9 @@ class ElementaryAudioWorkletProcessor extends AudioWorkletProcessor {
}

this.port.onmessage = (e) => {
switch (e.data.type) {
case 'renderInstructions':
this._native.postMessageBatch(e.data.batch, (type, msg) => {
// This callback will only be called in the event of an error, we just relay
// it to the renderer frontend.
this.port.postMessage([type, msg]);
});
let {requestId, requestType, payload} = e.data;

break;
switch (requestType) {
case 'processQueuedEvents':
this._native.processQueuedEvents((evtBatch) => {
if (evtBatch.length > 0) {
Expand All @@ -89,26 +83,41 @@ class ElementaryAudioWorkletProcessor extends AudioWorkletProcessor {
});

break;
case 'renderInstructions':
return this.port.postMessage(['reply', {
requestId,
result: this._native.postMessageBatch(payload.batch),
}]);
case 'updateSharedResourceMap':
for (let [key, val] of Object.entries(e.data.resources)) {
for (let [key, val] of Object.entries(payload.resources)) {
this._native.updateSharedResourceMap(key, val, (message) => {
this.port.postMessage(['error', message]);
});
}

break;
return this.port.postMessage(['reply', {
requestId,
result: null,
}]);
case 'reset':
this._native.reset();
break;

return this.port.postMessage(['reply', {
requestId,
result: null,
}]);
case 'pruneVirtualFileSystem':
this._native.pruneSharedResourceMap();
break;
case 'listVirtualFileSystem':
let result = this._native.listSharedResourceMap();
let promiseKey = e.data.promiseKey;

this.port.postMessage(['resolvePromise', {promiseKey, result}]);
break;
return this.port.postMessage(['reply', {
requestId,
result: null,
}]);
case 'listVirtualFileSystem':
return this.port.postMessage(['reply', {
requestId,
result: this._native.listSharedResourceMap(),
}]);
default:
break;
}
Expand Down
14 changes: 7 additions & 7 deletions js/packages/web-renderer/raw/elementary-wasm.js

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions js/packages/web-renderer/test/stdlib.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ test('std lib should have sparseq2', async function() {
});

node.connect(audioContext.destination);
let stats = core.render(el.sparseq2({seq: [{ value: 0, time: 0 }]}, 1));
console.log(stats);
let stats = await core.render(el.sparseq2({seq: [{ value: 0, time: 0 }]}, 1));

expect(stats.nodesAdded).toEqual(3); // root, sparseq, const
expect(stats.edgesAdded).toEqual(2);
Expand Down
8 changes: 4 additions & 4 deletions js/packages/web-renderer/test/vfs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,22 @@ test('vfs should show registered entries', async function() {
expect(await core.listVirtualFileSystem()).toEqual(['test']);

// We haven't rendered anything that holds a reference to the test entry
core.pruneVirtualFileSystem();
await core.pruneVirtualFileSystem();
expect(await core.listVirtualFileSystem()).toEqual([]);

// Now we put something back in
core.updateVirtualFileSystem({
await core.updateVirtualFileSystem({
'test2': Float32Array.from([2, 3, 4, 5]),
});

// After we render something referencing the test2 entry, prune shouldn't touch it
expect(await core.listVirtualFileSystem()).toEqual(['test2']);
expect(core.render(el.table({key: 'a', path: 'test2'}, 0.5))).toMatchObject({
expect(await core.render(el.table({key: 'a', path: 'test2'}, 0.5))).toMatchObject({
nodesAdded: 3,
edgesAdded: 2,
propsWritten: 4,
});

core.pruneVirtualFileSystem();
await core.pruneVirtualFileSystem();
expect(await core.listVirtualFileSystem()).toEqual(['test2']);
});
15 changes: 9 additions & 6 deletions wasm/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,24 @@ class ElementaryAudioProcessor

//==============================================================================
/** Message batch handling. */
void postMessageBatch (val payload, val errorCallback)
val postMessageBatch (val payload)
{
auto v = emValToValue(payload);

if (!v.isArray()) {
errorCallback(val("error"), val("Malformed message batch."));
return;
return valueToEmVal(elem::js::Object {
{"success", false},
{"message", "Malformed message batch"},
});
}

auto const& batch = v.getArray();
auto const rc = runtime->applyInstructions(batch);

if (rc != elem::ReturnCode::Ok()) {
errorCallback(val("error"), val(elem::ReturnCode::describe(rc)));
}
return valueToEmVal(elem::js::Object {
{"success", rc == elem::ReturnCode::Ok()},
{"message", elem::ReturnCode::describe(rc)},
});
}

void reset()
Expand Down

0 comments on commit 4c19c2e

Please sign in to comment.