Skip to content

Memoize getTransitiveDeps in src/jsifier.mjs. NFC - #28054

Closed
sbc100 wants to merge 2 commits into
emscripten-core:mainfrom
sbc100:cache-transitive-deps
Closed

sbc100 wants to merge 2 commits into
emscripten-core:mainfrom
sbc100:cache-transitive-deps

Conversation

@sbc100

@sbc100 sbc100 commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Cache the computed transitive dependency list for each symbol in transitiveDepsCache and reuse cached results when traversing dependencies in subsequent getTransitiveDeps calls. Only completed top-level traversals are inserted into the cache so that cyclic dependencies (such as $FS <-> $MEMFS) are handled safely without caching partial results.

In tools/compiler.mjs --symbols-only, this reduces graph node visits in getTransitiveDeps by ~9x (from 39,349 to 4,381), cutting the symbolDeps calculation time by ~41% (from ~16.0ms to ~9.5ms) and overall --symbols-only execution time by ~4.6% (from ~152ms to ~145ms).

See: #28052

@sbc100
sbc100 requested review from dschuff and kripken October 22, 2026 22:59
…ols-only` mode. NFC

Previously, `getTransitiveDeps` was called inside `addFromLibrary` while
`symbolHandler` was still iterating through `symbolsNeeded` and mutating
`__deps` (such as adding `setTempRet0` for i64-returning functions
without `WASM_BIGINT`).

Compute `symbolDeps` in the `if (symbolsOnly)` block after
`symbolHandler` completes so that all direct `__deps` are populated
before traversing the dependency graph.

Split out from emscripten-core#27995.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a module-level cache (transitiveDepsCache) in src/jsifier.mjs to optimize the getTransitiveDeps function and avoid quadratic behavior. The review feedback highlights two key areas for improvement: first, to prevent cross-compilation contamination in multi-run environments (like test suites), the global cache should be cleared at the start of runJSify; second, the logic for retrieving and filtering dependencies can be simplified and made more idiomatic using nullish coalescing.

Comment thread src/jsifier.mjs
return false;
}

const transitiveDepsCache = new Map();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Since runJSify can be called multiple times within the same Node.js process (for example, in test suites or in-process build pipelines), a module-level global Map like transitiveDepsCache will persist across different compilations. This can lead to stale cache entries and incorrect dependency resolution if different compilations use different libraries or settings.

To prevent this cross-compilation contamination, transitiveDepsCache should be cleared at the start of runJSify:

export async function runJSify(outputFile, symbolsOnly) {
  transitiveDepsCache.clear();
  ...

Comment thread src/jsifier.mjs
Comment on lines +143 to +148
const cached = transitiveDepsCache.get(sym);
let deps = cached;
if (!deps) {
deps = LibraryManager.library[sym + '__deps'] ?? [];
deps = deps.filter((d) => typeof d === 'string');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This block can be simplified and made more idiomatic by using the nullish coalescing operator (??) and declaring deps as a const.

      const cached = transitiveDepsCache.get(sym);
      const deps = cached ?? (LibraryManager.library[sym + '__deps'] ?? [])
        .filter((d) => typeof d === 'string');

@kripken

kripken commented Oct 22, 2026

Copy link
Copy Markdown
Member

The benefit seems to be just a few ms here - is it worthwhile?

@sbc100

sbc100 commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

5% seems like a big enough speedup to warrant landing to me.

@kripken

kripken commented Oct 22, 2026

Copy link
Copy Markdown
Member

5% of jsifier time? or total link time?

@sbc100

sbc100 commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

5% of jsifier time? or total link time?

The total link time for hello world is dominated by 3 things:

  1. Running emcc python
  2. emcc calling out to compiler.mjs before wasm-ld (--symbol-only)
  3. emcc calling out to compiler.mjs after wasm-ld

Each of these in my machine is around ~150. The cost of actually running wasm-ld is marginal.

So this is really 5% of 33% of the total.

And aggressively cache the result of --symbols-only too. So maybe you are right, we just leave the TODO in there ? :) I'm not sure.

Cache the computed transitive dependency list for each symbol in
`transitiveDepsCache` and reuse cached results when traversing
dependencies in subsequent `getTransitiveDeps` calls. Only completed
top-level traversals are inserted into the cache so that cyclic
dependencies (such as `$FS` <-> `$MEMFS`) are handled safely without
caching partial results.

In `tools/compiler.mjs --symbols-only`, this reduces graph node visits
in `getTransitiveDeps` by ~9x (from 39,349 to 4,381), cutting the
`symbolDeps` calculation time by ~41% (from ~16.0ms to ~9.5ms) and
overall `--symbols-only` execution time by ~4.6% (from ~152ms to
~145ms).

See: emscripten-core#28052
@sbc100
sbc100 force-pushed the cache-transitive-deps branch from 749186b to 8546616 Compare October 23, 2026 15:35
@kripken

kripken commented Oct 23, 2026

Copy link
Copy Markdown
Member

Yeah, if it's 1/3 of the total, then 1/3 of 5% is 1.67% which is pretty small - I'd prefer code simplicity over such a small win.

@sbc100

sbc100 commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Change

@sbc100 sbc100 closed this Oct 23, 2026
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