Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.20261016.0
1.20261016.1
52 changes: 29 additions & 23 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -364,47 +364,53 @@ if(EMSCRIPTEN)
${HEADER_FILES})

# Set the flags for our generated .js and .wasm files.
# Note #1: for compiling, we use -O3 to get the best performance, -matomics to enable atomic operations,
# -mbulk-memory to enable bulk memory operations, -pthread to enable native threading support,
# -sSHARED_MEMORY=1 to enable shared memory support, and -DNDEBUG to disable debugging information (it is
# not needed in production).
# Note #2: for linking, we use -O2 (rather than -O3) since our compressed .wasm file is then smaller with no loss of
# Note #1: our other C/C++ and linker flags (e.g., to enable threading, shared memory, SIMD, and WebAssembly
# exception handling) are those that src/bindings/javascript/CMakeLists.txt passes on to the Emscripten
# build that it kicks off (see the note above about building our third-party libraries).
# Note #2: for compiling, we use -O3 to get the best performance and -DNDEBUG to disable assertions (they are not
# needed in production).
# Note #3: for linking, we use -O2 (rather than -O3) since our compressed .wasm file is then smaller with no loss of
# performance (and it links faster), -flto to use the LTO version of Emscripten's system libraries and to
# optimise LLVM, which we build as LLVM bitcode (see src/3rdparty/LLVM/CMakeLists.txt; our other object
# files are not LTO ones), -pthread to enable native threading support, -sALLOW_TABLE_GROWTH to allow the
# WebAssembly table to grow, -sASSERTIONS=0 to disable assertions (they are not needed in production),
# -sEXPORT_ES6=1 to export ES6 modules, -sEXPORTED_FUNCTIONS to export the functions we need (i.e. those
# needed by the WebAssembly modules that we generate for our models, see initialiseWorkerWasmJS()),
# -sEXPORTED_RUNTIME_METHODS to export the runtime methods we need, -sINITIAL_MEMORY=2GB to have enough
# memory for "big" models, -sMODULARIZE=1 to generate a module factory function, -sSHARED_MEMORY=1 to
# enable shared memory support, -sSTACK_SIZE=8MB to reserve a reasonable amount of memory for the stack at
# startup (which is needed for "big" models), --bind to generate the necessary Embind bindings, and
# --emit-tsd to generate a TypeScript type definitions file.
# Note #3: for linking, we used to have -sALLOW_MEMORY_GROWTH=1, -sINITIAL_MEMORY=256MB, and -sMAXIMUM_MEMORY=2GB,
# but we now support threading and to allow memory growth might non-WASM code to run slowly. So, now, we
# use -sINITIAL_MEMORY=2GB and don't allow memory growth.
# files are not LTO ones), -pthread to enable native threading support, -sASSERTIONS=0 to disable
# assertions (they are not needed in production), -sCROSS_ORIGIN=1 to support our .js file being loaded
# from another origin than that of the Web page (see Note #5), -sEXPORT_ES6=1 to export ES6 modules,
# -sEXPORTED_FUNCTIONS to export the functions we need (i.e. those needed by the WebAssembly modules that
# we generate for our models, see initialiseWorkerWasmJS()), -sEXPORTED_RUNTIME_METHODS to export the
# runtime methods we need, -sINITIAL_MEMORY=2GB to have enough memory for "big" models, -sMODULARIZE=1 to
# generate a module factory function, -sSTACK_SIZE=8MB to reserve a reasonable amount of memory for the
# stack of each thread (which is needed for "big" models), --bind to generate the necessary Embind
# bindings, and --emit-tsd to generate a TypeScript type definitions file.
# Note #4: for linking, we used to have -sALLOW_MEMORY_GROWTH=1, -sINITIAL_MEMORY=256MB, and -sMAXIMUM_MEMORY=2GB,
# but we now support threading, which means that our memory is shared, and to allow shared memory to grow
# would make JavaScript's accesses to it (e.g., through HEAPU8) slower. So, now, we use
# -sINITIAL_MEMORY=2GB and don't allow memory growth.
# Note #5: a Web browser only creates a worker (and therefore a thread) from a script that has the same origin as
# the Web page, so without -sCROSS_ORIGIN=1, a thread couldn't be created if our .js file was loaded from
# another origin (e.g., a CDN), i.e. startRun() would throw an error (Chrome) or never complete (Firefox).
# With -sCROSS_ORIGIN=1, a worker is instead created from a Blob URL (which has the same origin as the Web
# page) that imports our .js file, whatever the origin of our .js file. This means that our .js file, if
# loaded from another origin, must be served with CORS headers and that a Web page that has a Content
# Security Policy must allow blob: workers (i.e. worker-src blob:), even if our .js file has the same
# origin as the Web page. Otherwise, a run started using startRun() would never actually start (both in
# Chrome and Firefox).

set(DTS_FILE ${CMAKE_PROJECT_NAME}.d.ts)

target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
-O3
-matomics
-mbulk-memory
-pthread
-sSHARED_MEMORY=1
-DNDEBUG)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
-O2
-flto
-pthread
-sALLOW_TABLE_GROWTH
-sASSERTIONS=0
-sCROSS_ORIGIN=1
-sEXPORT_ES6=1
-sEXPORTED_FUNCTIONS=[_free,_malloc,_memset,_nlaSolverAddress,_wasmNlaSolve,_pow,_sqrt,_fabs,_exp,_exp2,_log,_log10,_ceil,_floor,_fmin,_fmax,_fmod,_sin,_cos,_tan,_sinh,_cosh,_tanh,_asin,_acos,_atan,_asinh,_acosh,_atanh]
-sEXPORTED_RUNTIME_METHODS=HEAPU8
-sINITIAL_MEMORY=2GB
-sMODULARIZE=1
-sSHARED_MEMORY=1
-sSTACK_SIZE=8MB
--bind
--emit-tsd ${DTS_FILE})
Expand Down
17 changes: 17 additions & 0 deletions src/api/libopencor/sedinstance.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ class LIBOPENCOR_EXPORT SedInstance: public Logger
* Run all the tasks associated with this instance. If a run is already in progress (see @ref startRun), then wait
* for it to complete before running all the tasks associated with this instance.
*
* Note: in a Web browser, calling this method on the main thread straight after @ref startRun() blocks forever
* since the background thread cannot start until the main thread returns to its event loop, so poll @ref status()
* until the instance is idle first.
*
* @return The elapsed time in milliseconds.
*/

Expand All @@ -88,6 +92,15 @@ class LIBOPENCOR_EXPORT SedInstance: public Logger
* plot them progressively). The simulation settings and model changes used are those in effect when this method is
* called.
*
* Note: in a Web browser, this method can only be used if the page is cross-origin isolated (i.e. served with the
* "Cross-Origin-Opener-Policy: same-origin" and "Cross-Origin-Embedder-Policy: require-corp" HTTP headers) since
* threads are otherwise not supported, in which case an error is thrown. Also, if the page has a Content Security
* Policy, it must allow blob: workers (i.e. worker-src blob:) since the run would otherwise never start, i.e. the
* status of the instance would remain RUNNING. Finally, deleting the instance on the main thread before its run has
* actually started (e.g., straight after calling this method) blocks forever since the instance stops its run and
* waits for it to complete before being deleted, but the background thread cannot start until the main thread
* returns to its event loop.
*
* @return @c true if a new run was started, @c false if a run is already in progress.
*/

Expand All @@ -98,6 +111,10 @@ class LIBOPENCOR_EXPORT SedInstance: public Logger
*
* Wait for any currently-running instance to complete.
*
* Note: in a Web browser, calling this method on the main thread straight after @ref startRun() blocks forever
* since the background thread cannot start until the main thread returns to its event loop, so poll @ref status()
* until the instance is idle first.
*
* @return The elapsed time in milliseconds for the last completed instance run.
*/

Expand Down
5 changes: 4 additions & 1 deletion src/bindings/javascript/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ if(EMSCRIPTEN OR LIBOPENCOR_JAVASCRIPT_BINDINGS)
# catching the std::out_of_range exception thrown by std::stoi()). Unlike Emscripten's
# JavaScript-based exception handling, WebAssembly exception handling has no runtime cost when no
# exception is thrown.
# Note #3: those flags are also used to build our third-party libraries (see packages.cmake), so that they are
# Note #3: the linker flags also allow the WebAssembly table to grow (-sALLOW_TABLE_GROWTH), which we need to
# install the functions of the WebAssembly modules that we generate for our models (see
# initialiseWorkerWasmJS()).
# Note #4: those flags are also used to build our third-party libraries (see packages.cmake), so that they are
# all built with exception handling too.

set(EMSCRIPTEN_COMPILER_FLAGS "-fwasm-exceptions -matomics -mbulk-memory -msimd128 -pthread -sSHARED_MEMORY=1")
Expand Down
5 changes: 2 additions & 3 deletions src/bindings/javascript/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ void fileApi()

// Note: we avoid using emscripten::typed_memory_view() as an intermediate step since it creates a
// JavaScript's TypedArray view directly backed by wasmMemory.buffer. This is fragile and would break
// if ALLOW_MEMORY_GROWTH was to be enabled or if pthreads/SharedArrayBuffer support were to be added.
// By performing the copy entirely within a single EM_ASM() block, HEAPU8 is fetched and used
// atomically.
// if ALLOW_MEMORY_GROWTH was to be enabled. By performing the copy entirely within a single EM_ASM()
// block, HEAPU8 is fetched and used atomically.

return emscripten::val::take_ownership(static_cast<emscripten::EM_VAL>(EM_ASM_PTR({
let jsArray = new Uint8Array(HEAPU8.subarray($0, $0 + $1));
Expand Down
7 changes: 4 additions & 3 deletions src/bindings/javascript/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ namespace {
void restoreStackOnThrow()
{
// Make sure that the WebAssembly stack pointer gets restored when a binding throws a JavaScript exception (see Note
// #4 in stringconverter.h), i.e. a binding that takes a string (see fromJsString()) or that may reject a value (see
// throwRangeError()), as Embind itself does for a binding that allocates its arguments on the WebAssembly stack.
// #4 in stringconverter.h), i.e. a binding that takes a string (see fromJsString()), that may reject a value (see
// throwRangeError()), or that may fail in the current JavaScript environment (see throwError()), as Embind itself
// does for a binding that allocates its arguments on the WebAssembly stack.
// Note #1: a binding is listed as "<class name>:<member name>|<member name>|...", where a member name is that of a
// method, a static method, a property (in which case its setter is wrapped), or "constructor" (in which
// case the class is wrapped using a Proxy, so that it otherwise behaves as before, e.g., for instanceof).
Expand All @@ -55,7 +56,7 @@ void restoreStackOnThrow()
stackRestore(sp);
}
};
let bindings = 'File:constructor|childFileFromFileName FileManager:fileFromFileNameOrUrl SedBase:id|isDuplicateId|isValidId SedChangeAttribute:constructor|componentName|variableName|newValue SedDocument:serialise Strings:push_back|resize|set'.split(' ');
let bindings = 'File:constructor|childFileFromFileName FileManager:fileFromFileNameOrUrl SedBase:id|isDuplicateId|isValidId SedChangeAttribute:constructor|componentName|variableName|newValue SedDocument:serialise SedInstance:startRun Strings:push_back|resize|set'.split(' ');

addOnPostCtor(() => bindings.forEach((binding) => {
let parts = binding.split(':');
Expand Down
18 changes: 17 additions & 1 deletion src/bindings/javascript/sed.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ limitations under the License.

#include <libopencor>

#include <emscripten/threading.h>

namespace {

constexpr std::string_view INVALID_ID_ERROR {"The id is not a valid SED-ML identifier."};
Expand All @@ -26,6 +28,7 @@ constexpr std::string_view COMPONENT_NAME_ERROR {"The component name is not a va
constexpr std::string_view VARIABLE_NAME_ERROR {"The variable name is not a valid CellML identifier."};
constexpr std::string_view NEW_VALUE_ERROR {"The new value is not a valid double value."};
constexpr std::string_view CHANGE_ATTRIBUTE_ERROR {"The component and variable names must be valid CellML identifiers and the new value must be a valid double value."};
constexpr std::string_view START_RUN_ERROR {"The instance cannot be run in a background thread since threads are not supported (in a Web browser, the page must be cross-origin isolated)."};

void setId(libOpenCOR::SedBase &pThis, const JsString &pId)
{
Expand Down Expand Up @@ -153,7 +156,20 @@ void sedApi()
.smart_ptr<libOpenCOR::SedInstancePtr>("SedInstance")
.property("status", &libOpenCOR::SedInstance::status)
.function("run", &libOpenCOR::SedInstance::run)
.function("startRun", &libOpenCOR::SedInstance::startRun)
// clang-format off
.function("startRun", emscripten::optional_override([](libOpenCOR::SedInstance &pThis) {
// Note: threads require SharedArrayBuffer, which a Web browser only provides to a page that is cross-origin
// isolated (i.e. served with the "Cross-Origin-Opener-Policy: same-origin" and
// "Cross-Origin-Embedder-Policy: require-corp" HTTP headers). Without it, startRun() would fail to
// create its thread and throw a C++ exception, which would reach JavaScript as an opaque
// WebAssembly.Exception, so we throw an Error with a meaningful message instead.

if (!emscripten_has_threading_support()) {
throwError(START_RUN_ERROR);
}

return pThis.startRun();
})) // clang-format on
.function("waitForRun", &libOpenCOR::SedInstance::waitForRun)
.function("pauseRun", &libOpenCOR::SedInstance::pauseRun)
.function("resumeRun", &libOpenCOR::SedInstance::resumeRun)
Expand Down
12 changes: 10 additions & 2 deletions src/bindings/javascript/stringconverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ limitations under the License.
// we throw using emscripten::val::throw_() rather than from within an EM_ASM() block since the latter is
// declared as not throwing. Also, a JavaScript exception that leaves WebAssembly doesn't restore the
// WebAssembly stack pointer, meaning that some of the WebAssembly stack would be leaked every time a binding
// throws. So, every binding that may throw (i.e. that uses fromJsString() or throwRangeError()) must be listed
// in restoreStackOnThrow() (see main.cpp).
// throws. So, every binding that may throw (i.e. that uses fromJsString(), throwError(), or throwRangeError())
// must be listed in restoreStackOnThrow() (see main.cpp).

EMSCRIPTEN_DECLARE_VAL_TYPE(JsString);

Expand Down Expand Up @@ -188,6 +188,14 @@ inline std::string fromJsString(const JsString &pString)
// clang-format on
}

[[noreturn]] inline void throwError(std::string_view pMessage)
{
// Throw a JavaScript Error with the given message (see Note #4), e.g., when something cannot be done in the current
// JavaScript environment.

emscripten::val::global("Error").new_(std::string(pMessage)).throw_();
}

[[noreturn]] inline void throwRangeError(std::string_view pMessage)
{
// Throw a JavaScript RangeError with the given message (see Note #4), e.g., when a value is rejected by libOpenCOR.
Expand Down
Loading
Loading