perf: cache last source/name indexOf result in _serializeMappings - #58
Merged
Conversation
Consecutive mappings almost always share `mapping.source` (and frequently `mapping.name`) — bundler output groups mappings by source, then by name. The previous code called `this._sources.indexOf(mapping.source)` and `this._names.indexOf(mapping.name)` on every iteration, each hitting the ArraySet's Map.get for a string already resolved on the previous pass. Single-slot caches (`lastSourceStr → lastSourceIdx`, same for name) skip the redundant lookup whenever the string matches. On miss, fall back to indexOf and refresh the slot. No public surface change. bench-diff.sh main generate (two-process, SOLO=1 PHASES=generate): amp.js.map +8.3% babel.min.js.map +1.9% issue-41.js.map +14.8% preact.js.map +4.0% react.js.map +4.0% vscode.map +5.0% mean +6.3%
7rulnik
added a commit
that referenced
this pull request
May 10, 2026
…eMap (#64) * perf: typed-array MappingList — slab storage + slab-direct serializer + 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. * perf: BasicSourceMapConsumer.fromSourceMap reads MappingList slab directly 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: coverage for typed-array MappingList and slab paths - 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. * test: cover applySourceMap with source-less mappings + drop unreachable 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 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
SourceMapGenerator._serializeMappingscallsthis._sources.indexOf(mapping.source)andthis._names.indexOf(mapping.name)once per mapping. The underlyingArraySet.indexOfdoes aMap.get(str)— cheap individually, but consecutive mappings almost always share their source string (bundler output groups by source) and frequently share their name string too, so the lookup is mostly redundant.This PR adds single-slot caches —
(lastSourceStr, lastSourceIdx)and(lastNameStr, lastNameIdx)— kept across loop iterations. On a cache hit (same string as last call) we skipindexOf; on a miss we fall back and refresh the slot. Identity-checked with===, no allocation. Public surface unchanged.Bench
scripts/bench-diff.sh main generate(two-process,SOLO=1 PHASES=generate, node v24.13.0):Tests
yarn test:coveragethresholds (98/97/96) green:lib/source-map-generator.js: 100/100/100