Skip to content

Include heap views only when needed#27269

Merged
brendandahl merged 9 commits into
emscripten-core:mainfrom
brendandahl:heap-vars
Jul 22, 2026
Merged

Include heap views only when needed#27269
brendandahl merged 9 commits into
emscripten-core:mainfrom
brendandahl:heap-vars

Conversation

@brendandahl

@brendandahl brendandahl commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Previously, all standard heap views (HEAP8 through HEAPF64) were
always included in the JS bundle and initialized, even when unused.
Closure Compiler could not eliminate the new TypedArray(a)
expressions because in JavaScript and Closure Compiler's standard
externs, TypedArray constructors are not considered pure (side-effect-free).

Remove heap view symbols from default library inclusions. Only
include and initialize heap views that are required by runtime
features, exported, or referenced in user code and snippets.

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

I guess the reason this has an impact on codesize even closure build is that somehow these are not been detected and removed as dead code by closure?

Comment thread src/jsifier.mjs
Comment thread src/jsifier.mjs Outdated
Comment thread src/runtime_common.js Outdated
Comment thread src/runtime_common.js
return `${maybeExportHeap(x)}${x} = new ${type}(b);`;
}
return '';
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is almost enough complexity to just move the whole updateMemoryViews into a generator / helper. But I guess it makes sense for this incremental change to do it this way for now.

Comment thread src/runtime_common.js Outdated
Comment thread src/runtime_common.js Outdated
Comment thread src/runtime_common.js Outdated
Comment thread src/settings.js Outdated
Comment thread tools/link.py
feature_matrix.auto_enable_features()

if settings.WASM_BIGINT:
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$HEAP64', '$HEAPU64']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you basically moved the encoding of this information from here to hardcoding it in getRequiredHeapSymbols? Why is that better?

Comment thread tools/link.py Outdated
@brendandahl

Copy link
Copy Markdown
Collaborator Author

Nice!

I guess the reason this has an impact on codesize even closure build is that somehow these are not been detected and removed as dead code by closure?

We discussed in person, but for posterity...

Closure Compiler cannot eliminate those remaining new TypedArray(a) expressions because in JavaScript and Closure Compiler's standard externs, TypedArray constructors are not considered pure (side-effect-free) for three main reasons: 1) runtime exceptions, 2) property accessor traps, 3) no @nosideeffects in the externs defined by closure.

Comment thread tools/emscripten.py Outdated
Comment thread src/jsifier.mjs
'runtimeKeepalivePop',
'UTF8ToString',
'getValue',
'setValue',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if these are actually used much. Maybe add a TODO to remove them from this list?

Comment thread src/jsifier.mjs Outdated
}
}
// If the snippet contains eval(), it may dynamically evaluate strings that reference any
// heap view (e.g., eval('HEAP8[0]')). Since static string matching cannot detect which

@sbc100 sbc100 Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But wouldn't eval('HEAP8[0]') also work for check below? i.e. we would still see the HEAP8 reference below wouldn't we?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not always. This was for fixing emscripten_run_script which loads a string from memory to eval.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should update that comment.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if anyone is doing this it should be on them to explictly add these deps.

If you don't want to break such folks with this PR, then you can maybe include this code now and then remove it later (i.e. delay the breakage)?

@sbc100

sbc100 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Nice!
I guess the reason this has an impact on codesize even closure build is that somehow these are not been detected and removed as dead code by closure?

We discussed in person, but for posterity...

Closure Compiler cannot eliminate those remaining new TypedArray(a) expressions because in JavaScript and Closure Compiler's standard externs, TypedArray constructors are not considered pure (side-effect-free) for three main reasons: 1) runtime exceptions, 2) property accessor traps, 3) no @nosideeffects in the externs defined by closure.

Can you include this in the commit message in come form?

Comment thread src/jsifier.mjs Outdated
// Because addImplicitDeps only scans library functions and user code (pre-js/post-js/EM_JS),
// runtime scaffolding files that access linear memory directly must have their heap views
// explicitly declared here so they are emitted in the final JS bundle.
function getRequiredHeapSymbols() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure why we would prefer to implement this function rather than in link.py (where these symbols would be added to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.

For other cases there a build config implies a library symbol we do it in link.py, why should this be different?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's happening, my comment didn't post....

I was trying to put all the heap handling code in one place. I've added new commit with it moved to python.

It's called twice because it's not fully known if the HEAP symbols are needed until after link. We can figure out most pre link (e.g. if MAIN_MODULE, SAFE_HEAP, etc are set), but we don't know if (settings.HAS_MAIN and settings.MAIN_READS_PARAMS) until later. I suppose we could call it in compile_javascript. It will still be called twice, but from one place.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is kind of sad.. perhaps just the settings.HAS_MAIN and settings.MAIN_READS_PARAMS could be done in emscripten.py and the rest can stay in link.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. we do the things we can do in phase_linker_setup.py and we then update the list once we know about settings.HAS_MAIN and settings.MAIN_READS_PARAMS?

@sbc100 sbc100 Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if its even good idea to group all this stuff together either.. i.e. sometimes is nice to see the side effects of a given setting all in one place (i.e. to see the effects of MEMORY64), but its also nice to group things like by the effect (i.e. to see all the things the effect HEAP requirments). Is not clear to me the that latter grouping is any better than the former grouping.

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm % final comment

Comment thread tools/link.py
@@ -806,6 +806,49 @@ def setup_cross_origin_storage():
exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')")


def add_required_heap_symbols():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this be called from phase_linker_setup below (where most of it lived before) instead of moving to emcc.py

Comment thread tools/emscripten.py Outdated
@@ -155,6 +156,8 @@ def update_settings_glue(wasm_file, metadata, base_metadata):
for deps in metadata.js_deps:
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.extend(deps.split(','))

link.add_required_heap_symbols()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems rather odd that we could need to call it twice.

Comment thread tools/emscripten.py Outdated
if settings.HAS_MAIN and settings.MAIN_READS_PARAMS:
heaps = ['$HEAP32', '$HEAPU32']
if settings.WASM_BIGINT or settings.MEMORY64:
heaps += ['$HEAP64', '$HEAPU64']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest do you know why MAIN_READS_PARAMS means that we need both signed and unsigned versions of these heaps?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signed heaps were not actually needed. That was an extraneous copy from the previous code where this also handled stack overflow check.

Comment thread tools/emscripten.py Outdated

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm with a few final comments.

Comment thread tools/link.py Outdated
if settings.AUDIO_WORKLET:
heaps += ['$HEAP32', '$HEAPU32', '$HEAPF32']
if settings.WASM_BIGINT or settings.MEMORY64:
heaps += ['$HEAP64', '$HEAPU64']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines are duplicates on both branches of the if condition.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is under AUDIO_WORKLET so it's not added in all cases. Alternatively, I could flatten out this bigger if/else so it's less clever and just always add certain HEAP deps if a setting is set.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, fair enough. I wonder if the AUDIO_WORKLET code can add these via __deps maybe?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the audio worklet code is #included directly in runtime_common. It could maybe benefit from becoming a library, but probably a bigger change.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in libwebaudio.js already has some indirect deps like that:

  // _emscripten_create_audio_worklet() doesn't use stackAlloc,                      
  // etc., but the created worklet does.     

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a very round about way to include a dep.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, its is kind of roundabout. But its also the exist approach we have today.

Do you think this approach of hardcoding stuff into the .py files is better?

Surely having two hacky solutions is worse than just one?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we inject fake deps in other JS libs? It seems to me that the lib audio is the oddball one, most of the non-lib deps are added in link.py.

I don't really care what approach we do here, it seems in the end we should move more of the audio code to a library and add the deps where they're actually used.

Comment thread src/jsifier.mjs
deps.push('$HEAP8', '$HEAPU8', '$HEAP16', '$HEAPU16', '$HEAP32', '$HEAPU32', '$HEAPF32', '$HEAPF64');
if (WASM_BIGINT || MEMORY64) {
deps.push('$HEAP64', '$HEAPU64');
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If think this should not be needed because these get added by link.py if WASM_BIGINT or MEMORY64?

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Previously, all standard heap views (HEAP8 through HEAPF64) were
always included in the JS bundle and initialized, even when unused.
This increased bundle size and memory overhead.

Remove heap view symbols from default library inclusions. Only
include and initialize heap views that are required by runtime
features, exported, or referenced in user code and snippets.
@brendandahl
brendandahl enabled auto-merge (squash) July 21, 2026 21:28
Comment thread src/runtime_common.js
{{{ maybeExportHeap('HEAP64') }}}HEAP64 = new BigInt64Array(b);
{{{ maybeExportHeap('HEAPU64') }}}HEAPU64 = new BigUint64Array(b);
{{{ updateHeap('HEAP64', 'BigInt64Array') }}}
{{{ updateHeap('HEAPU64', 'BigUint64Array') }}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this land line is not visually aligned like the others?

This is an automatic change generated by tools/maint/rebaseline_tests.py.

The following (64) test expectation files were updated by
running the tests with `--rebaseline`:

```
test/codesize/audio_worklet_wasm.expected.js updated
test/codesize/hello_wasm_worker_wasm.expected.js updated
test/codesize/hello_world_wasm.expected.js updated
test/codesize/hello_world_wasm2js.expected.js updated
codesize/test_codesize_cxx_ctors1.json: 151884 => 151784 [-100 bytes / -0.07%]
codesize/test_codesize_cxx_ctors2.json: 151287 => 151187 [-100 bytes / -0.07%]
codesize/test_codesize_cxx_except.json: 195773 => 195674 [-99 bytes / -0.05%]
codesize/test_codesize_cxx_except_wasm.json: 167008 => 166908 [-100 bytes / -0.06%]
codesize/test_codesize_cxx_except_wasm_legacy.json: 164892 => 164792 [-100 bytes / -0.06%]
codesize/test_codesize_cxx_lto.json: 120735 => 120635 [-100 bytes / -0.08%]
codesize/test_codesize_cxx_mangle.json: 262144 => 262045 [-99 bytes / -0.04%]
codesize/test_codesize_cxx_noexcept.json: 153884 => 153784 [-100 bytes / -0.06%]
codesize/test_codesize_cxx_wasmfs.json: 179507 => 179386 [-121 bytes / -0.07%]
test/codesize/test_codesize_file_preload.expected.js updated
codesize/test_codesize_file_preload.json: 23969 => 23870 [-99 bytes / -0.41%]
codesize/test_codesize_files_js_fs.json: 18352 => 18253 [-99 bytes / -0.54%]
codesize/test_codesize_files_wasmfs.json: 63764 => 63585 [-179 bytes / -0.28%]
codesize/test_codesize_hello_O0.json: 38742 => 38592 [-150 bytes / -0.39%]
codesize/test_codesize_hello_O1.json: 8261 => 8066 [-195 bytes / -2.36%]
codesize/test_codesize_hello_O2.json: 5927 => 5771 [-156 bytes / -2.63%]
codesize/test_codesize_hello_O3.json: 5622 => 5467 [-155 bytes / -2.76%]
codesize/test_codesize_hello_Os.json: 5610 => 5455 [-155 bytes / -2.76%]
codesize/test_codesize_hello_Oz.json: 4781 => 4626 [-155 bytes / -3.24%]
codesize/test_codesize_hello_dylink_all.json: 856323 => 856386 [+63 bytes / +0.01%]
codesize/test_codesize_hello_esm_integration.json: 8756 => 8451 [-305 bytes / -3.48%]
codesize/test_codesize_hello_export_nothing.json: 2899 => 2685 [-214 bytes / -7.38%]
codesize/test_codesize_hello_single_file.json: 5008 => 4853 [-155 bytes / -3.10%]
codesize/test_codesize_hello_wasmfs.json: 5622 => 5467 [-155 bytes / -2.76%]
codesize/test_codesize_libcxxabi_message_O3.json: 3312 => 3099 [-213 bytes / -6.43%]
codesize/test_codesize_libcxxabi_message_O3_standalone.json: 3493 => 3315 [-178 bytes / -5.10%]
codesize/test_codesize_mem_O3.json: 9290 => 9135 [-155 bytes / -1.67%]
codesize/test_codesize_mem_O3_grow.json: 9608 => 9470 [-138 bytes / -1.44%]
codesize/test_codesize_mem_O3_grow_standalone.json: 9454 => 9298 [-156 bytes / -1.65%]
codesize/test_codesize_mem_O3_standalone.json: 9289 => 9132 [-157 bytes / -1.69%]
codesize/test_codesize_mem_O3_standalone_lib.json: 8503 => 8325 [-178 bytes / -2.09%]
codesize/test_codesize_mem_O3_standalone_narg.json: 8625 => 8447 [-178 bytes / -2.06%]
codesize/test_codesize_mem_O3_standalone_narg_flto.json: 7556 => 7378 [-178 bytes / -2.36%]
codesize/test_codesize_minimal_64.json: 2600 => 2388 [-212 bytes / -8.15%]
test/codesize/test_codesize_minimal_O0.expected.js updated
codesize/test_codesize_minimal_O0.json: 19854 => 19695 [-159 bytes / -0.80%]
codesize/test_codesize_minimal_O1.json: 3376 => 3119 [-257 bytes / -7.61%]
codesize/test_codesize_minimal_O2.json: 2549 => 2332 [-217 bytes / -8.51%]
codesize/test_codesize_minimal_O3.json: 2285 => 2073 [-212 bytes / -9.28%]
codesize/test_codesize_minimal_Os.json: 2285 => 2073 [-212 bytes / -9.28%]
codesize/test_codesize_minimal_Os_mr.json: 577 => 353 [-224 bytes / -38.82%]
codesize/test_codesize_minimal_Oz-ctors.json: 2256 => 2044 [-212 bytes / -9.40%]
codesize/test_codesize_minimal_Oz.json: 2285 => 2073 [-212 bytes / -9.28%]
codesize/test_codesize_minimal_esm.json: 2423 => 2212 [-211 bytes / -8.71%]
codesize/test_codesize_minimal_esm_integration.json: 4101 => 3721 [-380 bytes / -9.27%]
codesize/test_codesize_minimal_pthreads.json: 26063 => 25967 [-96 bytes / -0.37%]
codesize/test_codesize_minimal_pthreads_memgrowth.json: 26498 => 26419 [-79 bytes / -0.30%]
codesize/test_codesize_minimal_wasmfs.json: 2285 => 2073 [-212 bytes / -9.28%]
codesize/test_minimal_runtime_code_size_hello_embind.json: 14911 => 14911 [+0 bytes / +0.00%]
codesize/test_minimal_runtime_code_size_hello_embind_val.json: 11638 => 11638 [+0 bytes / +0.00%]
codesize/test_minimal_runtime_code_size_hello_wasm_worker.json: 4121 => 4099 [-22 bytes / -0.53%]
codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json: 18559 => 18559 [+0 bytes / +0.00%]
codesize/test_minimal_runtime_code_size_hello_webgl2_wasm_singlefile.json: 15040 => 15040 [+0 bytes / +0.00%]
codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json: 12724 => 12724 [+0 bytes / +0.00%]
codesize/test_minimal_runtime_code_size_hello_world_wasm.json: 927 => 927 [+0 bytes / +0.00%]
codesize/test_minimal_runtime_code_size_random_printf_wasm.json: 11054 => 11054 [+0 bytes / +0.00%]
codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json: 17417 => 17417 [+0 bytes / +0.00%]
test/codesize/test_small_js_flags.expected.js updated
codesize/test_small_js_flags.json: 3888 => 3732 [-156 bytes / -4.01%]
codesize/test_unoptimized_code_size.json: 177707 => 172354 [-5353 bytes / -3.01%]

Average change: -3.40% (-38.82% - +0.01%)
```
Comment thread tools/link.py
if settings.MAIN_MODULE or settings.EXPORT_ALL or settings.SAFE_HEAP:
# Dynamic linking side modules, full symbol exports, or safe heap instrumentation
# require all standard heap views and value helpers to be available.
heaps += ['$getValue', '$setValue']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically these are not "heaps", but maybe not and important distinction?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you want to call them?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, maybe this function should be called something more like add_implicit_library_deps?

Then the local could be called deps or syms or js_syms?

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your persistence with this!

@brendandahl
brendandahl merged commit dad92aa into emscripten-core:main Jul 22, 2026
38 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants