From 90468db6884b48561b5cffb8b40f607bdcb494e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=A9ron?= Date: Thu, 29 Oct 2026 23:04:09 +0100 Subject: [PATCH 1/2] Don't walk the main module's table in pthread workers The first call to getFunctionAddress walks the whole table, which creates a JS function object for every entry. With MAIN_MODULE and pthreads, every worker does this when it instantiates the main module, and keeps those objects for its lifetime: about 8 MiB per worker for a 50k-entry table. The dynamic linker only looks up the main module's exported functions, so the main thread now records their table slots and passes them to workers, which add just those to the map. Co-Authored-By: Claude Opus 5.5 --- ChangeLog.md | 4 ++++ src/lib/libaddfunction.js | 12 ++++++++++++ src/lib/libdylink.js | 18 ++++++++++++++++++ src/lib/libpthread.js | 2 ++ src/preamble.js | 5 +++++ src/runtime_pthread.js | 7 +++++++ test/core/pthread/test_pthread_dylink.c | 3 +++ test/core/pthread/test_pthread_dylink.h | 3 +++ test/core/pthread/test_pthread_dylink_side.c | 4 ++++ tools/link.py | 2 ++ 10 files changed, 60 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 584f498653de8..72d8e06803743 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -39,6 +39,10 @@ See docs/process.md for more on how version tagging works. detected automatically by the compiler, so `__deps` is no longer required for direct JS-to-JS references (`__deps` is still supported for dependencies on compiled C/C++ symbols or indirect dependencies). (#27995) +- In pthreads builds with `MAIN_MODULE`, workers no longer walk the main + module's whole function table, which created a JS function object for every + table entry in every worker. The main thread now passes them the table slots + of the main module's exported functions. 6.0.13 - 10/19/26 ----------------- diff --git a/src/lib/libaddfunction.js b/src/lib/libaddfunction.js index 4f772bc6bfeff..d7838350d4331 100644 --- a/src/lib/libaddfunction.js +++ b/src/lib/libaddfunction.js @@ -124,6 +124,18 @@ addToLibrary({ // First, create the map if this is the first use. if (!functionsInTableMap) { functionsInTableMap = new WeakMap(); +#if MAIN_MODULE && PTHREADS + // Walking the main module's part of the table would create a JS wrapper + // for every function in it, in every worker. The dynamic linker only + // looks up the main module's exported functions, so workers add just the + // slots of those, as recorded by the main thread. + if (ENVIRONMENT_IS_PTHREAD && mainModuleExportSlots) { + for (var slot of mainModuleExportSlots) { + functionsInTableMap.set(getWasmTableEntry(slot), slot); + } + updateTableMap(mainModuleTableSize, {{{ from64Expr('wasmTable.length') }}} - mainModuleTableSize); + } else +#endif updateTableMap(0, {{{ from64Expr('wasmTable.length') }}}); } return functionsInTableMap.get(func) || 0; diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index ca593f7d4337f..c8b07324ec7b6 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -200,6 +200,24 @@ var LibraryDylink = { ; }, +#if PTHREADS + // Called on the main thread once the main module is instantiated. Records + // which table slots hold the main module's exported functions, which new + // workers use in getFunctionAddress. + $recordMainModuleExportSlots__internal: true, + $recordMainModuleExportSlots: (exports) => { + mainModuleTableSize = {{{ from64Expr('wasmTable.length') }}}; + mainModuleExportSlots = []; + for (var symName in exports) { + var value = exports[symName]; + var slot = typeof value === 'function' && getFunctionAddress(value); + if (slot) { + mainModuleExportSlots.push(slot); + } + } + }, +#endif + $updateGOT__internal: true, $updateGOT__docs: '/** @param {boolean=} replace */', $updateGOT: (exports, replace) => { diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index eabb60ac34f87..f615d137ce391 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -435,6 +435,8 @@ var LibraryPThread = { // Share all modules that have been loaded so far. New workers // won't start running threads until these are all loaded. sharedModules, + mainModuleTableSize, + mainModuleExportSlots, #endif #if ASSERTIONS workerID: worker.workerID, diff --git a/src/preamble.js b/src/preamble.js index c6656a977b60f..4a4cfc99909a8 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -798,6 +798,11 @@ function getWasmImports() { assignWasmExports(wasmExports); #if MAIN_MODULE +#if PTHREADS + if (!ENVIRONMENT_IS_PTHREAD) { + recordMainModuleExportSlots(origExports); + } +#endif updateGOT(origExports); #endif diff --git a/src/runtime_pthread.js b/src/runtime_pthread.js index 27996fc16499e..bd9ae42344f83 100644 --- a/src/runtime_pthread.js +++ b/src/runtime_pthread.js @@ -18,6 +18,11 @@ var workerID = 0; // Map of modules to be shared with new threads. This gets populated by the // main thread and shared with all new workers via the initial `load` message. var sharedModules = {}; + +// The main module's initial table size, and the table slots of its exported +// functions. Recorded by the main thread and shared with all new workers via +// the initial `load` message (see getFunctionAddress). +var mainModuleTableSize, mainModuleExportSlots; #endif var startWorker; @@ -73,6 +78,8 @@ if (ENVIRONMENT_IS_PTHREAD) { #if MAIN_MODULE dynamicLibraries = msgData.dynamicLibraries; sharedModules = msgData.sharedModules; + mainModuleTableSize = msgData.mainModuleTableSize; + mainModuleExportSlots = msgData.mainModuleExportSlots; #if RUNTIME_DEBUG dbg(`worker: received ${Object.keys(msgData.sharedModules).length} shared modules: ${Object.keys(msgData.sharedModules)}`); #endif diff --git a/test/core/pthread/test_pthread_dylink.c b/test/core/pthread/test_pthread_dylink.c index 6ab09b24cb07d..49a25adb3ca37 100644 --- a/test/core/pthread/test_pthread_dylink.c +++ b/test/core/pthread/test_pthread_dylink.c @@ -1,3 +1,4 @@ +#include #include #include #include "test_pthread_dylink.h" @@ -13,6 +14,8 @@ void print_addresses() { printf("main_module: &foo_side %p -> %d\n", &foo_side, foo_side); printf("main_module: &bar_main %p\n", &bar_main); printf("main_module: &bar_side %p\n", &bar_side); + // Each function has one address in every module and every thread. + assert(bar_main_addr_side() == &bar_main); } void* thread_main(void* arg) { diff --git a/test/core/pthread/test_pthread_dylink.h b/test/core/pthread/test_pthread_dylink.h index 2367131225ddc..7f17ab3ec0e79 100644 --- a/test/core/pthread/test_pthread_dylink.h +++ b/test/core/pthread/test_pthread_dylink.h @@ -5,3 +5,6 @@ int bar_main(); // Function defined in main int bar_side(); // Function defined in side void print_addresses_side(); + +typedef int (*bar_func)(); +bar_func bar_main_addr_side(); // &bar_main, as seen by side diff --git a/test/core/pthread/test_pthread_dylink_side.c b/test/core/pthread/test_pthread_dylink_side.c index cb33cb6614c76..80ab9d8461ba6 100644 --- a/test/core/pthread/test_pthread_dylink_side.c +++ b/test/core/pthread/test_pthread_dylink_side.c @@ -13,3 +13,7 @@ void print_addresses_side() { printf("side_module: &bar_main %p\n", &bar_main); printf("side_module: &bar_side %p\n", &bar_side); } + +bar_func bar_main_addr_side() { + return &bar_main; +} diff --git a/tools/link.py b/tools/link.py index 9b817534cd5fd..f94874fb43f0a 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1278,6 +1278,8 @@ def limit_incoming_module_api(): settings.INCLUDE_FULL_LIBRARY = 1 # Called from preamble.js once the main module is instantiated. settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$loadDylibs'] + if settings.PTHREADS: + settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$recordMainModuleExportSlots'] # `__stack_pointer` is exported for PIC side modules. `__heap_base` is used # by `$getMemory` in `libdylink.js` (cannot be in `$getMemory__deps` because # `wasm-ld` processes stub library dependencies before synthesizing optional From b9efcac864f9bb91871ac45d6293da0538b65e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=A9ron?= Date: Thu, 29 Oct 2026 23:08:28 +0100 Subject: [PATCH 2/2] Add PR number to ChangeLog Co-Authored-By: Claude Opus 5.5 --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 72d8e06803743..9ba354a4fc94b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -42,7 +42,7 @@ See docs/process.md for more on how version tagging works. - In pthreads builds with `MAIN_MODULE`, workers no longer walk the main module's whole function table, which created a JS function object for every table entry in every worker. The main thread now passes them the table slots - of the main module's exported functions. + of the main module's exported functions. (#28105) 6.0.13 - 10/19/26 -----------------