blob: 722ed83ca0fdd3c64c13874b25eada5e471f498c [file]
// Copyright 2022 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// This is the JavaScript side of loop_emscripten.c
//
// References:
// * https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html
// * https://github.com/evanw/emscripten-library-generator
// * https://github.com/emscripten-core/emscripten/tree/main/src
// * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
// * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
const LibraryIreeLoopEmscripten = {
$iree_loop_emscripten_support__postset: 'iree_loop_emscripten_support();',
$iree_loop_emscripten_support: function() {
// iree_status_t objects for common iree_status_code_t values.
// Keep in sync with status.h.
const IREE_STATUS_OK = 0;
const IREE_STATUS_CODE_MASK = 0x1F;
const IREE_STATUS_INVALID_ARGUMENT = 3 & IREE_STATUS_CODE_MASK;
const IREE_STATUS_ABORTED = 10 & IREE_STATUS_CODE_MASK;
const IREE_STATUS_UNIMPLEMENTED = 12 & IREE_STATUS_CODE_MASK;
// iree_loop_command_e values, keep in sync with loop.h.
const IREE_LOOP_COMMAND_CALL = 0;
const IREE_LOOP_COMMAND_DISPATCH = 1;
const IREE_LOOP_COMMAND_WAIT_UNTIL = 2;
const IREE_LOOP_COMMAND_WAIT_ONE = 3;
const IREE_LOOP_COMMAND_WAIT_ANY = 4;
const IREE_LOOP_COMMAND_WAIT_ALL = 5;
class LoopCommand {
abort() {}
}
class LoopCommandCall extends LoopCommand {
constructor(scope, operationId, callback, userData, loop) {
super();
this.callback = callback;
this.userData = userData;
this.loop = loop;
this.timeoutId = setTimeout(() => {
Module['dynCall'](
'iiii', this.callback,
[this.userData, this.loop, IREE_STATUS_OK]);
// TODO(scotttodd): handle the returned status (sticky failure state?)
// at least free the status so it doesn't leak
delete scope.pendingOperations[operationId];
}, 0);
}
abort() {
clearTimeout(this.timeoutId);
Module['dynCall'](
'iiii', this.callback,
[this.userData, this.loop, IREE_STATUS_ABORTED]);
}
}
class LoopCommandWaitUntil extends LoopCommand {
constructor(scope, operationId, callback, userData, timeoutMs, loop) {
super();
this.callback = callback;
this.userData = userData;
this.loop = loop;
this.abortFn = undefined;
const abortPromise = new Promise((_, reject) => {
this.abortFn = reject;
});
const timeoutPromise = new Promise((resolve, _) => {
setTimeout(() => {
resolve();
}, timeoutMs);
});
Promise.race([abortPromise, timeoutPromise])
.then(() => {
Module['dynCall'](
'iiii', this.callback,
[this.userData, this.loop, IREE_STATUS_OK]);
// TODO(scotttodd): handle the returned status (sticky failure
// state?) at least free the status so it doesn't leak
delete scope.pendingOperations[operationId];
})
.catch(() => {
Module['dynCall'](
'iiii', this.callback,
[this.userData, this.loop, IREE_STATUS_ABORTED]);
})
.finally(() => {
delete scope.pendingOperations[operationId];
});
}
abort() {
this.abortFn();
}
}
class LoopCommandWaitPromise extends LoopCommand {
constructor(
scope, operationId, callback, userData, timeoutMs, waitPromise,
loop) {
super();
this.callback = callback;
this.userData = userData;
this.loop = loop;
this.abortFn = undefined;
// We're given a primary Promise to wait on, but we'll race it against
// two other Promises:
// * timeoutPromise: rejected after a timeout
// * abortPromise : rejected if abort() is called
// This way, whichever Promise settles first will decide the eventual
// state and either issue the callback with IREE_STATUS_OK or
// IREE_STATUS_ABORTED.
const abortPromise = new Promise((_, reject) => {
this.abortFn = reject;
});
let racePromise;
if (timeoutMs >= 0 && timeoutMs < 2147483647) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject();
}, timeoutMs);
});
racePromise =
Promise.race([waitPromise, abortPromise, timeoutPromise]);
} else {
// "Infinite" timeout.
racePromise = Promise.race([waitPromise, abortPromise]);
}
racePromise
.then(() => {
Module['dynCall'](
'iiii', this.callback,
[this.userData, this.loop, IREE_STATUS_OK]);
// TODO(scotttodd): handle the returned status (sticky failure
// state?) at least free the status so it doesn't leak
delete scope.pendingOperations[operationId];
})
.catch(() => {
Module['dynCall'](
'iiii', this.callback,
[this.userData, this.loop, IREE_STATUS_ABORTED]);
})
.finally(() => {
delete scope.pendingOperations[operationId];
});
}
abort() {
this.abortFn();
}
}
class LoopEmscriptenScope {
constructor() {
// Note: start at 1, leaving 0 as a sentinel for uninitialized.
this.nextOperationId = 1;
// Dictionary of operationIds -> LoopCommands.
this.pendingOperations = {};
}
destroy() {
for (const id in this.pendingOperations) {
const operation = this.pendingOperations[id];
operation.abort();
delete this.pendingOperations[id];
}
}
runCommand(command, callback, userData, timeoutMs, waitPromises, loop) {
// TODO(scotttodd): assert not destroyed to avoid reentrant queueing?
const operationId = this.nextOperationId++;
switch (command) {
case IREE_LOOP_COMMAND_CALL:
this.pendingOperations[operationId] = new LoopCommandCall(
this, operationId, callback, userData, loop);
break;
case IREE_LOOP_COMMAND_DISPATCH:
return IREE_STATUS_UNIMPLEMENTED;
case IREE_LOOP_COMMAND_WAIT_UNTIL:
this.pendingOperations[operationId] = new LoopCommandWaitUntil(
this, operationId, callback, userData, timeoutMs, loop);
break;
case IREE_LOOP_COMMAND_WAIT_ONE:
this.pendingOperations[operationId] = new LoopCommandWaitPromise(
this, operationId, callback, userData, timeoutMs,
waitPromises[0], loop);
break;
case IREE_LOOP_COMMAND_WAIT_ANY:
const anyPromise = Promise.any(waitPromises);
this.pendingOperations[operationId] = new LoopCommandWaitPromise(
this, operationId, callback, userData, timeoutMs, anyPromise,
loop);
break;
case IREE_LOOP_COMMAND_WAIT_ALL:
const allPromise = Promise.all(waitPromises);
this.pendingOperations[operationId] = new LoopCommandWaitPromise(
this, operationId, callback, userData, timeoutMs, allPromise,
loop);
break;
default:
return IREE_STATUS_UNIMPLEMENTED;
}
return IREE_STATUS_OK;
}
}
class LoopEmscripten {
constructor() {
// Note: start at 1, leaving 0 as a sentinel for uninitialized.
this.nextScopeHandle = 1;
// Dictionary of scopeHandles -> LoopEmscriptenScopes.
this.scopes = {};
}
iree_loop_allocate_scope() {
const scopeHandle = this.nextScopeHandle++;
this.scopes[scopeHandle] = new LoopEmscriptenScope();
return scopeHandle;
}
iree_loop_free_scope(scopeHandle) {
if (!(scopeHandle in this.scopes)) return;
const scope = this.scopes[scopeHandle];
scope.destroy();
delete this.scopes[scopeHandle];
}
iree_loop_command(
scopeHandle, command, callback, userData, timeoutMs,
promiseHandlesCount, promiseHandles, loop) {
if (!(scopeHandle in this.scopes)) return IREE_STATUS_INVALID_ARGUMENT;
const scope = this.scopes[scopeHandle];
const waitPromises = [];
for (let i = 0; i < promiseHandlesCount; ++i) {
const promiseHandle = getValue(promiseHandles + i * 4);
waitPromises[i] = IreeWaitHandleEmscripten.getPromise(promiseHandle);
}
return scope.runCommand(
command, callback, userData, timeoutMs, waitPromises, loop);
}
}
const instance = new LoopEmscripten();
_iree_loop_allocate_scope =
instance.iree_loop_allocate_scope.bind(instance);
_iree_loop_free_scope = instance.iree_loop_free_scope.bind(instance);
_iree_loop_command = instance.iree_loop_command.bind(instance);
},
$iree_loop_emscripten_support__deps:
['$dynCall', '$IreeWaitHandleEmscripten'],
iree_loop_allocate_scope: function() {},
iree_loop_allocate_scope__deps: ['$iree_loop_emscripten_support'],
iree_loop_free_scope: function() {},
iree_loop_free_scope__deps: ['$iree_loop_emscripten_support'],
iree_loop_command: function() {},
iree_loop_command__deps: ['$iree_loop_emscripten_support'],
}
mergeInto(LibraryManager.library, LibraryIreeLoopEmscripten);