perf: typed-array MappingList with slab-direct serializer + fromSourceMap#64
Merged
Conversation
… + applySourceMap rebuild
Replace lib/mapping-list.js's `Array<{...mappingObject}>` backing store with
an Int32Array slab. 6 i32 slots per mapping (genLine, genCol, srcIdx,
origLine, origCol, nameIdx), -1 sentinel for "no value". Source/name
strings live in the owning SourceMapGenerator's ArraySets; MappingList
stores the resolved indices so the serializer doesn't need a per-mapping
`indexOf` (which previously hit the ArraySet's Map.get even after the
single-slot indexOf cache from #58).
Per bench-data-followups #2 — generator memory ratio ≈ generator speed
ratio. The win is heap-pressure / GC / cache: 350k mapping objects per
addMapping cycle becomes 350k * 24 bytes contiguous slab.
`MappingList.add(genLine, genCol, origLine, origCol, srcIdx, nameIdx)`
takes positional integer args. `SourceMapGenerator.addMapping` resolves
sources/names through the existing ArraySets (which addMapping was already
calling) and passes the integer index in. No more `{ ... }` object literal
allocation per addMapping.
`SourceMapGenerator.applySourceMap` no longer mutates mapping objects
through the unsortedForEach callback — that pattern relied on the callback
receiving the actual stored reference, which slab storage can't provide.
The rewrite walks the old slab via the F_* constants, applies the
transformation, and emits into a fresh MappingList bound to the new
sources/names ArraySets, then swaps all three at once.
`SourceMapGenerator._serializeMappings` reads the slab directly instead of
toArray(). Source/name fields stored in the slab are already the int
indices needed for serialization (resolved when added) — no per-mapping
`indexOf` lookup. The compareByGeneratedPositionsInflated dedup check is
replaced with `ml._equalsPrev(i)` — a slab method that does six i32
equality checks. Same equality classes as the old check (same srcIdx ⇔
same source string after interning) so the dedup still works.
`unsortedForEach` and `toArray` remain API-compatible by materializing
JS objects on demand from the slab. Internal hot paths bypass them.
…ectly Drop the `aSourceMap._mappings.toArray().slice()` materialization. The generator's MappingList already stores source/name as integer indices into its _sources / _names ArraySets, and the new consumer's _sources / _names were just initialized from the same toArray() output — so the indices are identical and no per-mapping `indexOf` is needed. Reads i32 fields straight from `ml._buf` via the F_* layout constants exported from `lib/mapping-list.js`.
- test/internal/test-mapping-list.js (new): unit tests for the MappingList surface — unsortedForEach / toArray materialization, the 6-level sortedness cascade in `add`, the matching cascade in `_sort`'s comparator, the `_equalsPrev` dedup helper, and slab capacity growth. - test/internal/test-source-map-consumer-internals.js: add a fromSourceMap test that exercises a source-less generated mapping (case-1 per _validateMapping) so the `srcIdx === -1` slab-read branch is covered. - test/internal/test-util.js: pin direct genLine/genCol coverage for compareByGeneratedPositionsInflated. The function was previously covered transitively through MappingList.add's generatedPositionAfter; the new slab-backed MappingList doesn't call it, so those early-return branches need direct test coverage.
…le origCol guard Two coverage holes flagged by the per-branch lcov: - lib/source-map-generator.js:253 — the `srcIdx === -1 ? null : ...` ternary's `null` arm only fires when applySourceMap walks a source-less mapping, and existing applySourceMap tests are all fully-sourced. - lib/source-map-generator.js:259 — `origCol === -1 ? 0 : origCol` was unreachable. addMapping sets `origLine` and `origCol` together, so inside the `origLine !== -1` branch above, `origCol === -1` never fires. Dropping the guard removes the dead branch. Adds an applySourceMap test in test-source-map-consumer-internals.js that feeds the outer generator a source-less mapping alongside a fully-sourced one and asserts the source-less mapping passes through unchanged while the sourced one transforms. Bumps all-files branch coverage 96.94 → 97.21 (above the 97 threshold). source-map-generator.js is now 100/100/100.
This was referenced May 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replace
lib/mapping-list.js'sArray<{...mappingObject}>backing store with anInt32Arrayslab. 6 i32 slots per mapping (genLine,genCol,srcIdx,origLine,origCol,nameIdx),-1sentinel for "no value". Source/name strings live in the owningSourceMapGenerator's ArraySets; MappingList stores the resolved indices.Per
bench-data-followups.md#2 — generator memory ratio ≈ generator speed ratio. The win is heap-pressure / GC / cache: ~350k mapping-object allocations per addMapping cycle become 350k × 24 bytes of contiguous slab. The slab-direct serializer also drops theindexOfper mapping (the int index was already resolved when adding).Sequenced commits
perf: typed-array MappingList — slab storage + slab-direct serializer + applySourceMap rebuild—lib/mapping-list.jsrewritten withInt32Arraybacking;SourceMapGenerator.addMappingtakes positional integer args;_serializeMappingsreads the slab directly via the exportedF_*constants and uses a newml._equalsPrev(i)dedup helper;applySourceMapwalks the old slab and rebuilds a freshMappingListbound to newsources/namesArraySets (replaces the unsortedForEach-with-mutation pattern that no longer works with materialized callbacks).perf: BasicSourceMapConsumer.fromSourceMap reads MappingList slab directly— drops thetoArray().slice()materialization. The smc's_sources/_namesare initialized from the sameaSourceMap._sources/_names, so the integer indices in the slab transfer directly.test: coverage for typed-array MappingList and slab paths— newtest/internal/test-mapping-list.js; source-less mapping test added totest-source-map-consumer-internals.js; directcompareByGeneratedPositionsInflatedline/column tests added totest-util.js(the function used to be covered transitively throughMappingList.add; the slab rewrite removed that coupling).test: cover applySourceMap with source-less mappings + drop unreachable origCol guard— final coverage cleanup. Tested the applySourceMap path on source-less mappings and dropped an unreachableorigCol === -1defensive check (addMapping pairs origLine and origCol so they're either both -1 or both numeric).API surface: unchanged.
MappingList.unsortedForEachandMappingList.toArrayremain API-compatible by materializing JS objects on demand from the slab — only internal hot paths (_serializeMappings,BasicSourceMapConsumer.fromSourceMap) bypass them._validateMapping,addMappingargs,applySourceMapargs,fromSourceMapargs,toJSON/toStringoutput — all unchanged.Bench
Generate path —
bench-diff.sh main generate(SOLO=1 PHASES=adding,generate)addMappingwins scale with map size — biggest fixtures (babel.min 347k, vscode 2M) see the largest improvements because per-mapping object allocation savings dominate. The smallest fixtures (preact 2k, react 6k) see a small regression onaddMappingfrom the new positional-index resolution overhead, but recoup it on serialize.fromSourceMap —
scripts/bench-fromsourcemap.jsTests
yarn test:coverage:mapping-list.js100/100/100,source-map-generator.js100/100/100,source-map-consumer.js99.93/98.34/97.30. All-files: 98.35 / 97.21 / 96.75 — all above the 98 / 97 / 96 thresholds.Open follow-ups (out of scope here)
applySourceMapcould pass the oldsourcesArraySet'sat(idx)result directly to the new ArraySet without going through string interning twice — micro-optimization.ArraySet.addcould return the inserted index, dropping the trailingindexOfcall inaddMapping. Worth a separate PR.lastGeneratedColumnside storage (per the design proposal, option b) is not needed because the field is consumer-side only.