Repository navigation
Free a side module's file data once all threads have loaded it - #28007
Open
rustyconover wants to merge 1 commit into
Open
rustyconover wants to merge 1 commit into
rustyconover wants to merge 1 commit into
Conversation
In threaded builds dlopen reads the side module's file into linear memory (dso->file_data) so that other threads can load it, and never frees it, so every loaded library keeps a copy of its file in the heap. When the main runtime thread calls dlopen, _emscripten_dlsync_threads blocks until every running thread has loaded the module. The only threads that could still need the file data are ones started later on workers created before the dlopen. spawnThread now passes sharedModules in the run message, so those threads instantiate the compiled module instead, and load_library_done frees the file data. _emscripten_dlsync_threads now returns whether that holds. It doesn't when dlopen was called on another thread (the main thread catches up asynchronously, so a thread spawned meanwhile could still read the file data) or when a thread failed to load the module (it may retry), and the file data is kept as before. Also free the file data when dlopen fails, and clear the pointer after a short read so that it isn't freed twice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In a
-pthreadMAIN_MODULEprogram, every library loaded withdlopenkeeps a full copy of its file in the heap for the rest of the program's life. Loading 50 MB of side modules costs 50 MB of heap that never comes back. This PR frees that copy once no thread can need it.Results
performance.measureUserAgentSpecificMemory(), drops from 386.3 MB to 316.7 MB, a saving of 70 MB (18%). All 4 runs gave the same result, and load and query times are unchanged.dlopengrows by 8,194 KB onmainand by less than 1 MB with this PR.dlopen, on a worker that already existed, now reuse the compiled module. Before, each one copied the file data out of the heap (HEAP8.slice) and compiled the module again.The 70 MB comes from two effects together. The heap copies are freed, so the next library's file data reuses that space;
WebAssembly.Memoryitself doesn't shrink. And the new query threads no longer each copy and compile every module.What changes
load_library_donefreesdso->file_datawhen_emscripten_dlsync_threadsreports that every thread has the module. This resolves theTODOthere.runmessage sent to a pool worker now carriessharedModules, so threads started after thedlopeninstantiate the already-compiled module instead of reading the file data._emscripten_dlsync_threadsnow returns whether all threads synced.dlopenfails. Its pointer is now cleared after a shortread(), so that buffer can't be freed twice.Why it's safe
When the main runtime thread calls
dlopen,_emscripten_dlsync_threadsproxies a sync to each running thread and blocks until the sync is done. Threads that are blocked, or spawned but not yet running, handle that sync fromfutex_waitor before their entry point. So once it returns successfully, every thread that existed at the time of thedlopenhas loaded the module.The only other threads that could read
file_dataare ones started later on a pool worker created before thedlopen. That includes threads created by other pthreads throughCMD_SPAWN_THREAD. Therunmessage now gives themsharedModules. The main thread registers the module insharedModulesbeforeload_library_doneruns, and only the main thread runsspawnThread, so every laterrunmessage includes the module.Two cases still keep the file data, as today:
dlopencalled on a thread other than the main runtime thread, which includesPROXY_TO_PTHREAD. The main thread catches up asynchronously, so a thread spawned in the meantime could still read the file data. Collecting the promises only after the main thread has caught up would make this case safe too. I've left that for a follow-up._emscripten_proxy_dlsyncnow returns success for a thread it can't reach.Cost:
sharedModulesis cloned into eachrunmessage. Compiled modules are shared rather than copied when cloned. If this matters for programs that create many threads, sending only the modules a worker hasn't seen yet would keep the message smaller.Testing
New test:
core.test_pthread_dlopen_file_data. It runs in each core suite, and checks:dlopendoesn't grow by the size of the side module.dlopencan call into the module afterwards.dlopencan call into the module (CMD_SPAWN_THREAD).dlopencan call into the module. It runs on a pool worker that existed before thedlopen.The side module is embedded under a path that doesn't exist on the host, and is unlinked after the
dlopen, so the late threads can't fall back to reading the file. The test fails onmainbecause of the heap growth. It also fails if therunmessage doesn't carrysharedModules, because the late threads then can't load the module.The pthread, dylink, dlfcn, dlopen, dlsym, main_module and side_module selectors pass across
core0,core2,wasm64,core_2gbandother: 1,160 tests, 65 skipped.core2.test_pthread_dlopen_many, which is marked flaky (#18887), passed 10 runs out of 10.tools/maint/gen_sig_info.pyreproduces thelibsigs.jschange.The
test-browser-chrome-2gbandtest-browser-chrome-wasm64-4gbfailures (test_sdl_quit) also happen onmainright now.🤖 Generated with Claude Code