Skip to content

Commit 5ee7847

Browse files
brendandahlaheejin
andauthored
Unify module splitting implementation for JSPI (#26396)
Previously, module splitting with JSPI required a special built-in function `__load_secondary_module` and wasm-split setting to coordinate the loading of the secondary module. This change updates the `splitModuleProxyHandler` to support JSPI by wrapping the generated placeholder functions in `WebAssembly.Suspending`. The placeholder will then automatically load the secondary module asynchronously. A standalone `save_profile_data.js` script is extracted from existing tests for reuse, and a new test `test_split_module_embind_jspi` is added to ensure module splitting functions correctly alongside embind and JSPI. --------- Co-authored-by: Heejin Ahn <aheejin@gmail.com>
1 parent 43be3a1 commit 5ee7847

11 files changed

Lines changed: 94 additions & 47 deletions

src/lib/libasync.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -514,17 +514,6 @@ addToLibrary({
514514
});
515515
},
516516

517-
_load_secondary_module__sig: 'v',
518-
_load_secondary_module__async: 'auto',
519-
_load_secondary_module: async function() {
520-
// Mark the module as loading for the wasm module (so it doesn't try to load it again).
521-
wasmExports['load_secondary_module_status'].value = 1;
522-
var imports = {'primary': wasmRawExports};
523-
// Replace '.wasm' suffix with '.deferred.wasm'.
524-
var deferred = wasmBinaryFile.slice(0, -5) + '.deferred.wasm';
525-
await instantiateAsync(null, deferred, imports);
526-
},
527-
528517
$Fibers__deps: ['$Asyncify', 'emscripten_stack_set_limits', '$stackRestore'],
529518
$Fibers: {
530519
nextFiber: 0,

src/preamble.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ async function getWasmBinary(binaryFile) {
506506
#endif
507507

508508
#if SPLIT_MODULE
509-
{{{ makeModuleReceiveWithVar('loadSplitModule', undefined, 'instantiateSync') }}}
509+
{{{ makeModuleReceiveWithVar('loadSplitModule', undefined, JSPI ? '(secondaryFile, imports) => instantiateAsync(null, secondaryFile, imports)' : 'instantiateSync') }}}
510510
var splitModuleProxyHandler = {
511511
get(target, moduleName, receiver) {
512512
if (moduleName.startsWith('placeholder')) {
@@ -519,22 +519,23 @@ var splitModuleProxyHandler = {
519519
}
520520
return new Proxy({}, {
521521
get(target, base, receiver) {
522-
return (...args) => {
523-
#if ASYNCIFY == 2
524-
throw new Error('Placeholder function "' + base + '" should not be called when using JSPI.');
525-
#else
522+
let ret = {{{ asyncIf(ASYNCIFY == 2) }}} (...args) => {
526523
#if RUNTIME_DEBUG
527524
dbg(`placeholder function called: ${base}`);
528525
#endif
529526
var imports = {'primary': wasmRawExports};
530527
// Replace '.wasm' suffix with '.deferred.wasm'.
531-
loadSplitModule(secondaryFile, imports, base);
528+
{{{ awaitIf(ASYNCIFY == 2) }}}loadSplitModule(secondaryFile, imports, base);
532529
#if RUNTIME_DEBUG
533530
dbg('instantiated deferred module, continuing');
534531
#endif
535532
return wasmTable.get({{{ toIndexType('base') }}})(...args);
533+
};
534+
#if JSPI
535+
return new WebAssembly.Suspending(ret);
536+
#else
537+
return ret;
536538
#endif
537-
}
538539
}
539540
});
540541
}

test/other/save_profile_data.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
function save_profile_data() {
2+
var __write_profile = wasmExports['__write_profile'];
3+
if (__write_profile) {
4+
var len = __write_profile({{{ to64('0') }}}, 0);
5+
var offset = _malloc(len);
6+
var actualLen = __write_profile({{{ to64('offset') }}}, len);
7+
var profile_data = HEAPU8.subarray(offset, offset + len);
8+
if (typeof writeFile !== 'undefined') {
9+
writeFile('profile.data', profile_data);
10+
} else if (typeof fs !== 'undefined') {
11+
fs.writeFileSync('profile.data', profile_data);
12+
} else {
13+
console.log(JSON.stringify(Array.from(profile_data)));
14+
}
15+
console.log('wrote profile data')
16+
_free(offset);
17+
}
18+
}

test/other/test_split_module.post.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
11
#preprocess
2+
#include "save_profile_data.js"
23

34
function saveProfileData() {
4-
var __write_profile = wasmExports['__write_profile'];
5-
if (__write_profile) {
6-
var len = __write_profile({{{ to64('0') }}}, 0);
7-
var offset = _malloc(len);
8-
var actualLen = __write_profile({{{ to64('offset') }}}, len);
9-
var profile_data = HEAPU8.subarray(offset, offset + len);
10-
if (typeof writeFile !== 'undefined') {
11-
console.log('using writeFile')
12-
writeFile('profile.data', profile_data);
13-
} else if (typeof fs !== 'undefined') {
14-
console.log('using fs.writeFileSync')
15-
fs.writeFileSync('profile.data', profile_data);
16-
} else {
17-
console.log(JSON.stringify(Array.from(profile_data)));
18-
}
19-
console.log('profile size is', actualLen, 'bytes (allocated', len, 'bytes)');
20-
console.log('wrote profile data')
21-
_free(offset);
22-
}
5+
save_profile_data();
236

247
// Say hello *after* recording the profile so that all functions are deferred.
258
var result = _say_hello();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <emscripten.h>
2+
#include <emscripten/bind.h>
3+
4+
int primary_function() {
5+
return 42;
6+
}
7+
8+
int deferred_function() {
9+
return 82;
10+
}
11+
12+
EMSCRIPTEN_BINDINGS(module_splitting) {
13+
emscripten::function("primary_function", &primary_function);
14+
emscripten::function("deferred_function", &deferred_function, emscripten::async());
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#preprocess
2+
#include "save_profile_data.js"
3+
4+
async function saveProfileData() {
5+
console.log('primary_function: ' + Module.primary_function());
6+
save_profile_data();
7+
// deferred_function *after* recording the profile so that all functions are deferred.
8+
var result = Module.deferred_function();
9+
console.log('deferred_function: ' + result);
10+
console.log('deferred_function await: ' + await result);
11+
}
12+
13+
addOnPostRun(saveProfileData);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Module["loadSplitModule"] = async function(deferred, imports, prop) {
2+
console.log('Custom handler for loading split module.');
3+
4+
return instantiateAsync(null, deferred, imports);
5+
}

test/test_other.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12376,8 +12376,6 @@ def test_split_module(self, customLoader, jspi, opt):
1237612376
wasm_split_run = [wasm_split, '-g',
1237712377
'--enable-mutable-globals', '--enable-bulk-memory', '--enable-nontrapping-float-to-int',
1237812378
'--export-prefix=%', 'test_split_module.wasm.orig', '-o1', 'primary.wasm', '-o2', 'secondary.wasm', '--profile=profile.data']
12379-
if jspi:
12380-
wasm_split_run += ['--jspi', '--enable-reference-types']
1238112379
if self.get_setting('MEMORY64'):
1238212380
wasm_split_run += ['--enable-memory64']
1238312381
self.run_process(wasm_split_run)
@@ -12435,6 +12433,38 @@ def test_split_main_module(self):
1243512433
self.assertIn('Hello from main!', result)
1243612434
self.assertIn('Hello from lib!', result)
1243712435

12436+
@also_with_wasm64
12437+
@requires_jspi
12438+
def test_split_module_embind_jspi(self):
12439+
self.set_setting('SPLIT_MODULE')
12440+
self.cflags += ['-Wno-experimental']
12441+
self.cflags += ['--post-js', test_file('other/test_split_module_embind_jspi.post.js')]
12442+
self.cflags += ['--pre-js', test_file('other/test_split_module_embind_jspi.pre.js')]
12443+
self.cflags += ['-sEXPORTED_FUNCTIONS=_malloc,_free']
12444+
self.cflags += ['-lembind']
12445+
expected_pre_split_output = 'primary_function: 42\n'
12446+
expected_post_split_output = ('deferred_function: [object Promise]\n'
12447+
'deferred_function await: 82\n')
12448+
expected_output = (expected_pre_split_output +
12449+
'wrote profile data\n' +
12450+
expected_post_split_output)
12451+
result = self.do_runf('other/test_split_module_embind_jspi.cpp', expected_output=expected_output)
12452+
self.assertExists('test_split_module_embind_jspi.wasm')
12453+
self.assertExists('test_split_module_embind_jspi.wasm.orig')
12454+
self.assertExists('profile.data')
12455+
12456+
wasm_split = os.path.join(building.get_binaryen_bin(), 'wasm-split')
12457+
wasm_split_run = [wasm_split, '-g',
12458+
'--enable-mutable-globals', '--enable-bulk-memory', '--enable-nontrapping-float-to-int',
12459+
'--export-prefix=%', 'test_split_module_embind_jspi.wasm.orig', '-o1', 'test_split_module_embind_jspi.wasm', '-o2', 'test_split_module_embind_jspi.deferred.wasm', '--profile=profile.data']
12460+
self.run_process(wasm_split_run)
12461+
result = self.run_js('test_split_module_embind_jspi.js')
12462+
self.assertNotIn('profile', result)
12463+
self.assertIn((expected_pre_split_output +
12464+
'Custom handler for loading split module.\n' +
12465+
expected_post_split_output),
12466+
result)
12467+
1243812468
@crossplatform
1243912469
@flaky('https://github.com/emscripten-core/emscripten/issues/25206')
1244012470
def test_gen_struct_info(self):

tools/emscripten.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -842,11 +842,6 @@ def add_standard_wasm_imports(send_items_map):
842842
'memory_grow_post',
843843
]
844844

845-
if settings.SPLIT_MODULE and settings.ASYNCIFY == 2:
846-
# Calls to this function are generated by binaryen so it must be manually
847-
# imported.
848-
extra_sent_items.append('__load_secondary_module')
849-
850845
for s in extra_sent_items:
851846
send_items_map[s] = s
852847

tools/link.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,8 +1635,6 @@ def limit_incoming_module_api():
16351635

16361636
if settings.SPLIT_MODULE:
16371637
settings.INCOMING_MODULE_JS_API += ['loadSplitModule']
1638-
if settings.ASYNCIFY == 2:
1639-
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['_load_secondary_module']
16401638

16411639
# wasm side modules have suffix .wasm
16421640
if settings.SIDE_MODULE and utils.suffix(target) in ('.js', '.mjs'):

0 commit comments

Comments
 (0)