Skip to content

Commit

Permalink
Share side modules with worker threads via postMessage
Browse files Browse the repository at this point in the history
Any side modules that are loaded at the time of worker creation are
shared with the worker via postMessage.

As a followup we should extend this to modules that are loaded after
the worker is created but before the pthread runs (for example when
a module is loaded while a worker is unused).
  • Loading branch information
sbc100 committed May 24, 2023
1 parent f512e51 commit e045661
Show file tree
Hide file tree
Showing 14 changed files with 150 additions and 106 deletions.
3 changes: 3 additions & 0 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2076,7 +2076,10 @@ def phase_linker_setup(options, state, newargs):
assert not settings.SIDE_MODULE
if settings.MAIN_MODULE == 1:
settings.INCLUDE_FULL_LIBRARY = 1
# Called from preamble.js once the main module is instantiated.
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$loadDylibs']
if settings.STACK_OVERFLOW_CHECK == 2:
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$setDylinkStackLimits']
settings.REQUIRED_EXPORTS += ['malloc']

if settings.MAIN_MODULE == 1 or settings.SIDE_MODULE == 1:
Expand Down
132 changes: 81 additions & 51 deletions src/library_dylink.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ var LibraryDylink = {
// than just running the promises in parallel, this makes a chain of
// promises to run in series.
wasmPlugin['promiseChainEnd'] = wasmPlugin['promiseChainEnd'].then(
() => loadWebAssemblyModule(byteArray, {loadAsync: true, nodelete: true})).then(
() => loadWebAssemblyModule(byteArray, {loadAsync: true, nodelete: true}, name)).then(
(exports) => {
#if DYLINK_DEBUG
dbg(`registering preloadedWasm: ${name}`);
Expand Down Expand Up @@ -66,13 +66,47 @@ var LibraryDylink = {
return true;
},

// Dynamic version of shared.py:make_invoke. This is needed for invokes
// that originate from side modules since these are not known at JS
// generation time.
#if !DISABLE_EXCEPTION_CATCHING || SUPPORT_LONGJMP == 'emscripten'
$createInvokeFunction__internal: true,
$createInvokeFunction__deps: ['$dynCall', 'setThrew'],
$createInvokeFunction: function(sig) {
return function() {
var sp = stackSave();
try {
return dynCall(sig, arguments[0], Array.prototype.slice.call(arguments, 1));
} catch(e) {
stackRestore(sp);
// Create a try-catch guard that rethrows the Emscripten EH exception.
#if EXCEPTION_STACK_TRACES
// Exceptions thrown from C++ and longjmps will be an instance of
// EmscriptenEH.
if (!(e instanceof EmscriptenEH)) throw e;
#else
// Exceptions thrown from C++ will be a pointer (number) and longjmp
// will throw the number Infinity. Use the compact and fast "e !== e+0"
// test to check if e was not a Number.
if (e !== e+0) throw e;
#endif
_setThrew(1, 0);
}
}
},
#endif

// Resolve a global symbol by name. This is used during module loading to
// resolve imports, and by `dlsym` when used with `RTLD_DEFAULT`.
// Returns both the resolved symbol (i.e. a function or a global) along with
// the canonical name of the symbol (in some cases is modify the symbol as
// part of the loop process, so that actual symbol looked up has a different
// name).
$resolveGlobalSymbol__deps: ['$isSymbolDefined'],
$resolveGlobalSymbol__deps: ['$isSymbolDefined',
#if !DISABLE_EXCEPTION_CATCHING || SUPPORT_LONGJMP == 'emscripten'
'$createInvokeFunction',
#endif
],
$resolveGlobalSymbol__internal: true,
$resolveGlobalSymbol: function(symName, direct = false) {
var sym;
Expand Down Expand Up @@ -325,7 +359,12 @@ var LibraryDylink = {
loadedLibsByName: {},
// handle -> dso; Used by dlsym
loadedLibsByHandle: {},
init: () => newDSO('__main__', {{{ cDefs.RTLD_DEFAULT }}}, wasmImports),
init: () => {
#if ASSERTIONS
assert(wasmImports);
#endif
newDSO('__main__', {{{ cDefs.RTLD_DEFAULT }}}, wasmImports);
},
},

$dlSetError__internal: true,
Expand All @@ -340,36 +379,6 @@ var LibraryDylink = {
});
},

// Dynamic version of shared.py:make_invoke. This is needed for invokes
// that originate from side modules since these are not known at JS
// generation time.
#if !DISABLE_EXCEPTION_CATCHING || SUPPORT_LONGJMP == 'emscripten'
$createInvokeFunction__internal: true,
$createInvokeFunction__deps: ['$dynCall', 'setThrew'],
$createInvokeFunction: function(sig) {
return function() {
var sp = stackSave();
try {
return dynCall(sig, arguments[0], Array.prototype.slice.call(arguments, 1));
} catch(e) {
stackRestore(sp);
// Create a try-catch guard that rethrows the Emscripten EH exception.
#if EXCEPTION_STACK_TRACES
// Exceptions thrown from C++ and longjmps will be an instance of
// EmscriptenEH.
if (!(e instanceof EmscriptenEH)) throw e;
#else
// Exceptions thrown from C++ will be a pointer (number) and longjmp
// will throw the number Infinity. Use the compact and fast "e !== e+0"
// test to check if e was not a Number.
if (e !== e+0) throw e;
#endif
_setThrew(1, 0);
}
}
},
#endif

// We support some amount of allocation during startup in the case of
// dynamic linking, which needs to allocate memory for dynamic libraries that
// are loaded. That has to happen before the main program can start to run,
Expand Down Expand Up @@ -607,6 +616,7 @@ var LibraryDylink = {
// promise that resolves to its exports if the loadAsync flag is set.
$loadWebAssemblyModule__docs: `
/**
* @param {string=} libName
* @param {Object=} localScope
* @param {number=} handle
*/`,
Expand All @@ -617,11 +627,11 @@ var LibraryDylink = {
'$alignMemory', '$zeroMemory',
'$currentModuleWeakSymbols', '$alignMemory', '$zeroMemory',
'$updateTableMap',
#if !DISABLE_EXCEPTION_CATCHING || SUPPORT_LONGJMP == 'emscripten'
'$createInvokeFunction',
#endif
],
$loadWebAssemblyModule: function(binary, flags, localScope, handle) {
$loadWebAssemblyModule: function(binary, flags, libName, localScope, handle) {
#if DYLINK_DEBUG
dbg(`loadWebAssemblyModule: ${libName}`);
#endif
var metadata = getDylinkMetadata(binary);
currentModuleWeakSymbols = metadata.weakImports;
#if ASSERTIONS
Expand Down Expand Up @@ -744,10 +754,20 @@ var LibraryDylink = {
'{{{ WASI_MODULE_NAME }}}': proxy,
};

function postInstantiation(instance) {
function postInstantiation(module, instance) {
#if ASSERTIONS
// the table should be unchanged
assert(wasmTable === originalTable);
#endif
#if PTHREADS
if (!ENVIRONMENT_IS_PTHREAD && libName) {
#if DYLINK_DEBUG
dbg(`registering sharedModules: ${libName}`)
#endif
// cache all loaded modules in `sharedModules`, which gets passed
// to new workers when they are created.
sharedModules[libName] = module;
}
#endif
// add new entries to functionsInTableMap
updateTableMap(tableBase, metadata.tableSize);
Expand All @@ -760,12 +780,11 @@ var LibraryDylink = {
}
#if STACK_OVERFLOW_CHECK >= 2
if (moduleExports['__set_stack_limits']) {
#if PTHREADS
// When we are on an uninitialized pthread we delay calling
// __set_stack_limits until $setDylinkStackLimits.
if (!ENVIRONMENT_IS_PTHREAD || runtimeInitialized)
#endif
moduleExports['__set_stack_limits']({{{ to64('_emscripten_stack_get_base()') }}}, {{{ to64('_emscripten_stack_get_end()') }}});
if (runtimeInitialized) {
moduleExports['__set_stack_limits']({{{ to64('_emscripten_stack_get_base()') }}}, {{{ to64('_emscripten_stack_get_end()') }}});
}
}
#endif

Expand Down Expand Up @@ -838,16 +857,16 @@ var LibraryDylink = {
if (flags.loadAsync) {
if (binary instanceof WebAssembly.Module) {
var instance = new WebAssembly.Instance(binary, info);
return Promise.resolve(postInstantiation(instance));
return Promise.resolve(postInstantiation(binary, instance));
}
return WebAssembly.instantiate(binary, info).then(
(result) => postInstantiation(result.instance)
(result) => postInstantiation(result.module, result.instance)
);
}

var module = binary instanceof WebAssembly.Module ? binary : new WebAssembly.Module(binary);
var instance = new WebAssembly.Instance(module, info);
return postInstantiation(instance);
return postInstantiation(module, instance);
}

// now load needed libraries and the module itself.
Expand All @@ -863,7 +882,7 @@ var LibraryDylink = {
return loadModule();
},

#if STACK_OVERFLOW_CHECK >= 2 && PTHREADS
#if STACK_OVERFLOW_CHECK >= 2
// With PTHREADS we load libraries before we are running a pthread and
// therefore before we have a stack. Instead we delay calling
// `__set_stack_limits` until we start running a thread. We also need to call
Expand Down Expand Up @@ -964,6 +983,16 @@ var LibraryDylink = {

// libName -> libData
function loadLibData() {
#if PTHREADS
var sharedMod = sharedModules[libName];
#if DYLINK_DEBUG
dbg(`checking sharedModules: ${libName}: ${sharedMod ? 'found' : 'not found'}`);
#endif
if (sharedMod) {
return flags.loadAsync ? Promise.resolve(sharedMod) : sharedMod;
}
#endif

// for wasm, we can use fetch for async, but for fs mode we can only imitate it
if (handle) {
var data = {{{ makeGetValue('handle', C_STRUCTS.dso.file_data, '*') }}};
Expand Down Expand Up @@ -1003,10 +1032,10 @@ var LibraryDylink = {

// module not preloaded - load lib data and create new module from it
if (flags.loadAsync) {
return loadLibData().then((libData) => loadWebAssemblyModule(libData, flags, localScope, handle));
return loadLibData().then((libData) => loadWebAssemblyModule(libData, flags, libName, localScope, handle));
}

return loadWebAssemblyModule(loadLibData(), flags, localScope, handle);
return loadWebAssemblyModule(loadLibData(), flags, libName, localScope, handle);
}

// module for lib is loaded - update the dso & global namespace
Expand Down Expand Up @@ -1039,9 +1068,6 @@ var LibraryDylink = {
$loadDylibs__internal: true,
$loadDylibs__deps: ['$loadDynamicLibrary', '$reportUndefinedSymbols'],
$loadDylibs: function() {
#if DYLINK_DEBUG
dbg(`loadDylibs: ${dynamicLibraries}`);
#endif
if (!dynamicLibraries.length) {
#if DYLINK_DEBUG
dbg('loadDylibs: no libraries to preload');
Expand All @@ -1050,6 +1076,10 @@ var LibraryDylink = {
return;
}

#if DYLINK_DEBUG
dbg(`loadDylibs: ${dynamicLibraries}`);
#endif

// Load binaries asynchronously
addRunDependency('loadDylibs');
dynamicLibraries.reduce((chain, lib) => {
Expand Down
13 changes: 12 additions & 1 deletion src/library_pthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ var LibraryPThread = {
PThread.allocateUnusedWorker();
}
#endif
#if !MINIMAL_RUNTIME
// MINIMAL_RUNTIME takes care of calling loadWasmModuleToAllWorkers
// in postamble_minimal.js
addOnPreRun(() => {
addRunDependency('loading-workers')
PThread.loadWasmModuleToAllWorkers(() => removeRunDependency('loading-workers'));
});
#endif
#if MAIN_MODULE
PThread.outstandingPromises = {};
// Finished threads are threads that have finished running but we not yet
Expand Down Expand Up @@ -402,7 +410,9 @@ var LibraryPThread = {
'wasmOffsetConverter': wasmOffsetConverter,
#endif
#if MAIN_MODULE
'dynamicLibraries': Module['dynamicLibraries'],
// Shared all modules that have been loaded so far. New workers
// won't start running threads until these are all loaded.
'sharedModules': sharedModules,
#endif
#if ASSERTIONS
'workerID': worker.workerID,
Expand All @@ -423,6 +433,7 @@ var LibraryPThread = {
) {
return onMaybeReady();
}

let pthreadPoolReady = Promise.all(PThread.unusedWorkers.map(PThread.loadWasmModuleToWorker));
#if PTHREAD_POOL_DELAY_LOAD
// PTHREAD_POOL_DELAY_LOAD means we want to proceed synchronously without
Expand Down
2 changes: 1 addition & 1 deletion src/parseTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,7 @@ function makeRemovedModuleAPIAssert(moduleName, localName) {
function checkReceiving(name) {
// ALL_INCOMING_MODULE_JS_API contains all valid incoming module API symbols
// so calling makeModuleReceive* with a symbol not in this list is an error
assert(ALL_INCOMING_MODULE_JS_API.has(name));
assert(ALL_INCOMING_MODULE_JS_API.has(name), `${name} is not part of INCOMING_MODULE_JS_API`);
}

// Make code to receive a value on the incoming Module object.
Expand Down
30 changes: 6 additions & 24 deletions src/postamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,10 @@ function stackCheckInit() {
}
#endif

#if RELOCATABLE
var dylibsLoaded = false;
#if '$LDSO' in addedLibraryItems
LDSO.init();
#endif
#if MAIN_MODULE && PTHREADS
// Map of modules to be shared with new threads. This gets populated by the
// main thread an shared with all new workers.
var sharedModules = Module['sharedModules'] ||[];
#endif

#if MAIN_READS_PARAMS
Expand All @@ -158,25 +157,8 @@ function run() {
stackCheckInit();
#endif

#if RELOCATABLE
if (!dylibsLoaded) {
// Loading of dynamic libraries needs to happen on each thread, so we can't
// use the normal __ATPRERUN__ mechanism.
#if MAIN_MODULE
loadDylibs();
#else
reportUndefinedSymbols();
#endif
dylibsLoaded = true;

// Loading dylibs can add run dependencies.
if (runDependencies > 0) {
#if RUNTIME_DEBUG
dbg('loadDylibs added run() dependencies, not running yet');
#endif
return;
}
}
#if RELOCATABLE && !MAIN_MODULE
reportUndefinedSymbols();
#endif

#if WASM_WORKERS
Expand Down
15 changes: 8 additions & 7 deletions src/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,12 @@ function initRuntime() {

#if STACK_OVERFLOW_CHECK >= 2
#if RUNTIME_DEBUG
dbg('__set_stack_limits: ' + _emscripten_stack_get_base() + ', ' + _emscripten_stack_get_end());
dbg(`__set_stack_limits: ${ptrToString(_emscripten_stack_get_base())}, ${ptrToString(_emscripten_stack_get_end())}`);
#endif
___set_stack_limits(_emscripten_stack_get_base(), _emscripten_stack_get_end());
#if MAIN_MODULE
setDylinkStackLimits(_emscripten_stack_get_base(), _emscripten_stack_get_end());
#endif
#endif
#if RELOCATABLE
callRuntimeCallbacks(__RELOC_FUNCS__);
Expand Down Expand Up @@ -980,6 +983,10 @@ function createWasm() {
}
#endif
mergeLibSymbols(exports, 'main')
#if '$LDSO' in addedLibraryItems
LDSO.init();
#endif
loadDylibs();
#endif

#if MEMORY64
Expand Down Expand Up @@ -1047,13 +1054,7 @@ function createWasm() {
// We now have the Wasm module loaded up, keep a reference to the compiled module so we can post it to the workers.
wasmModule = module;
#endif

#if PTHREADS
PThread.loadWasmModuleToAllWorkers(() => removeRunDependency('wasm-instantiate'));
#else // singlethreaded build:
removeRunDependency('wasm-instantiate');
#endif // ~PTHREADS

return exports;
}
// wait for the pthread pool (if any)
Expand Down
Loading

0 comments on commit e045661

Please sign in to comment.