Skip to content

composeSourceMaps: Encode the composed map directly - #1983

Draft
robhogan wants to merge 1 commit into
pr1981from
pr1982
Draft

robhogan wants to merge 1 commit into
pr1981from
pr1982

Conversation

@robhogan

@robhogan robhogan commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Context

composeSourceMaps built its output with source-map's SourceMapGenerator, which allocates and validates an object per mapping and serialises by string concatenation:

const generator = new SourceMapGenerator({
file: consumers[0].file,
});
consumers[0].eachMapping(mapping => {
const original = findOriginalPosition(
consumers,
mapping.generatedLine,
mapping.generatedColumn,
);
generator.addMapping({
generated: {
line: mapping.generatedLine,

On a large map that was most of the remaining time.

This change

This collects the composed mappings into parallel arrays and encodes them with Metro's own B64Builder. Output is unchanged, including source and name ordering, de-duplication, sorting and the error for an invalid mapping.

That puts Metro's composer level with Expo's @jridgewell/remapping-based one (~680ms on the same input), with more faithful output. It's the point of this stack: it makes the case for Expo's Hermes export path using Metro's composer rather than its own.

composeSourceMaps is 20.6% faster than #1981 (95% CI 19.7-21.2%) and 3.25x faster than main (2,088ms -> 646ms on our benchmark app), with byte-identical output and no change in memory.

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
#1981 805ms (802 to 811) -61.4% (-61.6 to -61.1) 911MB (910 to 912) -14.9% (-15.0 to -14.7) 1,505MB
This PR 646ms (641 to 650) -20.6% (-21.2 to -19.7) -69.2% (-69.3 to -69.0) 914MB (907 to 915) 0.0% (-0.4 to +0.3) -14.8% (-15.2 to -14.6) 1,508MB

Changelog

 - **[Performance]**: `composeSourceMaps` encodes its output directly rather than through `source-map`

Test plan

Compared against #1981 on 40,000 randomly generated compositions of one to three maps, including repeated and out-of-order mappings, names, unmapped positions and invalid mappings. Every output and every error message is identical.

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 08:15
@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 force-pushed the pr1982 branch 3 times, most recently from fd8a027 to 5aabfda Compare September 28, 2026 12:22
@robhogan
robhogan requested a lite review from Copilot September 28, 2026 12:23

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 reviewed implementation has unresolved correctness issues, including a critical mapping truncation bug.

Review effort: Lite
Findings: 1 High severity 路 1 Medium severity

Open (2)
What changed in this PR

Optimizes composeSourceMaps by encoding maps directly with Metro鈥檚 B64Builder.

Changes:

  • Collects mappings in parallel arrays.
  • Sorts, deduplicates, and encodes mappings directly.
  • Preserves source-map metadata and ordering.
File Summary
packages/鈥媘etro-source-map/鈥媠rc/鈥媍omposeSourceMaps.js Direct encoding implementation. Findings include one critical line-gap truncation issue and two moderate correctness issues involving empty sources and null sorting.

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

Comment on lines +189 to +190
if (generatedLine !== previousGeneratedLine) {
builder.markLines(generatedLine - previousGeneratedLine);
generatedLine > 0 &&
generatedColumn >= 0 &&
(originalLine != null
? originalLine > 0 && (originalColumn ?? 0) >= 0 && source
`composeSourceMaps` built its output with `source-map`'s `SourceMapGenerator`, which allocates an object per mapping (on top of the three `composeSourceMaps` passed to each `addMapping`), validates each one, and serialises by concatenating a small string per VLQ field. On a large map that was most of the remaining time.

This collects the composed mappings into parallel arrays, assigning source and name indices as they're added, and encodes them with Metro's own `B64Builder`. The output is unchanged: sources and names are indexed in order of first appearance, a mapping identical to the one before it is dropped, mappings that arrive out of order are sorted with the same comparator, and an invalid mapping throws the same error.

`composeSourceMaps` with a bundle map and its Hermes map is 20.6% faster than the previous diff (95% CI 19.7-21.2%) and 3.25x faster than `main` (3.23-3.26x; 805ms -> 646ms on our benchmark app*), with byte-identical output and no change in memory (0MB, 95% CI -4 to 3MB*).

| | 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 | 805ms (802 to 811) |  | -61.4% (-61.6 to -61.1) | 911MB (910 to 912) |  | -14.9% (-15.0 to -14.7) | 1,505MB |
| This diff | 646ms (641 to 650) | -20.6% (-21.2 to -19.7) | -69.2% (-69.3 to -69.0) | 914MB (907 to 915) | 0.0% (-0.4 to +0.3) | -14.8% (-15.2 to -14.6) | 1,508MB |

## Changelog

```
 - **[Performance]**: `composeSourceMaps` encodes its output directly rather than through `source-map`
```

## Test plan

Compared against the previous diff on 40,000 randomly generated compositions of one to three maps, including repeated and out-of-order mappings, names, unmapped positions and invalid mappings. Every output and every error message is identical.

```
yarn jest
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).

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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants