From 41b9413e5d8da71e6c6a6f8a9afe8e8cbcad590c Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Tue, 20 Oct 2026 10:07:20 +1300 Subject: [PATCH 1/2] New version. --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 1c1da8189..372a18a92 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.20261019.0 +1.20261020.0 From 2504c958bafffbc64e8faad5f1f07f16fb60831c Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Tue, 20 Oct 2026 10:23:21 +1300 Subject: [PATCH 2/2] JavaScript: allow the memory to grow on demand. --- src/CMakeLists.txt | 39 ++++++++++++++++++++------------ src/bindings/javascript/file.cpp | 4 ++-- src/misc/compiler.cpp | 2 +- src/sed/sedinstancetask.cpp | 6 ++--- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b2ad64ef4..c77193132 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -372,19 +372,28 @@ if(EMSCRIPTEN) # 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, -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. + # files are not LTO ones), -pthread to enable native threading support, -Wno-pthreads-mem-growth to silence + # Emscripten's warning about threading and memory growth (see Note #4), -sALLOW_MEMORY_GROWTH=1 to let our + # memory grow on demand (see Note #4), -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, + # -sMAXIMUM_MEMORY=2GB to let our memory grow up to 2GB (see Note #4), -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: our memory starts small (16MB, which is Emscripten's default when, like here, our memory is imported) and + # grows on demand, up to 2GB. Since we support threading, our memory is shared, which means that another + # thread may grow it, so Emscripten precedes each of our JavaScript code's accesses to it (e.g., through + # HEAPU8 in an EM_ASM() block) with a check of whether it has grown. That check makes those accesses slower + # (about 15ns each), hence Emscripten's warning, but it only affects calls that cross the + # JavaScript/WebAssembly boundary (e.g., a call that passes or returns a string), not our WebAssembly code + # (e.g., the simulation of a model). We don't use -sGROWABLE_ARRAYBUFFERS=1|2, which would replace that + # check with a growable SharedArrayBuffer since our results (see toFloat64ArrayJS()) would then be views + # over a growable SharedArrayBuffer, which some Web APIs reject (e.g., structuredClone()). We also don't + # use -sMAXIMUM_MEMORY=4GB since our EM_JS() functions would then get a pointer of 2GB or more as a + # negative number and Embind calls would be slower (since pointers would need to be made unsigned). # 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). @@ -404,14 +413,16 @@ if(EMSCRIPTEN) -O2 -flto -pthread + -sALLOW_MEMORY_GROWTH=1 -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 + -sMAXIMUM_MEMORY=2GB -sMODULARIZE=1 -sSTACK_SIZE=8MB + -Wno-pthreads-mem-growth --bind --emit-tsd ${DTS_FILE}) diff --git a/src/bindings/javascript/file.cpp b/src/bindings/javascript/file.cpp index 325ff4763..3e219405c 100644 --- a/src/bindings/javascript/file.cpp +++ b/src/bindings/javascript/file.cpp @@ -46,8 +46,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. By performing the copy entirely within a single EM_ASM() + // JavaScript's TypedArray view directly backed by wasmMemory.buffer. This is fragile since our memory + // can grow (see src/CMakeLists.txt). 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({ diff --git a/src/misc/compiler.cpp b/src/misc/compiler.cpp index 50da74367..0337bc7db 100644 --- a/src/misc/compiler.cpp +++ b/src/misc/compiler.cpp @@ -342,7 +342,7 @@ std::string patchWasmSharedMemory(UnsignedChars &pWasmModule) // Memory import: patch its flags to mark it as shared and give it a maximum size, if it doesn't // already have one. That maximum size must be at least that of the memory of our main WebAssembly // module (i.e. the memory that we import), which we retrieve at runtime rather than hard-code it - // since it depends on -sINITIAL_MEMORY, etc. (see src/CMakeLists.txt). + // since it depends on -sMAXIMUM_MEMORY, etc. (see src/CMakeLists.txt). // Note: LLVM currently emits a 32-bit, non-shared memory import without a maximum size (i.e. flags // 0x00), but we handle a memory import with a maximum size (i.e. flags 0x01) and/or that is // already shared (i.e. flags 0x03) in case a future version of LLVM does so. Any other diff --git a/src/sed/sedinstancetask.cpp b/src/sed/sedinstancetask.cpp index 57c258c04..ac359e119 100644 --- a/src/sed/sedinstancetask.cpp +++ b/src/sed/sedinstancetask.cpp @@ -43,9 +43,9 @@ EM_JS(intptr_t, toFloat64ArrayJS, (const void* data, size_t size), { } return Emval.toHandle(new Float64Array(HEAPU8.subarray(data, data + 8 * size).buffer, data, size)); - // Note: we use HEAPU8.subarray() to create a view over the WASM heap's Float64Array buffer because it is safer than - // accessing HEAPU8.buffer directly in case the WASM heap was ever to grow (we don't allow this to happen, but - // it is still safer to use HEAPU8.subarray()) since it creates a view with the correct byte offset. + // Note: our memory can grow (see src/CMakeLists.txt), but HEAPU8 is always up to date when we access it and our + // memory is shared, i.e. backed by a SharedArrayBuffer, which never gets detached when our memory grows. So, + // our view remains valid even after our memory has grown (as long as our results don't get reallocated). }); // clang-format on static emscripten::val toFloat64Array(std::span data)