Skip to content

sourcemap: give a segment a name only when it has a name field - #47796

Open
robobun wants to merge 1 commit into
mainfrom
robobun/0526d078/sourcemap-name-per-segment
Open

robobun wants to merge 1 commit into
mainfrom
robobun/0526d078/sourcemap-name-per-segment

Conversation

@robobun

@robobun robobun commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • SourceMap.findEntry() and findOrigin() (node:module) give a four-field mappings segment an earlier segment's name: with names ["x", "y"] and mappings AAAAA,IAAI;, findEntry(0, 4).name is "x". Node.js v26.3.0 answers undefined. No user reported it.
  • Both decoders store the running name index in each row after the first name: src/sourcemap/Mapping.rs:715, src/jsc/bindings/highway_sourcemap.cpp:604, :766, :965.

Fix

  • Each decoder stores the name index of the segment's own fifth field, or -1. has_names then has no reader and is deleted.
  • The kernel's batched path prefetches its output. Edits to other lines of this function moved that path's speed on cold output by up to 25%.
  • Verified: test/js/node/module/sourcemap.test.js, sourcemap-simd.test.ts (15 and 3 tests fail on main). Also test/js/bun/sourcemap/.
  • Self-reviewed: 7 concerns raised, 7 addressed.

Background

  • A mappings segment has 1, 4 or 5 VLQ fields. The fifth is a delta on a running name index.
  • From 128 bytes a SIMD kernel decodes. Its batched path takes a block of four-field segments at once.
  • Considered fixing the four stores only: that leaves a flag that decides nothing.

Downsides

Notes

Who it reaches

  • No issue asks for this. Only new SourceMap(payload) keeps names (src/runtime/api/JSSourceMap.rs:154 has the one allow_names: true). So it reaches a caller that builds a SourceMap from a map with names and reads .name. Stack traces do not read names.
  • A first-party case: bun compile --minify --sourcemap=external writes a name only for a site inside a named function (src/aot/report/obfuscation_map.rs). docs/compiler/debugging.mdx says that any source map library recovers the names. On main, Bun's own SourceMap names two anonymous frames of a 14-line program traverseArray and map. Node.js and this PR give them no name.
  • Real maps: find test/node_modules -name '*.map' -type f lists 8,006 files, 594 with a named segment. An independent decoder checked findEntry().name at 5,929,755 segments. Main names 1,544,702 four-field segments. Node.js v26.3.0 names 451, each the unterminated last segment of a map. This PR names none, with the kernel and with the scalar decoder. All 625,603 five-field segments have their own name in all three.

Node.js and the end of the string

Node.js sets name per segment, only when a fifth field follows: https://github.com/nodejs/node/blob/v26.3.0/lib/internal/source_map/source_map.js#L304-L308. At the end of the string peek() returns '', which is not a separator, so Node.js reads fields that are not there. nodejs/node#63795 reports that. Its fix PRs nodejs/node#63801 and nodejs/node#66264 are open.

mappings, names ["x", "y"] lookup Node.js v26.3.0 main this PR
AAAAA,IAAI; 0:4 no name x no name
AAAAA,CAAC 0:1 x x no name
AAAAA,CAAC, 0:1 x x no name
AAAA 0:0 x no name no name
AAAAA,C,CAAC; 0:1 row 0:1, no name row 0:0, x row 0:0, x
  • Rows 2 to 4 are the deliberate difference from Node.js. ECMA-426 gives those segments no name, and main already answers no name in row 4.
  • The tc39 fixture vendored at test/js/node/test/fixtures/test426 expects mappedName: null for each four-field segment of basicMapping. Main fails 7 of its 12 checks. A new test runs that fixture test.
  • To follow Node.js v26.3.0 at the end of the string, change the None arm of the match in mapping::parse.
  • Row 5 is a one-field segment. Bun keeps no row for it. sourcemap: keep a mappings segment that has no source #47734 changes that.
  • findOrigin takes 0-based arguments in Bun and 1-based arguments in Node.js. This PR does not change that. The new findOrigin test uses a map with two equal lines, so it holds for both.

The batched path and the prefetch

  • With the name rule alone, new SourceMap over 200,000 batched segments (oBAAC,) took 13% to 28% longer on an AVX-512 host. The loop drops each map before it builds the next. It was +7% at 16,000 segments and equal at 2,000 and at 2,000,000. Instructions per row fell (95.1 to 92.5).
  • The name store is not the cause. An inline vector store in place of the memset call measured the same. A harness calls the kernel on output that is not in the cache. There main runs 26.1 cycles per row and the name rule alone 33.2. Main with only the general loop's name store changed runs 28.8 to 33.1, and this input does not reach that loop.
  • The path copies its rows out with memcpy after two loads of staging arrays stall on store forwarding. How early the output lines reach the cache depended on how the function compiled. Demand fills from DRAM over 200 constructs: 180,000 to 290,000 on main, 830,000 to 1,330,000 with the name rule alone, 43,000 to 47,000 with the prefetch.
  • The path now calls hwy::Prefetch on its four output columns, 64 rows ahead.
  • aarch64 is unchanged: hwy::Prefetch compiles to nothing without SSE2. No ARM machine was available to measure that path.

new SourceMap, wall ns per row, release builds of main 3f3a9ff and of this PR, AMD EPYC with AVX-512, median of 5 interleaved rounds:

input segments main this PR
batched (oBAAC,) 2,000 7.14 7.19 (+0.7%)
batched 8,000 6.72 6.63 (-1.3%)
batched 20,000 7.04 6.83 (-3.0%)
batched 200,000 8.40 8.01 (-4.5%)
batched 2,000,000 10.37 10.39 (+0.3%)
uniform (CAAC,) 200,000 4.90 4.75 (-2.9%)
every third segment named 200,000 8.51 8.27 (-2.8%)
three segments per line 200,000 7.77 7.33 (-5.6%)
every segment named 200,000 8.06 8.00 (-0.8%)
uniform, scalar decoder only 200,000 10.06 9.80 (-2.6%)
uniform, 99 bytes 20 19.78 18.91 (-4.4%)

Another run of the 2,000-segment row (8 rounds) measured +1.3%.

Kernel cycles per row in the harness, 30 cells (5 shapes, block 16/32/64, name column on/off), median of 4 rounds, this PR against main:

  • Output not in the cache: 24 cells below main, 6 above, -15.3% to +1.5%. The batched path: -3.3% to -15.3%.
  • Output in the cache: 20 below, 10 above, -5.9% to +2.3%. The cell at +2.3% is the uniform path without a name column, which this PR does not change.

Measurements

  • Kernel instructions per row, 36 ThinLTO cells (6 shapes x block 16/32/64 x name column on/off), main 3f3a9ff against this PR: 33 below, 3 above. The 3 above: the batched path at block 16 (+1.5% and +1.3%) and five-field segments at block 64 without a name column (+0.3%). NEON under qemu, 12 cells: 10 below, 2 equal.
  • new SourceMap instructions per row, main to PR, SIMD / scalar-only: uniform4 51.18 to 49.35 / 155.06 to 151.02, batch4 95.29 to 93.61 / 165.90 to 161.91, mixed5 200.00 to 200.27 / 233.89 to 223.58, lines4 142.58 to 140.83 / 159.51 to 155.74, all5 145.13 to 144.72 / 190.48 to 183.30. Run-to-run noise is 0.15%.
  • Row memory (estimateShallowMemoryUsageOf): name-less new SourceMap, 20 rows: 1,048 to 1,048 bytes. 200,000 rows: 5,734,496 to 5,734,496 bytes.
  • ParseMappingsImpl 36,661 to 36,018 bytes (8 ISA copies), mapping::parse 5,424 to 5,342. Release binary 140,474,656 to 140,474,656 bytes.
  • Calls to memcpy / memset inside ParseMappingsImpl, per ISA copy: main 3 / 4, PR 3 / 5, plus 4 prefetcht0. The batched path fills the name column with memset.
  • bench/snippets/source-map-parse.mjs, 9.5 MB map: main 11.34 ms, PR 11.20 ms (median of 6 interleaved rounds, spread 8%).
  • tc39 fixture name mismatches: 7 of 35 to 0.
  • scripts/verify-baseline-static passes on the release build.

Design notes

  • name_idx = WrapAdd(name_idx, d_name) has no condition on purpose. Under if (field_count == 5) the kernel is above main in 13 of the 36 instruction cells (up to +4.3%, AVX2 copy).
  • The scalar loop still moves the list to the variant with a name column at the first name, as on main. A choice of the variant up front from allow_names measured +7% to +12% time for a name-less map with the SIMD decoder off, because each row is 24 bytes and not 20.
  • hn::StoreN for the batched name store is above main in 11 of the 12 NEON instruction cells. A prefill of the name column with -1 adds a pass over the column to each construct.

Tests

  • Each of the four stores is observed. With one store changed back to the running index, the new SIMD test fails. Checked for each store with the kernel forced to 16, 32 and 64-byte blocks.
  • sourcemap-simd.test.ts pinned the carried name in two places (entries[81] and entries[139] were "n0"). They are null now, as in Node.js.
  • The debug-only test reads the two block counters that moved in ParseMappingsState. The kernel indexes that state by position and Rust by field name.
  • Also run: all of test/js/bun/sourcemap/, test/js/node/module/module-sourcemap.test.js, test/cli/install/bun-pm-diff.test.ts, test/js/bun/util/inspect-error.test.js, test/bake/dev/sourcemap.test.ts, test/bake/dev/server-sourcemap.test.ts.

Other open PRs in these files


[human-review] gate passed · iteration 0 · 5 files touched

fails on main (without fix)
ASAN without fix: 18 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/module/sourcemap-simd.test.ts test/js/node/module/sourcemap.test.js
bun test v1.4.4 (367d939d9)

test/js/node/module/sourcemap-simd.test.ts:
(pass) SourceMap SIMD mappings decode > all 1-char 4-field segments (the 76% case) [861.64ms]
(pass) SourceMap SIMD mappings decode > mixed 1/2/3-char VLQs in each of the four positions [850.10ms]
(pass) SourceMap SIMD mappings decode > 2-char VLQ in each of the five positions (Masked-VByte shuffle) [856.41ms]
(pass) SourceMap SIMD mappings decode > 5-field segments with names [797.39ms]
249 |     }
250 |     const { mappings, probes, sourcesLen, namesLen } = build([segs]);
251 |     expect(mappings.length).toBeGreaterThanOrEqual(128);
252 |     const scalar = await assertSimdMatchesScalar("4-5-mixed", mappings, sourcesLen, namesLen, probes);
253 |     expect(scalar.error).toBeNull();
254 |     expect(scalar.entries.map(entry => entry.name)).toEqual(segs.map((_, i) => (i % 3 === 0 ? `n${i / 3}` : null)));
                                                          ^
error: expect(receive
... (truncated)

release without fix: 1 skipped
bun test v1.4.4-canary.1 (5ae41ef38)

test/js/node/module/sourcemap-simd.test.ts:
(skip) SourceMap SIMD mappings decode > the debug log counts the blocks of the fast paths and of the general loop
(pass) SourceMap SIMD mappings decode > all 1-char 4-field segments (the 76% case) [99.01ms]
(pass) SourceMap SIMD mappings decode > 2-char VLQ in each of the five positions (Masked-VByte shuffle) [85.31ms]
(pass) SourceMap SIMD mappings decode > 5-field segments with names [82.46ms]
(pass) SourceMap SIMD mappings decode > interleaved 4- and 5-field segments [83.20ms]
(pass) SourceMap SIMD mappings decode > 4-field segments after a 5-field segment have name: null in every decode path [83.43ms]
(pass) SourceMap SIMD mappings decode > all-4-field map with non-empty names: every row has name: null [83.36ms]
(pass) SourceMap SIMD mappings decode > 1-field (generated-column-only) segments are skipped but accumulate [69.47ms]
(pass) SourceMap SIMD mappings decode > 4-field rows before the first 5-field segment keep name: null [79.48ms]
(pass) SourceMap SIMD mappings decode > semicolon runs (line resets) and trailing ';' [61.77ms]
(pass) SourceMap SIMD mappings decode > block-boun
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/module/sourcemap-simd.test.ts test/js/node/module/sourcemap.test.js
bun test v1.4.4 (367d939d9)

test/js/node/module/sourcemap-simd.test.ts:
(pass) SourceMap SIMD mappings decode > mixed 1/2/3-char VLQs in each of the four positions [1003.10ms]
(pass) SourceMap SIMD mappings decode > all 1-char 4-field segments (the 76% case) [1143.24ms]
(pass) SourceMap SIMD mappings decode > interleaved 4- and 5-field segments [921.09ms]
(pass) SourceMap SIMD mappings decode > 2-char VLQ in each of the five positions (Masked-VByte shuffle) [1139.65ms]
(pass) SourceMap SIMD mappings decode > 5-field segments with names [1052.84ms]
(pass) SourceMap SIMD mappings decode > 4-field segments after a 5-field segment have name: null in every decode path [1059.93ms]
(pass) SourceMap SIMD mappings decode > the debug log counts the blocks of the fast paths and of the general loop [1100.01ms]
(pass) SourceMap SIMD mappings decode > all-4-field map with non-empty names: every row has name: null [1123.13ms]
(pass) SourceMap SIMD mappings decode > 1-fie
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 964ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/424] build.rs build_script_build
[2/424] gen generated_host_exports.rs
generated_host_exports.rs: 119 exports (lazy=4, generic=115, rust=0); 288 extern-C blocks audited
[3/423] gen cpp.rs (cppbind)
[4/423] rustc bun_highway 
[5/423] rustc bun_hash 
[6/423] rustc bun_safety 
[7/423] rustc bun_zlib_sys 
[8/423] rustc bun_zstd 
[9/423] rustc bun_boringssl_sys 
[10/423] rustc bun_output 
[11/423] rustc bun_brotli 
[12/423] rustc bun_perf 
[13/423] rustc bun_ptr 
[14/423] rustc bun_cares_sys 
[15/423] rustc bun_picohttp 
[16/423] rustc bun_base64 
[17/423] rustc bun_errno 
[18/423] rustc bun_tcc_sys 
[19/423] rustc bun_clap 
[20/423] rustc bun_lsquic_sys 
[21/423] rustc bun_valkey 
[22/423] rustc bun_text_diff 
[23/423] rustc bun_zlib 
[24/423] rustc bun_paths 
[25/423] rustc bun_collections 
[26/423] rustc bun_url 
[27/423] rustc bun_shell_parser 
[28/423] rustc bun_semver 
[29/423] rustc bun_png 
[30/423] rustc bun_analytics 
[31/423] rustc bun_which 
[32/423] rustc bun_tar 
[33/423] rustc bun_dns 
[34/423
... (truncated)
diff hotspot
src/highway/lib.rs                         |   7 +-
 src/jsc/bindings/highway_sourcemap.cpp     |  40 ++++++-----
 src/sourcemap/Mapping.rs                   | 110 +++++++++++++----------------
 test/js/node/module/sourcemap-simd.test.ts |  93 +++++++++++++++++++++---
 test/js/node/module/sourcemap.test.js      |  96 +++++++++++++++++++++++++
 5 files changed, 254 insertions(+), 92 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                        reads  edits  tests
src/highway/lib.rs                              0      0     47
src/jsc/bindings/highway_sourcemap.cpp          1      0     50
src/sourcemap/Mapping.rs                        2      0     47
test/js/node/module/sourcemap-simd.test.ts      0      0     29
test/js/node/module/sourcemap.test.js           2      1     35

A four-field mappings segment that came after a five-field segment got
the name of that earlier segment. Both decoders stored the running name
index once any segment had a name. Each row now stores the name index
of its own fifth field, or -1.

The parse-wide has_names flag has no reader after that, so it is gone
from the kernel state and from the scalar loop.

The batched path of the kernel now prefetches its output. Its speed on
output that is not in the cache depended on how the function happened
to compile.
@robobun

robobun commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:17 PM PT - Nov 2nd, 2026

❌ @robobun, your commit 2b63fc3 has 41 failures in Build #137050 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47796

That installs a local version of the PR into your bun-47796 executable, so you can run:

bun-47796 --bun

@github-actions github-actions Bot added the claude label Nov 2, 2026
@robobun

robobun commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduction, on main (3f3a9ff) and on Bun 1.4.2:

const { SourceMap } = require("node:module");
const map = new SourceMap({ version: 3, sources: ["a.ts"], names: ["x", "y"], mappings: "AAAAA,CAAC,CAAC;" });
console.log([0, 1, 2].map(column => map.findEntry(0, column).name));
  • Bun on main prints [ "x", "x", "x" ].
  • Node.js v26.3.0 prints [ 'x', undefined, undefined ].
  • Bun with this PR prints [ "x", undefined, undefined ].

test/js/node/module/sourcemap.test.js and test/js/node/module/sourcemap-simd.test.ts fail on main (15 and 3 tests) and pass with this PR.

CI

Build 137050 finished. No failure touches this diff.

  • 28 test files are red. 27 of them are also red in the builds of main at the base of this branch (137005, a4c3a5e) and one commit later (137093): 26 under test/aot/, and test/bundler/compile-aot-size.test.ts.
  • The other red file is test/js/web/request/request-clone-leak.test.ts on the ASAN lane. It measured an RSS delta of 64 MB against a limit of less than 64 MB.
  • 37 more test files failed one time and passed on the retry.
  • All 14 build jobs passed.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between a4c3a5e and 2b63fc3.

Files selected for processing: 5
  • src/highway/lib.rs
  • src/jsc/bindings/highway_sourcemap.cpp
  • src/sourcemap/Mapping.rs
  • test/js/node/module/sourcemap-simd.test.ts
  • test/js/node/module/sourcemap.test.js

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The source-map parsers now assign name indices per segment: five-field segments carry decoded names, while four-field segments have no name. The parser state no longer stores has_names. Tests cover scalar and SIMD decoding, lookup behavior, fixture results, and truncated name-field VLQs.

Changes

Source-map name handling

Layer / File(s) Summary
Parser state layout
src/highway/lib.rs, src/jsc/bindings/highway_sourcemap.cpp
The Rust and C++ parser state layouts no longer include has_names; their size checks specify nine i32 values.
Per-segment name parsing
src/sourcemap/Mapping.rs, src/jsc/bindings/highway_sourcemap.cpp, test/js/node/module/sourcemap-simd.test.ts, test/js/node/module/sourcemap.test.js
Scalar and SIMD parsing assign names to segments with a fifth field and return no name for four-field segments. The SIMD batch path prefetches output columns 64 rows ahead. Tests cover segment boundaries, lookup results, fixture mappings, SIMD paths, and truncated name-field VLQs.

Possibly related PRs

  • oven-sh/bun#47380: Changes VLQ decoding for long fields in the same SIMD/scalar mapping decoder path.

Suggested reviewers: alii, jarred-sumner

Merge Risk: Minimal · up to 2b63f

The change prevents unnamed source-map segments from inheriting earlier names. No actionable merge-blocking risk is established by the supplied evidence; merge after normal checks pass.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: assigning a name to a segment only when the segment contains a name field.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is complete and directly addresses the required information. It explains what the PR changes, the reported problem, design decisions, limitations, performance impact, and verification …
Full details: Description check

Explanation

The description is complete and directly addresses the required information. It explains what the PR changes, the reported problem, design decisions, limitations, performance impact, and verification results. It does not use the exact template headings, but it includes equivalent content and detailed test evidence.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked: the new hwy::Prefetch calls can form addresses up to 64 rows past the end of the output buffer, but prefetch hints never fault and no store past os_end was added; the kSt* renumbering in highway_sourcemap.cpp matches the 9-field Rust ParseMappingsState on both sides (size and offsetof asserts updated together); and the unconditional name_idx accumulate in the general loop is sound because d_name is zero-initialized and only assigned when field_count == 5.

Extended reasoning...

The change touches the SIMD source-map kernel (C++), its Rust state mirror, and the scalar parser in src/sourcemap/Mapping.rs, plus tests; it touches no auth, crypto, or injection surface. The one inline finding concerns 6-field segment handling that the PR reformats but does not change in behavior. The note records the kernel-level concerns I ruled out from reading the diff so a human reviewer can focus elsewhere.

Comment thread src/sourcemap/Mapping.rs Resolved

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant