Skip to content

Commit e11edd8

Browse files
authored
Optimize JS compiler caching for standard builds (#27026)
Linker system stubs, such as libpthread_stub.js, are dynamically appended to settings.JS_LIBRARIES for all standard C/C++ builds. Previously, the JS output cache check did not distinguish between these system stubs and user-defined JS libraries (passed via --js-library). As a result, standard builds always bypassed the compilation cache, triggering a Node.js compilation run on every invocation. Restrict the cache bypass condition to check only for user libraries (libraries located outside the Emscripten source tree). Standard compilations now successfully hit the JS output cache, decreasing baseline compilation wall clock time of a hello world C program by 52% (from 472 ms down to 282 ms). Add an integration test to validate JavaScript compilation caching (covering hits, misses, user library bypasses, and option-based cache entries).
1 parent 7229f15 commit e11edd8

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

test/test_sanity.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,3 +870,45 @@ def test_emcc_with_relative_python_path(self):
870870
output = self.do([EMCC, test_file('hello_world.c')], env=env)
871871
self.assertNotContained('error', output)
872872
self.assertExists('a.out.js')
873+
874+
def test_emcc_javascript_compilation_caching(self):
875+
restore_and_set_up()
876+
877+
# Create a separate temporary cache folder to avoid dirtying or reading from the default cache.
878+
test_cache_dir = self.in_dir('test_cache')
879+
js_output_cache_dir = os.path.join(test_cache_dir, 'js_output')
880+
881+
def js_cache_files():
882+
if not os.path.exists(js_output_cache_dir):
883+
return []
884+
return sorted([f for f in os.listdir(js_output_cache_dir) if f.endswith('.js')])
885+
886+
def js_cache_size():
887+
return len(js_cache_files())
888+
889+
with env_modify({'EM_CACHE': test_cache_dir}):
890+
# 1. First compile. Cache-miss: should compile and populate the cache.
891+
self.run_process([EMCC, test_file('hello_world.c'), '-O2', '-o', 'out.js'])
892+
self.assertExists(js_output_cache_dir)
893+
894+
self.assertEqual(js_cache_size(), 1, f'Expected 1 cached JS file, found: {js_cache_files()}')
895+
896+
cached_file_path = os.path.join(js_output_cache_dir, js_cache_files()[0])
897+
initial_mtime = os.path.getmtime(cached_file_path)
898+
899+
# 2. Second compile. Cache-hit: mtime of cache file should remain strictly identical (not overwritten).
900+
self.run_process([EMCC, test_file('hello_world.c'), '-O2', '-o', 'out.js'])
901+
902+
self.assertEqual(js_cache_size(), 1)
903+
self.assertEqual(os.path.getmtime(cached_file_path), initial_mtime, 'Cache was overwritten on second compile (expected a cache hit)')
904+
905+
# 3. Third compile with custom user library. Cache-bypass: should not add any cache entries.
906+
create_file('my_lib.js', 'addToLibrary({ my_custom_symbol: () => {} });')
907+
self.run_process([EMCC, test_file('hello_world.c'), '-O2', '--js-library=my_lib.js', '-o', 'out.js'])
908+
909+
self.assertEqual(js_cache_size(), 1, 'Cache entry was incorrectly created for custom JS library compile')
910+
911+
# 4. Fourth compile with a changed compiler option. Distinct Cache Entry: should generate a second cache entry.
912+
self.run_process([EMCC, test_file('hello_world.c'), '-O2', '-sASSERTIONS=1', '-o', 'out.js'])
913+
914+
self.assertEqual(js_cache_size(), 2, f'Expected 2 cached JS files after compiling with different options, found: {js_cache_files()}')

tools/emscripten.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,8 @@ def compile_javascript_cached():
346346
# this step is performed while the cache is locked.
347347
# Sadly we have to skip the caching whenever we have user JS libraries. This is because
348348
# these libraries can import arbitrary other JS files (either vis node's `import` or via #include)
349-
if DEBUG or settings.BOOTSTRAPPING_STRUCT_INFO or config.FROZEN_CACHE or settings.JS_LIBRARIES:
349+
has_user_libs = any(not lib.startswith(utils.path_from_root('src/')) for lib in settings.JS_LIBRARIES)
350+
if DEBUG or settings.BOOTSTRAPPING_STRUCT_INFO or config.FROZEN_CACHE or has_user_libs:
350351
return compile_javascript()
351352

352353
content_hash = generate_js_compiler_input_hash()

0 commit comments

Comments
 (0)