Skip to content

perf(loader): schedule loader chains as execution units - #15401

Draft
intellild wants to merge 4 commits into
mainfrom
codex/loader-chain-optimization
Draft

perf(loader): schedule loader chains as execution units#15401
intellild wants to merge 4 commits into
mainfrom
codex/loader-chain-optimization

Conversation

@intellild

@intellild intellild commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Extract the LoaderChain execution-unit optimization from #15225 onto current main, while keeping the current CacheFacade / ItemCacheFacade storage implementation unchanged.

The loader cache is resolved for the complete cacheable LoaderChain on the Rust side immediately before normal-chain execution decides whether it needs to yield to JavaScript:

Rust plans LoaderChain execution units
  ↓
Pitching (may yield to JavaScript when pitch capability is not yet known)
  ↓
Process resource
  ↓
Rust confirms the complete normal chain will execute
  ↓
Rust computes the chain cache identity and checks CacheFacade
  ├─ hit  → restore output/dependencies and skip normal-chain JavaScript execution
  └─ miss → execute native/JavaScript child chains
               ↓
             store one root-chain result

For example, for a cacheable babel-loader -> custom JS loader chain, a warm cache hit restores the normal-chain result in Rust and does not schedule the Babel transform on the JavaScript worker. In a fresh process, pitching may still enter JavaScript once to discover that a loader has no pitch function. On a miss, adjacent JavaScript loaders run as one execution unit and the final chain result is stored once.

To minimize the surface relative to main, this PR:

  • reuses the existing LoaderCacheAction model instead of introducing a separate chain action;
  • keeps cache storage on the current main implementation;
  • removes the JS-side loader cache binding and worker request APIs;
  • preserves main's JS loader version identity by keeping loader_cache_version in the Rust resolver, while removing only the old JS cache binding;
  • computes the ordered loader name/options/version identity directly in Rust's loader_cache_key, without adding loader_chain_cache_key or serializing a synthetic loader name in before_normal_chain;
  • encapsulates dependency delta calculation in LoaderDependencies::difference / is_subset_of;
  • requires ArcComputed owners to be Pin<Arc<T>>, with Loaders and ResolvedModuleOptions created through Arc::pin before computed references can be formed.

Performance

Release-to-release Babel-only warm benchmark in /Users/bytedance/projects/mock-oai:

  • current branch: fee86872f3, locally built with build:release;
  • baseline: published @rspack/core@2.2.1 with its release binding;
  • Node 22.22.2 on darwin-arm64;
  • 3 timed samples after one warmup per variant;
  • Rsbuild build cache and Rspack optimizations disabled;
  • only the loader tier of experiments.newCache enabled;
  • builtin:swc-loader caching disabled in every run;
  • parallel mode uses 8 Babel workers.
Babel parallel Babel cache Rspack 2.2.1 median This branch median Branch vs 2.2.1
off off 67.02 s 63.75 s -4.88%
off on 15.50 s 11.16 s -28.00%
on off 27.21 s 28.50 s +4.74%
on on 21.32 s 11.88 s -44.28%

With loader cache enabled, the branch is 1.39x faster than published Rspack 2.2.1 with Babel parallel off and 1.79x faster with parallel on. Within the branch, enabling loader cache reduces the median by 82.50% (5.71x) with parallel off and 58.30% (2.40x) with parallel on.

Within the branch's warm-cache results, parallel on is 0.72 s (6.45%) slower than parallel off: 11.88 s versus 11.16 s. The normal Babel transform is skipped on a Rust cache hit, but the cache lookup currently happens after pitching. A fresh benchmark process can therefore still cross Rust → main-thread JavaScript to load the loader and discover that it has no pitch function, while parallel mode also carries its loader metadata and scheduling bookkeeping. Across 10,895 code modules, the 0.72 s delta is about 66 µs per module, which is consistent with coordination overhead rather than Babel transformation work.

The parallel-on/cache-off result is 4.74% slower than the published baseline; the other cache-off result is 4.88% faster. SWC loader caching was intentionally excluded so the comparison focuses on Babel loader execution and cache behavior.

Related links

Validation

  • cargo check -p rspack_util -p rspack_cacheable -p rspack_loader_runner -p rspack_core -p rspack_plugin_javascript -p rspack_plugin_css --all-targets --locked
  • cargo check -p rspack_binding_api --all-targets --locked
  • cargo test -p rspack_loader_runner --locked (10 passed)
  • cargo test -p rspack_core lazily_reuse_parser_and_generator_options_for_rule_ids --locked (1 passed)
  • pnpm --filter @rspack/binding run build:release
  • git diff --check
  • Release-to-release Babel benchmark: two implementations × parallel on/off × loader cache on/off
  • SWC cache benchmark intentionally skipped

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

by OpenAI Codex

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

📦 Binary Size-limit

Comparing 4875483 to fix: stabilize incremental missing-module diagnostic location (#15456) by harpsealjs

🎉 Size decreased by 76.00KB from 68.48MB to 68.40MB (⬇️0.11%)

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Rsdoctor Bundle Diff Analysis

Found 5 projects in monorepo, 0 projects with changes.

📊 Quick Summary
Project Total Size Gzip Size Change Gzip Change
react-10k 5.3 MB 1.3 MB 0 0
popular-libs 1.7 MB 523.6 KB 0 0
react-1k 799.1 KB 217.5 KB 0 0
react-5k 2.6 MB 662.0 KB 0 0
ui-components 4.9 MB 1.4 MB 0 0

Generated by Rsdoctor GitHub Action

@codspeed-hq

codspeed-hq Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 50 untouched benchmarks
⏩ 47 skipped benchmarks1


Comparing codex/loader-chain-optimization (4875483) with main (0ae3f0f)

Open in CodSpeed

Footnotes

  1. 47 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@intellild intellild changed the title refactor(loader): schedule loader chains as execution units perf(loader): schedule loader chains as execution units Aug 31, 2026
@intellild

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T12:15:44.991601Z 7364cb1 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 788f2e2bb4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rspack_loader_runner/src/chain.rs
@intellild
intellild force-pushed the codex/loader-chain-optimization branch from 9cbb92f to 7364cb1 Compare September 1, 2026 12:03
@intellild

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 7364cb13a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@intellild
intellild force-pushed the codex/loader-chain-optimization branch from 7364cb1 to 42ce415 Compare September 2, 2026 09:06
@intellild

Copy link
Copy Markdown
Contributor Author

@codspeedbot is this regression real?

@codspeed-hq

codspeed-hq Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@codspeedbot is this regression real?

Short answer: mostly no. 12 of the 13 flagged benchmarks cannot execute any code this PR touches — the steps there are a whole-binary codegen shift. The one number that reflects real work is rust@build_module_graph, which CodSpeed did not flag but is +2.0%, and that extra time is genuinely in the new loader-chain path.

The flagged 13 are not measuring your change

  • The comparison base isn't the problem: head 42ce415 shows the exact same 13 regressions against the report base 3d12941 and against the newest main 375056c.
  • These are not noise either: Simulation values are byte-identical across reruns, e.g. rust@module_graph_api = 3.7187569 ms on two different branch commits and 3.4838169 ms on three different main commits.
  • But the measured regions contain no loader code. rust@module_graph_api only calls connection_by_dependency_id / get_module_by_dependency_id / get_resolved_module / get_*_connections on a graph built in setup. Its flamegraph diff:
Subtree main branch
bench closure self 622.1 µs 587.6 µs (faster)
get_module_by_dependency_id 408.5 µs 533.4 µs
OverlayMap::get 331.9 µs 441.9 µs
connection_by_dependency_id 346.5 µs 346.5 µs
get_resolved_module 213.2 µs 213.2 µs
memcpy self 378.9 µs 378.9 µs

Call counts are identical; the delta lives entirely inside the hash-map lookup internals, which the two binaries inline differently (main goes through equivalent_key + _mm_movemask_epi8, the branch through match_tag/full/eq<u8>). Adding ~1k lines to rspack_core re-triggered inlining decisions in the shared bench binary. The sources@* benches are byte-identical between the two runs, consistent with that. So: reproducible, but not caused by the loader logic.

The one real cost: rust@build_module_graph

438.4 ms → 447.2 ms (+8.8 ms, +2.0%), and this bench does run the new path:

  • run_loaders subtree: 21.2 → 24.8 ms (+3.6 ms, +17%)
  • memcpy inside NormalModule::build: 12.9 → 15.7 ms (+2.8 ms), callers include create_loader_context, LoaderResult::new, run_loaders
  • resolve_normal_module: 106.7 → 108.3 ms (+1.6 ms), with new plan_loader_chains, Loaders::new, LoaderRunnerOptions::from_elem allocations
  • Parse side flat: JavaScriptParserAndGenerator::parse 207.4 ms in both, scan_dependencies 131.0 ms in both

That is the chain-planning/bookkeeping overhead you describe in the PR description, measured with no offsetting benefit: these fixtures use native loaders only, so no chain cache hit and no JS round-trip is saved. Per module it is small, and it does not surface at bundle scale — bundle@basic-react-development 151.6 → 151.7 ms, bundle@misc-development 2.8 s both, bundle@threejs-production-sourcemap 191.2 → 191.1 ms, rust@build_swc-loader 1 s both.

Suggestion

Nothing to fix for the 12 flagged benches; they can be acknowledged. If you want to claw back the +2%, the target is the per-module allocation in chain planning: the Vec allocations in plan_loader_chains/Loaders::new/LoaderRunnerOptions::from_elem and the extra copies in create_loader_context / LoaderResult::new. A SmallVec for the chain layout and avoiding one clone of the loader item list would cover most of the +3.6 ms in run_loaders.

Summary
  • Warming up the performance wizard...
  • Triage the 13 flagged regressions — Compared head 42ce415 against both main 3d12941 (report base) and latest main 375056c: identical set of 13 regressions, so the unexpected base is not the cause.
  • Check run-to-run determinism — Simulation values are byte-identical across two branch commits and across main commits (e.g. module_graph_api 3.7187569 ms on branch, 3.4838169 ms on main), so these are deterministic instruction-count steps, not noise.
  • Attribute rust@module_graph_api (-6.3%) — Measured region is pure ModuleGraph lookups (no loader code). Call counts identical; the whole +235 us sits in hashbrown/std HashMap lookup internals that are inlined differently, and the bench closure's own self time actually dropped 622.1 -> 587.6 us => whole-binary codegen shift.
  • Attribute rust@build_module_graph (+2.0%) — 438.4 -> 447.2 ms, the only bench that executes the new code: run_loaders 21.2 -> 24.8 ms, memcpy inside NormalModule::build 12.9 -> 15.7 ms, resolve_normal_module 106.7 -> 108.3 ms, parse subtree flat at 207.4 ms.
  • Answer on the PR — Reported: 12 of 13 flagged benches are a codegen artifact; the real, small cost is the chain-planning overhead visible in build_module_graph.
  • Handling comment — Comment handling finished successfully

Learnings updated. See what CodSpeed remembers about this repository.

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

@intellild
intellild force-pushed the codex/loader-chain-optimization branch 2 times, most recently from b0303f1 to 349fc09 Compare September 3, 2026 09:38
@intellild
intellild force-pushed the codex/loader-chain-optimization branch from b4ecb7f to 7e9b4ce Compare September 4, 2026 07:12
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.

1 participant