diff --git a/VERSION.txt b/VERSION.txt index f800d458e..cac73a68b 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.20261016.0 +1.20261016.1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8efff70a6..b2ad64ef4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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}) diff --git a/src/api/libopencor/sedinstance.h b/src/api/libopencor/sedinstance.h index f5c6a52d6..b46ad1357 100644 --- a/src/api/libopencor/sedinstance.h +++ b/src/api/libopencor/sedinstance.h @@ -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. */ @@ -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. */ @@ -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. */ diff --git a/src/bindings/javascript/CMakeLists.txt b/src/bindings/javascript/CMakeLists.txt index b077edc5a..16fb49bdd 100644 --- a/src/bindings/javascript/CMakeLists.txt +++ b/src/bindings/javascript/CMakeLists.txt @@ -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") diff --git a/src/bindings/javascript/file.cpp b/src/bindings/javascript/file.cpp index de7a94597..325ff4763 100644 --- a/src/bindings/javascript/file.cpp +++ b/src/bindings/javascript/file.cpp @@ -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(EM_ASM_PTR({ let jsArray = new Uint8Array(HEAPU8.subarray($0, $0 + $1)); diff --git a/src/bindings/javascript/main.cpp b/src/bindings/javascript/main.cpp index 070ceffcb..84616b815 100644 --- a/src/bindings/javascript/main.cpp +++ b/src/bindings/javascript/main.cpp @@ -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 ":||...", 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). @@ -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(':'); diff --git a/src/bindings/javascript/sed.cpp b/src/bindings/javascript/sed.cpp index 65e5fc739..e3af1f5d8 100644 --- a/src/bindings/javascript/sed.cpp +++ b/src/bindings/javascript/sed.cpp @@ -18,6 +18,8 @@ limitations under the License. #include +#include + namespace { constexpr std::string_view INVALID_ID_ERROR {"The id is not a valid SED-ML identifier."}; @@ -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) { @@ -153,7 +156,20 @@ void sedApi() .smart_ptr("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) diff --git a/src/bindings/javascript/stringconverter.h b/src/bindings/javascript/stringconverter.h index 976404aca..bd776520e 100644 --- a/src/bindings/javascript/stringconverter.h +++ b/src/bindings/javascript/stringconverter.h @@ -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); @@ -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. diff --git a/src/sed/sedinstance.cpp b/src/sed/sedinstance.cpp index 4a4d23930..778a8ef5b 100644 --- a/src/sed/sedinstance.cpp +++ b/src/sed/sedinstance.cpp @@ -265,6 +265,20 @@ double SedInstance::Impl::executeRun() return res; } +double SedInstance::Impl::joinRun() +{ + // Wait for our asynchronous run to complete and return its elapsed time (or rethrow the exception it threw). + // Note: we join the thread of our asynchronous run rather than only wait for its future to become ready, so that + // the thread's resources get released straight away. Indeed, our WASM version would otherwise only release + // them (i.e. free the thread's stack and make its Web worker available for another thread) once the main + // thread returns to its event loop, meaning that asynchronous runs started and waited for from a loop (e.g., + // a parameter sweep in Node.js) would eventually exhaust our WASM memory. + + mRunThread.join(); + + return mRunFuture.get(); +} + double SedInstance::Impl::run() { const std::scoped_lock runLock(mRunMutex); @@ -272,7 +286,7 @@ double SedInstance::Impl::run() // Wait for any asynchronous run to complete since our tasks cannot be run while they are already being run. if (mRunFuture.valid()) { - mLastRunElapsedTime.store(mRunFuture.get(), std::memory_order_relaxed); + mLastRunElapsedTime.store(joinRun(), std::memory_order_relaxed); } // Reset our control flags (see the note in prepareRun()). @@ -310,7 +324,7 @@ bool SedInstance::Impl::startRun() return false; } - mLastRunElapsedTime.store(mRunFuture.get(), std::memory_order_relaxed); + mLastRunElapsedTime.store(joinRun(), std::memory_order_relaxed); } // Reset our control flags (see the note in prepareRun()). @@ -329,21 +343,29 @@ bool SedInstance::Impl::startRun() // Note #1: we must be flagged as not running anymore once our run is done, even if executeRun() throws an exception // (it reports a failure as an issue, but it might still throw, e.g., std::bad_alloc when adding an issue), // hence we use a guard to do so. Otherwise, we would be stuck in RUNNING. - // Note #2: std::async() may throw an exception (e.g., std::system_error if no thread could be created), in which - // case we must also be flagged as not running anymore. We have no way to trigger such an exception in our - // tests, hence we ignore our try...catch statement during code coverage. + // Note #2: we use a joinable thread rather than std::async() since the latter may use a detached thread (e.g., with + // libc++), which we couldn't join (see joinRun()). We still use a future (through a packaged task) so that + // we can retrieve the elapsed time of our run or the exception that it threw. + // Note #3: creating our thread may throw an exception (e.g., std::system_error if no thread could be created), in + // which case we must also be flagged as not running anymore and must not have a future (since it would + // never become ready). We have no way to trigger such an exception in our tests, hence we ignore our + // try...catch statement during code coverage. + + std::packaged_task runTask([this]() { + auto resetRunning = [](std::atomic *pRunning) { + pRunning->store(false, std::memory_order_release); + }; + const std::unique_ptr, decltype(resetRunning)> runningGuard {&mRunning, resetRunning}; + + return executeRun(); + }); + auto runFuture {runTask.get_future()}; #ifndef CODE_COVERAGE_ENABLED try { #endif - mRunFuture = std::async(std::launch::async, [this]() { - auto resetRunning = [](std::atomic *pRunning) { - pRunning->store(false, std::memory_order_release); - }; - const std::unique_ptr, decltype(resetRunning)> runningGuard {&mRunning, resetRunning}; - - return executeRun(); - }); + mRunThread = std::thread(std::move(runTask)); + mRunFuture = std::move(runFuture); #ifndef CODE_COVERAGE_ENABLED } catch (...) { mRunning.store(false, std::memory_order_release); @@ -363,7 +385,7 @@ double SedInstance::Impl::waitForRun() return mLastRunElapsedTime.load(std::memory_order_relaxed); } - mLastRunElapsedTime.store(mRunFuture.get(), std::memory_order_relaxed); + mLastRunElapsedTime.store(joinRun(), std::memory_order_relaxed); mRunning.store(false, std::memory_order_release); return mLastRunElapsedTime.load(std::memory_order_relaxed); diff --git a/src/sed/sedinstance_p.h b/src/sed/sedinstance_p.h index d0f146619..1eb247971 100644 --- a/src/sed/sedinstance_p.h +++ b/src/sed/sedinstance_p.h @@ -24,6 +24,7 @@ limitations under the License. #include #include #include +#include namespace libOpenCOR { @@ -39,6 +40,7 @@ class SedInstance::Impl: public Logger::Impl mutable std::atomic mRunning {false}; mutable std::mutex mRunMutex; + mutable std::thread mRunThread; mutable std::future mRunFuture; mutable std::atomic mLastRunElapsedTime {0.0}; @@ -55,6 +57,7 @@ class SedInstance::Impl: public Logger::Impl void prepareRun(); double executeRun(); + double joinRun(); double run(); bool startRun(); diff --git a/tests/api/sed/instancetests.cpp b/tests/api/sed/instancetests.cpp index 966dc25f9..302031829 100644 --- a/tests/api/sed/instancetests.cpp +++ b/tests/api/sed/instancetests.cpp @@ -177,6 +177,31 @@ TEST(InstanceSedTest, asynchronousRunCanBeRestarted) EXPECT_FALSE(instance->hasIssues()); } +TEST(InstanceSedTest, manyAsynchronousRuns) +{ + // Note: each asynchronous run is done in its own thread, the resources of which must be released once the run has + // been waited for. Otherwise, our WASM version would run out of memory after a few hundred runs. + + static const auto RUN_COUNT = 300; + static const auto SIMULATION_PROPERTY = 10; + + auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("cellml_2.cellml"))}; + auto document {libOpenCOR::SedDocument::create(file)}; + const auto &simulation {std::dynamic_pointer_cast(document->simulations()[0])}; + + simulation->setNumberOfSteps(SIMULATION_PROPERTY); + simulation->setOutputEndTime(static_cast(SIMULATION_PROPERTY)); + + auto instance {document->instantiate()}; + + for (size_t i {0}; i < RUN_COUNT; ++i) { + EXPECT_TRUE(instance->startRun()); + EXPECT_GT(instance->waitForRun(), 0.0); + } + + EXPECT_FALSE(instance->hasIssues()); +} + TEST(InstanceSedTest, progressBeforeAnyRun) { auto file {libOpenCOR::File::create(libOpenCOR::resourcePath("cellml_2.cellml"))}; diff --git a/tests/bindings/javascript/README.md b/tests/bindings/javascript/README.md index 31c7d2f5f..666af359c 100644 --- a/tests/bindings/javascript/README.md +++ b/tests/bindings/javascript/README.md @@ -7,4 +7,4 @@ cd [libOpenCOR] emrun --browser chrome build/tests/bindings/javascript/index.html ``` -Note: libOpenCOR's JavaScript bindings require `SharedArrayBuffer` for pthread support, which in turn requires the `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` HTTP headers (or their `` equivalents, which are already present in the HTML). If the demo page fails to load the WebAssembly module, verify that these headers are being served. +Note: libOpenCOR's JavaScript bindings require `SharedArrayBuffer` to run a simulation in a background thread (i.e. using `startRun()`), which in turn requires the page to be served with the `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` HTTP headers. `emrun` serves them, but if you use another Web server, you must make sure that it serves them too (Web browsers ignore their `` equivalents). Otherwise, `startRun()` throws an error. diff --git a/tests/bindings/javascript/res/index.html b/tests/bindings/javascript/res/index.html index e70c5767d..f22569bba 100644 --- a/tests/bindings/javascript/res/index.html +++ b/tests/bindings/javascript/res/index.html @@ -5,8 +5,6 @@ - - diff --git a/tests/bindings/javascript/sed.instance.test.js b/tests/bindings/javascript/sed.instance.test.js index fb46e81b6..b47a5c854 100644 --- a/tests/bindings/javascript/sed.instance.test.js +++ b/tests/bindings/javascript/sed.instance.test.js @@ -209,6 +209,73 @@ test.describe('Sed instance tests', () => { assert.strictEqual(instance.hasIssues, false); }); + test('Many asynchronous runs', () => { + // Note: each asynchronous run is done in its own thread, the resources of which must be released once the run has + // been waited for. Otherwise, our WASM version would run out of memory after a few hundred runs. + + const RUN_COUNT = 300; + const SIMULATION_PROPERTY = 10; + + const file = new loc.File(utils.resourcePath('cellml_2.cellml')); + + file.setContents(utils.fileContents(file.path)); + + const document = new loc.SedDocument(file); + const simulation = document.simulations.get(0); + + simulation.numberOfSteps = SIMULATION_PROPERTY; + simulation.outputEndTime = SIMULATION_PROPERTY; + + const instance = document.instantiate(); + + for (let i = 0; i < RUN_COUNT; ++i) { + assert.strictEqual(instance.startRun(), true); + assert.ok(instance.waitForRun() > 0.0); + } + + assert.strictEqual(instance.hasIssues, false); + }); + + test('Asynchronous run without threading support', () => { + // Note: this has no C++ or Python equivalent since threads are always supported in C++ and Python while, in a Web + // browser, they are only supported if the page is cross-origin isolated, i.e. if SharedArrayBuffer is + // available, hence we simulate this by hiding SharedArrayBuffer. + + const file = new loc.File(utils.resourcePath('cellml_2.cellml')); + + file.setContents(utils.fileContents(file.path)); + + const document = new loc.SedDocument(file); + const instance = document.instantiate(); + const sharedArrayBuffer = globalThis.SharedArrayBuffer; + + delete globalThis.SharedArrayBuffer; + + try { + assert.throws( + () => { + instance.startRun(); + }, + { + name: 'Error', + message: + '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).' + } + ); + } finally { + globalThis.SharedArrayBuffer = sharedArrayBuffer; + } + + assert.strictEqual(instance.status, loc.SedInstance.Status.IDLE); + + // The instance can still be run, be it synchronously or, once threads are supported again, asynchronously. + + assert.ok(instance.run() > 0.0); + assert.strictEqual(instance.startRun(), true); + assert.ok(instance.waitForRun() > 0.0); + assert.strictEqual(instance.hasIssues, false); + }); + test('Progress before any run', () => { const file = new loc.File(utils.resourcePath('cellml_2.cellml')); diff --git a/tests/bindings/python/test_sed_instance.py b/tests/bindings/python/test_sed_instance.py index 9bcd40289..f189871cf 100644 --- a/tests/bindings/python/test_sed_instance.py +++ b/tests/bindings/python/test_sed_instance.py @@ -199,6 +199,29 @@ def test_asynchronous_run_can_be_restarted(): assert not instance.has_issues +def test_many_asynchronous_runs(): + # Note: each asynchronous run is done in its own thread, the resources of which must be released once the run has + # been waited for. Otherwise, our WASM version would run out of memory after a few hundred runs. + + RUN_COUNT = 300 + SIMULATION_PROPERTY = 10 + + file = loc.File(utils.resource_path("cellml_2.cellml")) + document = loc.SedDocument(file) + simulation = document.simulations[0] + + simulation.number_of_steps = SIMULATION_PROPERTY + simulation.output_end_time = float(SIMULATION_PROPERTY) + + instance = document.instantiate() + + for _ in range(RUN_COUNT): + assert instance.start_run() is True + assert instance.wait_for_run() > 0.0 + + assert not instance.has_issues + + def test_progress_before_any_run(): file = loc.File(utils.resource_path("cellml_2.cellml")) document = loc.SedDocument(file)