Skip to content

[Debuginfo] Add a JS_DEBUG_LEVEL setting to control the JS debug level #24936

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

kripken
Copy link
Member

@kripken kripken commented Aug 14, 2025

Normally -g affects the debug level in LLVM, Binaryen, and JS, but for
optimized builds with source maps we don't want all those to match.

One way to fix that might be a breaking change, to make -gsource-map
not set -g for JS, or to make it only set it for LLVM. However, that would
break many debug workflows in confusing ways, and it seems less
consistent (that a -gX flag doesn't see the debug level).

This PR proposes that we add a new setting to explicitly control the JS
debug level. It defaults to the -gX level, but now e.g.

-O3 -gsource-map -sJS_DEBUG_LEVEL=0

will emit an optimized build, with a source map, and with optimized JS
(since we set the JS debug level to 0, overriding the default -g3 from
-gsource-map).

Fixes #20462
Fixes #24626

Copy link
Collaborator

@sbc100 sbc100 left a comment

Choose a reason for hiding this comment

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

lgtm.

I'm curious how much usage remains of the (internal) DEBUG_LEVEL setting? Perhaps it can be removed as a followup?

@kripken
Copy link
Member Author

kripken commented Aug 14, 2025

Hmm, good question, but unfortunately I don't think we can remove DEBUG_LEVEL. That affects whether we'll emit the names section and DWARF for example, in various places. (In theory we could add internal settings for each of them but I'm not sure it's simpler.)

@adamvleggett
Copy link

adamvleggett commented Aug 14, 2025

Would it make sense to just support -gsource-map and -g0 (or any level) together, allowing the optimization level to be controlled explicitly when needed? The specific case I have in mind is generating source maps for release binaries, without changing the release binary in any way from one that doesn’t have a source map.

The existing behavior would not need to change. The patch could (it seems) be limited to cmdline.py. Local dirty patching by setting GENERATE_SOURCE_MAP=1 explicitly seems to work fine.

@kripken
Copy link
Member Author

kripken commented Aug 14, 2025

@adamvleggett

Would it make sense to just support -gsource-map and -g0 together, allowing the optimization level to be controlled explicitly when needed?

Atm (before this PR) -g0 sets the global debug level to 0, the same as clang, which means LLVM will not emit debug info (and there is nothing to emit a source map from). We could change the meaning of -g0 (perhaps "set the global debug level, but leave the source map setting as is"), but it would add complexity internally, and it would be a breaking change (imagine, in particular, some build of -gsource-map -g0 that was not meant to emit any debug info at all, like maybe the -gsource-map was there by mistake; if we made a breaking change here, that build would start to ship a source map to all users, which could be a disaster).

@adamvleggett
Copy link

adamvleggett commented Aug 14, 2025

@kripken I am currently getting valid source maps combined with fully stripped wasm binaries in Emscripten 4.0.12 using -g0 (at link time only) combined with either manually setting GENERATE_SOURCE_MAP=1 in cmdline.py or disabling the addition of -g.

These include source file and line info.

This behavior (-gsource-map -g0) existed in Emscripten 3.1.45 and changed.

@adamvleggett
Copy link

I tested this change locally with -sJS_DEBUG_LEVEL=0 added to my link arguments, followed by a separate wasm-opt run to strip the generated WebAssembly module. The source map size is roughly identical to that generated by my method, but the WebAssembly module size is about 3KB larger than the release build without source map generation.

Incidentally, this is an identical size to what I can get with -gline-tables-only followed by the strip, without the changes in this PR.

@kripken
Copy link
Member Author

kripken commented Aug 14, 2025

(at link time only)

Ah, yes, if you separate the compile and link stages then you sort of have some control over different debug levels for LLVM vs JS, as LLVM is already done by the link stage. And some changes to cmdline.py might be enough, as you say.

But, if you do LTO then I don't think that approach would work (the debug level would affect compilation, as it happens during link). However, if I'm missing something and you can get LTO and other cases working your way, I'm certainly open to a PR that replaces this one.

@adamvleggett
Copy link

adamvleggett commented Aug 15, 2025

@kripken You are right, I hadn't considered the LTO case. I guess that forces the necessity of a new setting.

In that case, I'd like to propose something like -sLINK_DEBUG_LEVEL, which would cover the link stage for both JS and WebAssembly. The idea is that -gsource-map would still set the high global DEBUG_LEVEL needed to ensure LLVM generates the DWARF info (for both LTO and non-LTO builds). The -sLINK_DEBUG_LEVEL setting would then override this for all post-compilation tools.

I am not personally at liberty to contribute to open source projects due to my employment.

@kripken
Copy link
Member Author

kripken commented Aug 15, 2025

What do you see the advantage of LINK_DEBUG_LEVEL over JS_DEBUG_LEVEL? (sorry if I'm missing some obvious use case or other benefit)

@adamvleggett
Copy link

What do you see the advantage of LINK_DEBUG_LEVEL over JS_DEBUG_LEVEL? (sorry if I'm missing some obvious use case or other benefit)

This would enable Binaryen to run its most aggressive minification on the .wasm binary, which to me is half of the problem.

@kripken
Copy link
Member Author

kripken commented Aug 15, 2025

But JS_DEBUG_LEVEL only affects JS, so Binaryen would still be allowed to run all its optimizations with JS_DEBUG_LEVEL=0?

@adamvleggett
Copy link

This is true, but adding -gsource-map sets a high global DEBUG_LEVEL for the entire link stage.

This not only disables stripping of debug symbols from the .wasm binary, but also prevents Binaryen from running with its most aggressive optimizations. Some of these, like metadce which is tied to the Closure pass, cannot be fully replicated by a standalone wasm-opt run later.

@kripken
Copy link
Member Author

kripken commented Aug 15, 2025

Hmm, I don't think that is an issue. First, metadce depends on the opt level, not debug:

emscripten/tools/link.py

Lines 156 to 161 in cab75bc

def will_metadce():
# The metadce JS parsing code does not currently support the JS that gets generated
# when assertions are enabled.
if settings.ASSERTIONS:
return False
return settings.OPT_LEVEL >= 3 or settings.SHRINK_LEVEL >= 1

I added a test now that verifies this.

Your worry is reasonable, though, and I looked if any other optimization is disabled by DEBUG_LEVEL, but I can't find any (as the new test confirms).

@kripken
Copy link
Member Author

kripken commented Aug 15, 2025

You mentioned earlier:

the WebAssembly module size is about 3KB larger than the release build without source map generation

I suspect that is the combination of the sourcemap URL section + the names section. The former is necessary, and the latter is currently emitted with source maps - there is a TODO in the code to consider changing it, but we should do that separately from this PR, and for now it is simple to remove manually using llvm-objcopy --remove-section=name

@adamvleggett
Copy link

You are right on both counts:

  • metadce is tied to the opt level and was not the issue I was seeing.
  • wasm-objdump shows that the .code sections are identical. The vast majority of the difference comes from the name and sourceMappingURL sections, especially the name section.

I guess this would leave LINK_DEBUG_LEVEL as a convenience feature that would avoid the need for a separate strip stage. I have not had success using llvm-objcopy to remove the name section, but that probably doesn't merit discussion here.

I'll leave it to you then - I'd like to have the convenience, but I understand it may add complexity.

@dschuff
Copy link
Member

dschuff commented Aug 15, 2025

Does the --minify flag do something like this already?

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.

enabling -gsource-map disables JS whitespace minification Support optimized builds with source maps
4 participants