From 4bc646cc1b1b133503025c9f2506d4adb80bfe69 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Sun, 1 Nov 2026 00:29:16 +0000 Subject: [PATCH 1/9] Make dlsym O(1) in the number of exports `_dlsym_js` enumerated every export of the library on each call, `Object.keys(lib.exports).indexOf(symbol)`, before doing anything else. For `RTLD_DEFAULT` on a main module that is `wasmImports`, which holds every export of the main module, so each `dlsym` call cost O(N log N) in the number of exported symbols, whether the symbol exists or not. The index is only needed when `addFunction` allocates a new table slot and the other threads replay the insertion by index (`_dlsym_catchup_js`). Test membership with `hasOwnProperty`, which gives the same answer as `Object.keys(...).indexOf(symbol) != -1` for these plain objects, and compute the enumeration index only in the branch that stores it into `symbolIndex`. A main module with 250,000 exported functions goes from 80.9 ms to 0.009 ms per `dlsym` call. Co-Authored-By: Claude Fable 5.1 --- AUTHORS | 1 + ChangeLog.md | 4 ++++ src/lib/libdylink.js | 20 +++++++++----------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index 213b805baa8e2..f67f9e4983ef5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -606,3 +606,4 @@ a license to everyone to use it as detailed in LICENSE.) * Abdelrahman Teima * Jaap Aarts * Dimokritos Kolitsos +* Bas van Dijk diff --git a/ChangeLog.md b/ChangeLog.md index 19d13ac143729..9696ac18a90fd 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -40,6 +40,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) +- `dlsym` no longer enumerates every export of the library on each call, which + made a lookup in a main module with hundreds of thousands of exports take tens + of milliseconds; the index is only computed when a new table slot is + allocated. 6.0.13 - 10/19/26 ----------------- diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 7437339180719..d8cdb2122d843 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -1307,27 +1307,22 @@ var LibraryDylink = { #if DYLINK_DEBUG dbg('dlsym_js:', symbol); #endif - var result; - var newSymIndex; - var lib = LDSO.loadedLibsByHandle[handle]; #if ASSERTIONS assert(lib, `Tried to dlsym() from an unopened handle: ${handle}`); #endif - newSymIndex = Object.keys(lib.exports).indexOf(symbol); - if (newSymIndex == -1 || lib.exports[symbol].stub) { + if (!Object.prototype.hasOwnProperty.call(lib.exports, symbol) || lib.exports[symbol].stub) { dlSetError(`Tried to lookup unknown symbol "${symbol}" in dynamic lib: ${lib.name}`) return 0; } + var exportName = symbol; #if !WASM_BIGINT var origSym = 'orig$' + symbol; - result = lib.exports[origSym]; - if (result) { - newSymIndex = Object.keys(lib.exports).indexOf(origSym); + if (lib.exports[origSym]) { + exportName = origSym; } - else #endif - result = lib.exports[symbol]; + var result = lib.exports[exportName]; if (typeof result === 'function') { #if DYLINK_DEBUG @@ -1355,7 +1350,10 @@ var LibraryDylink = { #if DYLINK_DEBUG dbg('adding symbol to table:', symbol); #endif - {{{ makeSetValue('symbolIndex', 0, 'newSymIndex', '*') }}}; + // Other threads replay this insertion by the symbol's enumeration index + // (`_dlsym_catchup_js`). Computing it enumerates every export of the + // library, so only do so when a slot was actually allocated. + {{{ makeSetValue('symbolIndex', 0, 'Object.keys(lib.exports).indexOf(exportName)', '*') }}}; } } #if DYLINK_DEBUG From c0bb84f01217693309155064494938e5695ae20c Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Sun, 1 Nov 2026 21:07:49 +0000 Subject: [PATCH 2/9] Keep newSymIndex on its own line in _dlsym_js Review feedback: compute the enumeration index into `newSymIndex` so the `makeSetValue` line stays simple. Co-Authored-By: Claude Opus 5.5 --- src/lib/libdylink.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index d8cdb2122d843..29c0614c25b81 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -1353,7 +1353,8 @@ var LibraryDylink = { // Other threads replay this insertion by the symbol's enumeration index // (`_dlsym_catchup_js`). Computing it enumerates every export of the // library, so only do so when a slot was actually allocated. - {{{ makeSetValue('symbolIndex', 0, 'Object.keys(lib.exports).indexOf(exportName)', '*') }}}; + var newSymIndex = Object.keys(lib.exports).indexOf(exportName); + {{{ makeSetValue('symbolIndex', 0, 'newSymIndex', '*') }}}; } } #if DYLINK_DEBUG From 7fc5485454ae3188606f167631a340c435cada47 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Sun, 1 Nov 2026 21:42:38 +0000 Subject: [PATCH 3/9] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (1) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_hello_dylink_all.json: 862336 => 862390 [+54 bytes / +0.01%] Average change: +0.01% (+0.01% - +0.01%) ``` Co-Authored-By: Claude Opus 5.5 --- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index b27fcd6720c2d..ea3f9d9c34bf3 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 272893, + "a.out.js": 272947, "a.out.nodebug.wasm": 589443, - "total": 862336, + "total": 862390, "sent": [ "IMG_Init", "IMG_Load", From be5b072f90661f334aeecb463d8e5da54f238580 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Sun, 1 Nov 2026 23:00:30 +0100 Subject: [PATCH 4/9] Apply suggestion from @basvandijk --- src/lib/libdylink.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 29c0614c25b81..c2dddee47f9b4 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -1351,8 +1351,8 @@ var LibraryDylink = { dbg('adding symbol to table:', symbol); #endif // Other threads replay this insertion by the symbol's enumeration index - // (`_dlsym_catchup_js`). Computing it enumerates every export of the - // library, so only do so when a slot was actually allocated. + // (`_dlsym_catchup_js`). Using `indexOf` here is O(N), + // so only do so when a slot was actually allocated. var newSymIndex = Object.keys(lib.exports).indexOf(exportName); {{{ makeSetValue('symbolIndex', 0, 'newSymIndex', '*') }}}; } From b611cb3d87c003e05a2f4cd8f38d4867ab6af345 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 2 Nov 2026 01:03:36 +0100 Subject: [PATCH 5/9] Apply suggestion from @basvandijk --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index f67f9e4983ef5..75fdda50b4ec7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -606,4 +606,4 @@ a license to everyone to use it as detailed in LICENSE.) * Abdelrahman Teima * Jaap Aarts * Dimokritos Kolitsos -* Bas van Dijk +* Bas van Dijk From 122a5f3811a25bab9c44ee0cd4f08cb21b57fe43 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 2 Nov 2026 19:49:51 +0100 Subject: [PATCH 6/9] chore: remove Changelog.md entry --- ChangeLog.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 9696ac18a90fd..19d13ac143729 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -40,10 +40,6 @@ 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) -- `dlsym` no longer enumerates every export of the library on each call, which - made a lookup in a main module with hundreds of thousands of exports take tens - of milliseconds; the index is only computed when a new table slot is - allocated. 6.0.13 - 10/19/26 ----------------- From df9bde2a540616bb7c69ed4a9493fa5de73f3156 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 2 Nov 2026 19:06:00 +0000 Subject: [PATCH 7/9] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (1) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_hello_dylink_all.json: 862405 => 862459 [+54 bytes / +0.01%] Average change: +0.01% (+0.01% - +0.01%) ``` Co-Authored-By: Claude Opus 5.5 --- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 8f2744dd18d0b..694b5290de989 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 272893, + "a.out.js": 272947, "a.out.nodebug.wasm": 589512, - "total": 862405, + "total": 862459, "sent": [ "IMG_Init", "IMG_Load", From 57568eb376cc47a6b75323c933f682a6d6ee67ee Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 2 Nov 2026 21:13:32 +0000 Subject: [PATCH 8/9] Use the hasOwn helper in _dlsym_js #28129 added `$hasOwn` to libcore.js. It aliases `Object.hasOwn` when the minimum browser versions support it and falls back to `Object.prototype.hasOwnProperty.call` otherwise. Co-Authored-By: Claude Opus 5.5 --- src/lib/libdylink.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 5f5d96fb141b0..45a27c5834c44 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -1311,7 +1311,7 @@ var LibraryDylink = { #if ASSERTIONS assert(lib, `Tried to dlsym() from an unopened handle: ${handle}`); #endif - if (!Object.prototype.hasOwnProperty.call(lib.exports, symbol) || lib.exports[symbol].stub) { + if (!hasOwn(lib.exports, symbol) || lib.exports[symbol].stub) { dlSetError(`Tried to lookup unknown symbol "${symbol}" in dynamic lib: ${lib.name}`) return 0; } From 57a3884ddb4be941172011188a750bb4bb051921 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Mon, 2 Nov 2026 21:20:29 +0000 Subject: [PATCH 9/9] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (1) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_hello_dylink_all.json: 862442 => 862462 [+20 bytes / +0.00%] Average change: +0.00% (+0.00% - +0.00%) ``` Co-Authored-By: Claude Opus 5.5 --- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 0c03a480d8bfd..6826cfa3a0965 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 272930, + "a.out.js": 272950, "a.out.nodebug.wasm": 589512, - "total": 862442, + "total": 862462, "sent": [ "IMG_Init", "IMG_Load",