Skip to content

Source map consumer: Decode VLQ mappings in a single pass - #1979

Open
robhogan wants to merge 2 commits into
mainfrom
pr1979
Open

robhogan wants to merge 2 commits into
mainfrom
pr1979

Conversation

@robhogan

@robhogan robhogan commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

MappingsConsumer decoded a map by slicing each segment out as a string, decoding it with vlq through a Map cache, and spreading a generator into an array:

const mappingRaw = mappingsRaw.slice(i, next);
let decodedVlqValues;
if (vlqCache.has(mappingRaw)) {
decodedVlqValues = vlqCache.get(mappingRaw);
} else {
decodedVlqValues = decodeVlq(mappingRaw);
vlqCache.set(mappingRaw, decodedVlqValues);

This decodes base64 VLQ in place from character codes, in one pass. Output is unchanged, including the errors for malformed segments, and vlq is dropped from metro-source-map's dependencies.

composeSourceMaps is 1.63x faster than #1978 (95% CI 1.62-1.64x) and 1.89x faster than main (1,801ms -> 1,107ms on our benchmark app), with 16% less memory (1,069MB -> 898MB) and identical output.

Benchmark

AI-driven.

Time (median) vs previous vs main Composition memory vs previous vs main Peak RSS
main 2,088ms (2,082 to 2,094) 1,070MB (1,070 to 1,070) 1,664MB
#1978 1,801ms (1,794 to 1,806) -13.6% (-13.8 to -13.3) 1,069MB (1,069 to 1,069) -0.1% (-0.2 to -0.1) 1,663MB
This PR 1,107ms (1,099 to 1,111) -38.6% (-38.9 to -38.3) -47.1% (-47.4 to -46.7) 898MB (897 to 899) -16.0% (-16.1 to -15.8) -15.9% (-16.1 to -15.6) 1,492MB

Changelog

 - **[Performance]**: Decode source maps in one pass in `metro-source-map`'s `Consumer`

Test plan

New tests for multi-digit and negative deltas, empty segments and lines, and malformed segments. They pass on main too.

Benchmark (AI-driven): Mattermost Mobile 2.45.0 (React Native 0.83.9, Metro 0.83.7), iOS release bundle, unminified: 7,086 sources, 52.6MB bundle, 82.4MB flat source map. Composed with its hermesc -O -output-source-map map (hermes-compiler 0.14.1, 10.7MB, 1.84M segments). Timings are composeSourceMaps alone, excluding JSON parsing, over 105 rounds on Node 22.13.1 on an M5 Pro. Each round runs main, every PR in this stack and a memory baseline, in a shuffled order. Figures are medians with 95% bootstrap confidence intervals, for changes of the per-round paired ratio. Composition memory is peak RSS less that of the same process parsing both maps without composing them (594MB).

@robhogan
robhogan added this pull request to stack #1982 September 26, 2026 07:54
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 26, 2026
@robhogan robhogan changed the title Consumer: Decode VLQ mappings in a single pass Source map consumer: Decode VLQ mappings in a single pass Sep 28, 2026
@robhogan
robhogan marked this pull request as ready for review September 28, 2026 12:08
@robhogan
robhogan requested a lite review from Copilot September 28, 2026 12:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

馃煛 Changes recommended

The decoder has unresolved correctness and malformed-input compatibility issues.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Optimizes source-map VLQ decoding with a single-pass inline decoder and removes the direct vlq dependency.

Changes:

  • Replaces per-segment decoding with in-place character-code parsing.
  • Adds coverage for complex and malformed mappings.
  • Removes vlq from metro-source-map dependencies.
File Summary Findings
packages/鈥媘etro-source-map/鈥媠rc/鈥婥onsumer/鈥婱appingsConsumer.js Implements single-pass VLQ decoding. Critical unsigned-shift compatibility issue; truncated fields may be silently accepted; malformed-input error text changes.
packages/鈥媘etro-source-map/鈥媠rc/鈥媉_tests__/鈥婥onsumer-test.js Adds decoding and malformed-input tests. No findings.
packages/鈥媘etro-source-map/鈥媝ackage.json Removes the obsolete dependency. No findings.

馃挕 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/metro-source-map/src/Consumer/MappingsConsumer.js Outdated
"nullthrows": "^1.1.1",
"ob1": "0.87.1",
"source-map": "^0.5.6",
"vlq": "^1.0.0"

@robhogan robhogan Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is safe to merge without Meta import because even though we're dropping a dependency here, metro-symbolicate has the same vlq dependency, so no lockfile change.

(This code snippet makes it look like we're dropping two deps - we're not, source-map just loses the trailing comma)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

馃數 Needs a closer look

One or more issues must be addressed before approval.

Review effort: Lite
Findings: None

Resolved since last review (1)

@robhogan
robhogan requested a review from huntie September 28, 2026 12:18
Base automatically changed from pr1978 to main September 28, 2026 13:17
robhogan and others added 2 commits September 28, 2026 14:17
`MappingsConsumer` decoded a map by slicing each segment out as a string, decoding it with `vlq` through a `Map` cache, and yielding it from a generator that was then spread into an array. This decodes base64 VLQ in place from character codes in one pass, straight into the array.

Output is unchanged, including the errors thrown for segments with two or three fields. The only difference is the message for a character that isn't base64. `metro-source-map` no longer uses `vlq`, so it's removed from its dependencies (it's still a dependency of `metro-symbolicate`).

`composeSourceMaps` with a bundle map and its Hermes map is 1.63x faster than the previous diff (95% CI 1.62-1.64x) and 1.89x faster than `main` (1.88-1.90x; 1,801ms -> 1,107ms on our benchmark app*). Composition's memory falls by 16.0% (15.8-16.1%; 1,069MB -> 898MB*). The output is byte-identical.

| | Time (median) | vs previous | vs `main` | Composition memory* | vs previous | vs `main` | Peak RSS |
|---|---|---|---|---|---|---|---|
| `main` | 2,088ms (2,082 to 2,094) | | | 1,070MB (1,070 to 1,070) | | | 1,664MB |
| Previous diff | 1,801ms (1,794 to 1,806) |  | -13.6% (-13.8 to -13.3) | 1,069MB (1,069 to 1,069) |  | -0.1% (-0.2 to -0.1) | 1,663MB |
| This diff | 1,107ms (1,099 to 1,111) | -38.6% (-38.9 to -38.3) | -47.1% (-47.4 to -46.7) | 898MB (897 to 899) | -16.0% (-16.1 to -15.8) | -15.9% (-16.1 to -15.6) | 1,492MB |

## Changelog

```
 - **[Performance]**: Decode source maps in one pass in `metro-source-map`'s `Consumer`
```

## Test plan

New tests cover multi-digit and negative deltas, empty segments and lines, and malformed segments. They pass on `main` and on this diff.

```
yarn jest packages/metro-source-map packages/metro-symbolicate
yarn flow check
yarn lint
```

\* Benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9, Metro 0.83.7), iOS release bundle, unminified: 7,086 sources, 52.6MB bundle, 82.4MB flat source map. Composed with its `hermesc -O -output-source-map` map (hermes-compiler 0.14.1; 10.7MB, 1.84M segments). Timings are `composeSourceMaps` alone, excluding JSON parsing, over 105 rounds on Node 22.13.1 on an M5 Pro; each round runs `main`, every diff in this stack and a memory baseline, in a shuffled order. Figures are medians with 95% bootstrap confidence intervals - for changes, of the per-round paired ratio. Composition memory is peak RSS less that of the same process parsing both maps without composing them (594MB).
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 28, 2026

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

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants