perf(sourcemap): owned merge in SourceJoiner::join (4005→5 allocs/chunk)#9881
perf(sourcemap): owned merge in SourceJoiner::join (4005→5 allocs/chunk)#9881hyfdev wants to merge 3 commits into
Conversation
✅ Deploy Preview for rolldown-rs canceled.
|
7578f14 to
b64baae
Compare
Merging this PR will improve performance by 10.13%
|
| 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
Footnotes
-
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. ↩
-
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. ↩
|
Wait for upgrade from oxc-sourcemap. |
…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.
a078bad to
8a8e790
Compare
…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.
|
Solved by #10250 |
What
SourceJoiner::joinused to borrow every input sourcemap intoConcatSourceMapBuilder, then detach the combined map by deep-copying every name, source, andsourcesContentstring. On a 2,000-module chunk that meant 4,005 allocations injoinalone.This change moves owned maps into the concat builder instead:
SourceJoiner::joinconsumes the joiner and usesadd_sourcemap_ownedfor maps it can take. A mixed-path fallback keeps borrowedSourceimplementations correct and copies only their borrowed entries.RenderedModuleview. The text stays shared throughArc; 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.SourceMapSourcestores its map as anOptionand exposestake_sourcemap, leaving the plugin-facing view mapless while preservingRenderedModule.code.oxc_sourcemapis bumped to8.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
sourcemap/join_with_sourcemap(2,000 modules)join_with_sourcemapjoin_no_sourcemapjoinwall-clock in the original prototype (release, mimalloc)CodSpeed's aggregate verdict is a 10.13% performance improvement. It used
main@4b9c35ebecause 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.joinruns 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
RenderedModulestill exposes identical code after its map is detached, while chunk rendering receives that map.Verification
a078bad82.cargo clippy -p rolldown_sourcemap -p rolldown -p bench --all-targets -- --deny warningscargo test -p rolldown_sourcemapcargo 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 allocstwice with an identical snapshotUpstream
[email protected]