Skip to content

Commit b66f862

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).
1 parent 999496e commit b66f862

72 files changed

Lines changed: 609 additions & 603 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ See docs/process.md for more on how version tagging works.
6363
- The `PThread.runningWorkers` field was removed from the `PThread` object.
6464
If you have JS code that was depending on this you can transition to using the
6565
`PThread.pthreads` object. (#26998)
66+
- Emscripten's startup dependency system was modernized to be promise-based.
67+
These APIs are mostly internal. The new API is called `addRunBlocker`. The
68+
previous APIs (`addRunDependency`/`removeRunDependency`) still exist but are
69+
deprecated. Internal subsystems (including Fetch, pthread pool seeding,
70+
dylib preloading, LZ4 packages, and Filesystem preloading) have been
71+
transitioned to use the new `addRunBlocker` API directly.
6672

6773
5.0.7 - 04/30/26
6874
----------------

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: Use ``addRunBlocker`` instead
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 ``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 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
@@ -614,7 +614,7 @@ var LibraryBrowser = {
614614
},
615615

616616
// TODO: currently not callable from a pthread, but immediately calls onerror() if not on main thread.
617-
emscripten_async_load_script__deps: ['$UTF8ToString'],
617+
emscripten_async_load_script__deps: ['$UTF8ToString', '$resolveRunBlockers'],
618618
emscripten_async_load_script: async (url, onload, onerror) => {
619619
url = UTF8ToString(url);
620620
#if PTHREADS
@@ -624,20 +624,14 @@ var LibraryBrowser = {
624624
return;
625625
}
626626
#endif
627-
#if ASSERTIONS
628-
assert(runDependencies === 0, 'async_load_script must be run when no other dependencies are active');
629-
#endif
627+
630628
{{{ runtimeKeepalivePush() }}}
631629

632630
var loadDone = () => {
633631
{{{ runtimeKeepalivePop() }}}
634632
if (onload) {
635633
var onloadCallback = () => callUserCallback({{{ makeDynCall('v', 'onload') }}});
636-
if (runDependencies > 0) {
637-
dependenciesFulfilled = onloadCallback;
638-
} else {
639-
onloadCallback();
640-
}
634+
resolveRunBlockers().then(onloadCallback);
641635
}
642636
}
643637

src/lib/libcore.js

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

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

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

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

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

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

src/lib/libdylink.js

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,8 +1235,8 @@ var LibraryDylink = {
12351235
},
12361236

12371237
$loadDylibs__internal: true,
1238-
$loadDylibs__deps: ['$loadDynamicLibrary', '$reportUndefinedSymbols', '$addRunDependency', '$removeRunDependency'],
1239-
$loadDylibs: async () => {
1238+
$loadDylibs__deps: ['$loadDynamicLibrary', '$reportUndefinedSymbols', '$addRunBlocker'],
1239+
$loadDylibs: () => {
12401240
if (!dynamicLibraries.length) {
12411241
#if DYLINK_DEBUG
12421242
dbg('loadDylibs: no libraries to preload');
@@ -1248,19 +1248,17 @@ var LibraryDylink = {
12481248
#if DYLINK_DEBUG
12491249
dbg('loadDylibs:', dynamicLibraries);
12501250
#endif
1251-
addRunDependency('loadDylibs');
1252-
1253-
// Load binaries asynchronously
1254-
for (var lib of dynamicLibraries) {
1255-
await loadDynamicLibrary(lib, {loadAsync: true, global: true, nodelete: true, allowUndefined: true})
1256-
}
1257-
// we got them all, wonderful
1258-
reportUndefinedSymbols();
1259-
1251+
addRunBlocker((async () => {
1252+
// Load binaries asynchronously
1253+
for (var lib of dynamicLibraries) {
1254+
await loadDynamicLibrary(lib, {loadAsync: true, global: true, nodelete: true, allowUndefined: true})
1255+
}
1256+
// we got them all, wonderful
1257+
reportUndefinedSymbols();
12601258
#if DYLINK_DEBUG
1261-
dbg('loadDylibs done!');
1259+
dbg('loadDylibs done!');
12621260
#endif
1263-
removeRunDependency('loadDylibs');
1261+
})());
12641262
},
12651263

12661264
// void* dlopen(const char* filename, int flags);

src/lib/libfetch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
var LibraryFetch = {
1010
$Fetch__postset: 'Fetch.init();',
11-
$Fetch__deps: ['$HandleAllocator'],
11+
$Fetch__deps: ['$HandleAllocator', '$addRunBlocker'],
1212
$Fetch: Fetch,
1313
_emscripten_fetch_get_response_headers_length__deps: ['$lengthBytesUTF8'],
1414
_emscripten_fetch_get_response_headers_length: fetchGetResponseHeadersLength,

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)