Skip to content

perf(sourcemap): owned merge in SourceJoiner::join (4005→5 allocs/chunk)#9881

Closed
hyfdev wants to merge 3 commits into
rolldown:mainfrom
hyfdev:rdsm/sourcemap-owned-join
Closed

perf(sourcemap): owned merge in SourceJoiner::join (4005→5 allocs/chunk)#9881
hyfdev wants to merge 3 commits into
rolldown:mainfrom
hyfdev:rdsm/sourcemap-owned-join

Conversation

@hyfdev

@hyfdev hyfdev commented Jun 18, 2026

Copy link
Copy Markdown
Member

What

SourceJoiner::join used to borrow every input sourcemap into ConcatSourceMapBuilder, then detach the combined map by deep-copying every name, source, and sourcesContent string. On a 2,000-module chunk that meant 4,005 allocations in join alone.

This change moves owned maps into the concat builder instead:

  • SourceJoiner::join consumes the joiner and uses add_sourcemap_owned for maps it can take. A mixed-path fallback keeps borrowed Source implementations correct and copies only their borrowed entries.
  • Normal chunk rendering now detaches each module's map before its source text is shared with the plugin-facing RenderedModule view. The text stays shared through Arc; a lightweight chunk source owns the map and moves it into the builder. This is required for the optimization to cover the real ESM/CJS/IIFE/UMD production path, not only synthetic owned inputs and HMR.
  • SourceMapSource stores its map as an Option and exposes take_sourcemap, leaving the plugin-facing view mapless while preserving RenderedModule.code.
  • The no-sourcemap fast path no longer scans every source for line offsets that only the sourcemap builder needs.
  • oxc_sourcemap is bumped to 8.0.2, which contains the owned concat API from perf(concat): move owned sourcemaps in instead of borrow + recopy oxc-project/oxc-sourcemap#368 (the released successor to fix: deconflict for duplicate canonical names #365).

Measured impact

metric before after
deterministic allocations, sourcemap/join_with_sourcemap (2,000 modules) 4,005 5 (-99.9%)
CodSpeed simulation, join_with_sourcemap 10.4 ms 9.5 ms (+9.31% efficiency)
CodSpeed simulation, join_no_sourcemap 1.6 ms 1.4 ms (+10.96% efficiency)
per-join wall-clock in the original prototype (release, mimalloc) ~417 µs ~314 µs (-25%)

CodSpeed's aggregate verdict is a 10.13% performance improvement. It used main@4b9c35e because a newer successful baseline was unavailable and flags a runtime-environment mismatch, so the exact percentages are directional. The allocation snapshot is committed and reproduced identically across two runs. join runs once per output chunk and remains a small part of a full build, so this is an allocation-pressure and per-operation improvement, not a large end-to-end claim.

Correctness coverage

  • Mixed owned + borrowed maps retain both mappings, source IDs, source contents, and line offsets.
  • The plugin-facing RenderedModule still exposes identical code after its map is detached, while chunk rendering receives that map.
  • Existing sourcemap fixtures pass, including CJS live bindings, source exclusion, debug IDs, inline URLs, issue sourcemapping did not generate correctly #6099, post-banner/shebang mapping, and the esbuild source-map suite.

Verification

  • Full GitHub CI, including CodSpeed, passes at a078bad82.
  • cargo clippy -p rolldown_sourcemap -p rolldown -p bench --all-targets -- --deny warnings
  • cargo test -p rolldown_sourcemap
  • cargo test -p rolldown --lib (70 passed)
  • cargo test -p rolldown --test integration -- --skip test262::test262_module_code (26 passed; test262 is not initialized in the worktree)
  • cargo allocs twice with an identical snapshot

Upstream

@netlify

netlify Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit a078bad
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a36490f4f68340008b7f342

@hyfdev
hyfdev force-pushed the rdsm/sourcemap-owned-join branch 4 times, most recently from 7578f14 to b64baae Compare June 20, 2026 07:13
@hyfdev
hyfdev marked this pull request as ready for review June 20, 2026 07:31
@hyfdev
hyfdev requested a review from Boshen June 20, 2026 07:31
@codspeed-hq

codspeed-hq Bot commented Jun 20, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 10.13%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 5 untouched benchmarks
⏩ 10 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
join_no_sourcemap 1.6 ms 1.4 ms +10.96%
join_with_sourcemap 10.4 ms 9.5 ms +9.31%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing hyf0:rdsm/sourcemap-owned-join (a078bad) with main (4b9c35e)2

Open in CodSpeed

Footnotes

  1. 10 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.

  2. No successful run was found on main (632c59e) during the generation of this report, so 4b9c35e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Comment thread crates/rolldown_sourcemap/src/source_joiner.rs
@Boshen
Boshen marked this pull request as draft June 20, 2026 12:56
@Boshen

Boshen commented Jun 20, 2026

Copy link
Copy Markdown
Member

Wait for upgrade from oxc-sourcemap.

hyfdev added 3 commits July 13, 2026 09:56
…unk)

`SourceJoiner::join` builds a `ConcatSourceMapBuilder` that borrows every
input map's strings, then calls `SourceMap::into_owned` on the merged result to
detach it — deep-copying every name/source/sourcesContent string of every
module, one heap allocation each, on the per-chunk codegen path.

Change `join(&self)` to consume `self` so each owned `SourceMap` can be *moved*
into the builder via the new `ConcatSourceMapBuilder::add_sourcemap_owned`
(oxc_sourcemap). The merged map is then already `'static`, so the trailing
`into_owned` is dropped. A new `Source::into_sourcemap(self: Box<Self>)` hands
each source's owned map over by value.

All production callers already own their joiner and call `join` once, so the
signature change is mechanical; the `join` micro-bench switches to
`iter_batched` (the joiner is now consumed per iteration).

Measured (2000-module chunk): `cargo allocs` join_with_sourcemap 4005 -> 5;
per-join wall-clock ~417us -> ~314us (-25%). join runs once per output chunk,
so the headline is allocation count, not end-to-end build time.
Detach each rendered module's owned sourcemap before its source text is shared with the plugin-facing RenderedModule view. Chunk rendering then wraps the shared text with the owned map and moves it into ConcatSourceMapBuilder, while borrowed Source callers retain a copy-on-detach fallback.\n\nBump oxc_sourcemap to 8.0.2, add mixed owned/borrowed and RenderedModule regressions, and update the deterministic allocation snapshot from 4005 to 5 allocations for a 2000-module join.
@Boshen
Boshen force-pushed the rdsm/sourcemap-owned-join branch from a078bad to 8a8e790 Compare July 13, 2026 02:12
graphite-app Bot pushed a commit that referenced this pull request Jul 13, 2026
…unk) (#10250)

`SourceJoiner::join` borrowed every input map's strings into a `ConcatSourceMapBuilder`, then called `into_owned` on the merged result to detach it — deep-copying every name/source/sourcesContent string of every module, one heap allocation each, on the per-chunk codegen path.

This moves each source's owned `SourceMap` into the builder via `ConcatSourceMapBuilder::add_sourcemap_owned`; the merged map is already `'static`, so the trailing `into_owned` is gone. Sources that can only lend a borrowed map (e.g. one shared through an `Arc`) fall back to `add_sourcemap` + copy-on-detach, so output is unchanged.

Measured via `tasks/track_memory_allocations` (2000-module chunk):

```
sourcemap/join_with_sourcemap    4005 -> 5 allocs
```

A second commit refines the join: it takes `&mut self` — the merge only needs `iter_mut()` + `take_sourcemap()`, so it never needed to consume `self` — and the benches move to `iter_batched_ref` so the micro-bench stops timing the per-iteration joiner teardown (that teardown, not the concat, was the spurious `join_no_sourcemap` CodSpeed delta). It also skips the newline scan of the *final* joined source, whose line count only advances the offset for a following source.

This is the focused, measured core extracted from #9881 (original work by @hyf0). It touches only `rolldown_sourcemap` plus the benches/allocation snapshot; the `ecma_generator`/format pipeline is left untouched, so real-build sourcemap output is identical to `main`. Wiring the chunk renderer to hand `join` its owned per-module maps — instead of the `Arc`-shared borrow it copies today — is a follow-up.
@hyfdev

hyfdev commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Solved by #10250

@hyfdev hyfdev closed this Jul 13, 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.

4 participants