Skip to content

Commit 09b1d2f

Browse files
committed
Modernize dependency system with Promise-based run blockers
Replace the callback-based dependenciesFulfilled/runDependencies system with a modern async/await approach using "run blockers" (Promises). This modernizes the startup sequence, ensuring preRun and other startup hooks are executed exactly once by pausing and resuming execution flow instead of re-running the entry point. To maintain backward compatibility, a bridge is implemented for the existing addRunDependency and removeRunDependency APIs, routing them through the new Promise-based blocking mechanism. - Make run() in postamble.js async to support await. - Implement $addRunBlocker and $resolveRunBlockers in libcore.js. - Bridge $addRunDependency and $removeRunDependency to use $addRunBlocker. - Wrap runDependencies == 0 check in ASSERTIONS guard to avoid empty block in optimized builds (fixes Closure Compiler warnings). The following (18) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_cxx_ctors1.json: 151867 => 151979 [+112 bytes / +0.07%] codesize/test_codesize_cxx_ctors2.json: 151270 => 151382 [+112 bytes / +0.07%] codesize/test_codesize_cxx_except.json: 195760 => 195873 [+113 bytes / +0.06%] codesize/test_codesize_cxx_except_wasm.json: 166991 => 167103 [+112 bytes / +0.07%] codesize/test_codesize_cxx_except_wasm_legacy.json: 164875 => 164987 [+112 bytes / +0.07%] codesize/test_codesize_cxx_lto.json: 120708 => 120820 [+112 bytes / +0.09%] codesize/test_codesize_cxx_mangle.json: 262239 => 262352 [+113 bytes / +0.04%] codesize/test_codesize_cxx_noexcept.json: 153867 => 153979 [+112 bytes / +0.07%] codesize/test_codesize_cxx_wasmfs.json: 179724 => 179741 [+17 bytes / +0.01%] test/codesize/test_codesize_file_preload.expected.js updated codesize/test_codesize_file_preload.json: 23823 => 23697 [-126 bytes / -0.53%] codesize/test_codesize_files_js_fs.json: 18247 => 18342 [+95 bytes / +0.52%] codesize/test_codesize_hello_O0.json: 38598 => 38578 [-20 bytes / -0.05%] codesize/test_codesize_hello_dylink.json: 44132 => 44228 [+96 bytes / +0.22%] codesize/test_codesize_hello_dylink_all.json: 855885 => 855967 [+82 bytes / +0.01%] test/codesize/test_codesize_minimal_O0.expected.js updated codesize/test_codesize_minimal_O0.json: 19738 => 19658 [-80 bytes / -0.41%] codesize/test_unoptimized_code_size.json: 176480 => 176351 [-129 bytes / -0.07%] Average change: +0.02% (-0.53% - +0.52%) ```
1 parent 8f23d7b commit 09b1d2f

40 files changed

Lines changed: 498 additions & 382 deletions

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ See docs/process.md for more on how version tagging works.
9494
If you have JS code that was depending on this you can transition to using the
9595
`PThread.pthreads` object. (#26998)
9696
- The POSIX `pause()` function will now return 0 rather than EINTR. (#27044)
97+
- Emscripten's startup dependency system was modernized to be promise-based.
98+
These APIs are mostly internal. The new API is called `addRunBlocker`. The
99+
previous APIs (`addRunDependency`/`removeRunDependency`) still exist but are
100+
deprecated. Internal subsystems (including Fetch, pthread pool seeding,
101+
dylib preloading, LZ4 packages, and Filesystem preloading) have been
102+
transitioned to use the new `addRunBlocker` API directly.
97103

98104
5.0.7 - 04/30/26
99105
----------------

site/source/docs/api_reference/emscripten.h.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ Functions
327327
328328
Asynchronously loads a script from a URL.
329329
330-
This integrates with the run dependencies system, so your script can call ``addRunDependency`` multiple times, prepare various asynchronous tasks, and call ``removeRunDependency`` on them; when all are complete (or if there were no run dependencies to begin with), ``onload`` is called. An example use for this is to load an asset module, that is, the output of the file packager.
330+
This integrates with the run-blocker system, so your script can call ``addRunBlocker`` multiple times, with various asynchronous tasks (promises). Only when all are complete will ``onload`` be called. An example use for this is to load an asset module, that is, the output of the file packager.
331331
332332
This function is currently only available in main browser thread, and it will immediately fail by calling the supplied onerror() handler if called in a pthread.
333333

site/source/docs/api_reference/preamble.js.rst

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -267,32 +267,22 @@ Conversion functions — strings, pointers and arrays
267267

268268

269269

270-
Run dependencies
271-
================
272-
273-
Note that generally run dependencies are managed by the file packager and other parts of the system. It is rare for developers to use this API directly.
274-
275-
276-
.. js:function:: addRunDependency(id)
270+
Run blockers (dependencies)
271+
===========================
277272

278-
Adds an ``id`` to the list of run dependencies.
273+
Note that generally run blockers are managed by the file packager and other internal systems. It is rare for developers to use this API directly.
279274

280-
This adds a run dependency and increments the run dependency counter.
275+
.. js:function:: addRunBlocker(promise)
281276

282-
.. COMMENT (not rendered): **HamishW** Remember to link to Execution lifecycle in Browser environment or otherwise link to information on using this. Possibly its own topic.
283-
284-
:param id: An arbitrary id representing the operation.
285-
:type id: String
277+
Adds a promise that must be resolved before the program starts running.
286278

279+
.. js:function:: addRunDependency(id)
287280

281+
Deprecated: Use ``addRunBlocker`` instead
288282

289283
.. js:function:: removeRunDependency(id)
290284

291-
Removes a specified ``id`` from the list of run dependencies.
292-
293-
:param id: The identifier for the specific dependency to be removed (added with :js:func:`addRunDependency`)
294-
:type id: String
295-
285+
Deprecated: When using ``addRunBlocker``, there is no need to manually remove dependencies, just resolve the promise.
296286

297287

298288
Stack trace

site/source/docs/porting/emscripten-runtime-environment.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,11 @@ Execution lifecycle
118118

119119
When an Emscripten-compiled application is loaded, it starts by preparing data in the ``preloading`` phase. Files you marked for :ref:`preloading <emcc-preload-file>` (using ``emcc --preload-file``, or manually from JavaScript with :js:func:`FS.createPreloadedFile`) are set up at this stage.
120120

121-
You can add additional operations with :js:func:`addRunDependency`, which is a counter of all dependencies to be executed before compiled code can run. As these are completed you can call :js:func:`removeRunDependency` to remove the completed dependencies.
121+
You can add additional operations with :js:func:`addRunBlocker`, which takes a promise that will prevent your program's ``main()`` function from running until it is resolved.
122122

123123
.. note:: Generally it is not necessary to add additional operations — preloading is suitable for almost all use cases.
124124

125-
When all dependencies are met, Emscripten will call your programs's ``main()`` function. The ``main()`` function should be used to perform initialization tasks, and will often call :c:func:`emscripten_set_main_loop` (as :ref:`described above <emscripten-runtime-environment-howto-main-loop>`). The main loop function will be then be called at the requested frequency.
125+
When all the blockers are resolved, Emscripten will call your programs's ``main()`` function. The ``main()`` function should be used to perform initialization tasks, and will often call :c:func:`emscripten_set_main_loop` (as :ref:`described above <emscripten-runtime-environment-howto-main-loop>`). The main loop function will be then be called at the requested frequency.
126126

127127
You can affect the operation of the main loop in several ways:
128128

src/Fetch.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -292,28 +292,27 @@ var Fetch = {
292292
},
293293
#endif
294294

295-
async init() {
295+
init() {
296296
Fetch.xhrs = new HandleAllocator();
297297
#if FETCH_SUPPORT_INDEXEDDB
298298
#if PTHREADS
299299
if (ENVIRONMENT_IS_PTHREAD) return;
300300
#endif
301301

302-
addRunDependency('library_fetch_init');
303-
try {
304-
var db = await Fetch.openDatabase('emscripten_filesystem', 1);
302+
addRunBlocker((async () => {
303+
try {
304+
var db = await Fetch.openDatabase('emscripten_filesystem', 1);
305305
#if FETCH_DEBUG
306-
dbg('fetch: IndexedDB successfully opened.');
306+
dbg('fetch: IndexedDB successfully opened.');
307307
#endif
308-
Fetch.dbInstance = db;
309-
} catch (e) {
308+
Fetch.dbInstance = db;
309+
} catch (e) {
310310
#if FETCH_DEBUG
311-
dbg('fetch: IndexedDB open failed.');
311+
dbg('fetch: IndexedDB open failed.');
312312
#endif
313-
Fetch.dbInstance = false;
314-
} finally {
315-
removeRunDependency('library_fetch_init');
316-
}
313+
Fetch.dbInstance = false;
314+
}
315+
})());
317316
#endif // ~FETCH_SUPPORT_INDEXEDDB
318317
}
319318
}

src/lib/libbrowser.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ var LibraryBrowser = {
617617
},
618618

619619
// TODO: currently not callable from a pthread, but immediately calls onerror() if not on main thread.
620-
emscripten_async_load_script__deps: ['$UTF8ToString', '$runDependencies', '$dependenciesFulfilled'],
620+
emscripten_async_load_script__deps: ['$UTF8ToString', '$resolveRunBlockers'],
621621
emscripten_async_load_script: async (url, onload, onerror) => {
622622
url = UTF8ToString(url);
623623
#if PTHREADS
@@ -627,20 +627,14 @@ var LibraryBrowser = {
627627
return;
628628
}
629629
#endif
630-
#if ASSERTIONS
631-
assert(runDependencies === 0, 'async_load_script must be run when no other dependencies are active');
632-
#endif
630+
633631
{{{ runtimeKeepalivePush() }}}
634632

635633
var loadDone = () => {
636634
{{{ runtimeKeepalivePop() }}}
637635
if (onload) {
638636
var onloadCallback = () => callUserCallback({{{ makeDynCall('v', 'onload') }}});
639-
if (runDependencies > 0) {
640-
dependenciesFulfilled = onloadCallback;
641-
} else {
642-
onloadCallback();
643-
}
637+
resolveRunBlockers().then(onloadCallback);
644638
}
645639
}
646640

src/lib/libcore.js

Lines changed: 30 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,126 +2244,50 @@ addToLibrary({
22442244
$wasmMemory: 'memory',
22452245
#endif
22462246

2247-
$getUniqueRunDependency: (id) => {
2248-
#if ASSERTIONS
2249-
var orig = id;
2250-
while (1) {
2251-
if (!runDependencyTracking[id]) return id;
2252-
id = orig + Math.random();
2253-
}
2254-
#else
2255-
return id;
2256-
#endif
2257-
},
2258-
22592247
$noExitRuntime__postset: () => addAtModule(makeModuleReceive('noExitRuntime')),
22602248
$noExitRuntime: {{{ !EXIT_RUNTIME }}},
22612249

2262-
#if !MINIMAL_RUNTIME
2263-
// A counter of dependencies for calling run(). If we need to
2264-
// do asynchronous work before running, increment this and
2265-
// decrement it. Incrementing must happen in a place like
2266-
// Module.preRun (used by emcc to add file preloading).
2267-
// Note that you can add dependencies in preRun, even though
2268-
// it happens right before run - run will be postponed until
2269-
// the dependencies are met.
2270-
$runDependencies__internal: true,
2271-
$runDependencies: 0,
2272-
// overridden to take different actions when all run dependencies are fulfilled
2273-
$dependenciesFulfilled__internal: true,
2274-
$dependenciesFulfilled: null,
2275-
#if ASSERTIONS
2276-
$runDependencyTracking__internal: true,
2277-
$runDependencyTracking: {},
2278-
$runDependencyWatcher__internal: true,
2279-
$runDependencyWatcher: null,
2250+
#if expectToReceiveOnModule('monitorRunDependencies')
2251+
$pendingRunBlockers__internal: true,
2252+
$pendingRunBlockers: 0,
22802253
#endif
22812254

2282-
$addRunDependency__deps: ['$runDependencies', '$removeRunDependency',
2283-
#if ASSERTIONS
2284-
'$runDependencyTracking',
2285-
'$runDependencyWatcher',
2255+
$runBlockers__internal: true,
2256+
$runBlockers: [],
2257+
$addRunBlocker__deps: ['$runBlockers', '$resolveRunBlockers',
2258+
#if expectToReceiveOnModule('monitorRunDependencies')
2259+
'$pendingRunBlockers',
22862260
#endif
22872261
],
2288-
$addRunDependency: (id) => {
2289-
runDependencies++;
2290-
2262+
$addRunBlocker: (promise) => {
22912263
#if expectToReceiveOnModule('monitorRunDependencies')
2292-
Module['monitorRunDependencies']?.(runDependencies);
2293-
#endif
2294-
2295-
#if ASSERTIONS
2296-
#if RUNTIME_DEBUG
2297-
dbg('addRunDependency', id);
2298-
#endif
2299-
assert(id, 'addRunDependency requires an ID')
2300-
assert(!runDependencyTracking[id]);
2301-
runDependencyTracking[id] = 1;
2302-
if (runDependencyWatcher === null && globalThis.setInterval) {
2303-
// Check for missing dependencies every few seconds
2304-
runDependencyWatcher = setInterval(() => {
2305-
if (ABORT) {
2306-
clearInterval(runDependencyWatcher);
2307-
runDependencyWatcher = null;
2308-
return;
2309-
}
2310-
var shown = false;
2311-
for (var dep in runDependencyTracking) {
2312-
if (!shown) {
2313-
shown = true;
2314-
err('still waiting on run dependencies:');
2315-
}
2316-
err(`dependency: ${dep}`);
2317-
}
2318-
if (shown) {
2319-
err('(end of list)');
2320-
}
2321-
}, 10000);
2322-
#if ENVIRONMENT_MAY_BE_NODE
2323-
// Prevent this timer from keeping the runtime alive if nothing
2324-
// else is.
2325-
runDependencyWatcher.unref?.()
2326-
#endif
2264+
if (Module['monitorRunDependencies']) {
2265+
pendingRunBlockers++;
2266+
Module['monitorRunDependencies'](pendingRunBlockers);
2267+
var decrement = () => {
2268+
pendingRunBlockers--;
2269+
Module['monitorRunDependencies'](pendingRunBlockers);
2270+
};
2271+
var wrapped = promise.then(
2272+
(val) => { decrement(); return val; },
2273+
(err) => { decrement(); throw err; }
2274+
);
2275+
runBlockers.push(wrapped);
2276+
return;
23272277
}
23282278
#endif
2279+
runBlockers.push(promise);
23292280
},
23302281

2331-
$removeRunDependency__deps: ['$runDependencies', '$dependenciesFulfilled',
2332-
#if ASSERTIONS
2333-
'$runDependencyTracking',
2334-
'$runDependencyWatcher',
2335-
#endif
2336-
],
2337-
$removeRunDependency: (id) => {
2338-
runDependencies--;
2339-
2340-
#if expectToReceiveOnModule('monitorRunDependencies')
2341-
Module['monitorRunDependencies']?.(runDependencies);
2342-
#endif
2343-
2344-
#if ASSERTIONS
2345-
#if RUNTIME_DEBUG
2346-
dbg('removeRunDependency', id);
2347-
#endif
2348-
assert(id, 'removeRunDependency requires an ID');
2349-
assert(runDependencyTracking[id]);
2350-
delete runDependencyTracking[id];
2351-
#endif
2352-
if (runDependencies == 0) {
2353-
#if ASSERTIONS
2354-
if (runDependencyWatcher !== null) {
2355-
clearInterval(runDependencyWatcher);
2356-
runDependencyWatcher = null;
2357-
}
2358-
#endif
2359-
if (dependenciesFulfilled) {
2360-
var callback = dependenciesFulfilled;
2361-
dependenciesFulfilled = null;
2362-
callback(); // can add another dependenciesFulfilled
2363-
}
2282+
$resolveRunBlockers__internal: true,
2283+
$resolveRunBlockers__deps: ['$runBlockers'],
2284+
$resolveRunBlockers: async () => {
2285+
while (runBlockers.length) {
2286+
var current = runBlockers;
2287+
runBlockers = [];
2288+
await Promise.all(current);
23642289
}
23652290
},
2366-
#endif
23672291

23682292
// The following addOn<X> functions are for adding runtime callbacks at
23692293
// various executions points. Each addOn<X> function has a corresponding

src/lib/libfetch.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ var LibraryFetch = {
1111
$Fetch__deps: [
1212
'$HandleAllocator',
1313
#if FETCH_SUPPORT_INDEXEDDB
14-
'$addRunDependency',
15-
'$removeRunDependency',
14+
'$addRunBlocker',
1615
#endif
1716
],
1817
$Fetch: Fetch,

src/lib/libfs_shared.js

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,14 @@ addToLibrary({
5454
'$asyncLoad',
5555
'$PATH_FS',
5656
'$FS_createDataFile',
57-
'$getUniqueRunDependency',
58-
'$addRunDependency',
59-
'$removeRunDependency',
57+
'$addRunBlocker',
6058
'$FS_handledByPreloadPlugin',
6159
],
62-
$FS_preloadFile: async (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => {
60+
$FS_preloadFile: (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => {
6361
// TODO we should allow people to just pass in a complete filename instead
6462
// of parent and name being that we just join them anyways
6563
var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent;
66-
var dep = getUniqueRunDependency(`cp ${fullname}`); // might have several active requests for the same fullname
67-
addRunDependency(dep);
68-
69-
try {
64+
var promise = (async () => {
7065
var byteArray = url;
7166
if (typeof url == 'string') {
7267
byteArray = await asyncLoad(url);
@@ -77,9 +72,9 @@ addToLibrary({
7772
if (!dontCreateFile) {
7873
FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn);
7974
}
80-
} finally {
81-
removeRunDependency(dep);
82-
}
75+
})();
76+
addRunBlocker(promise);
77+
return promise;
8378
},
8479
#endif
8580

0 commit comments

Comments
 (0)