From 47c8db8907f2d6550c5973619b68bf8b8181516a Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 23 Jun 2026 07:54:16 -0700 Subject: [PATCH 1/2] Memory profiling harness and baseline investigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Scripts and findings for profiling Metro's memory and CPU during bundling, and an end-to-end benchmark of the compact VLQ source-map work stacked on top. **Methodology:** - Start Metro with `NODE_ARGS="--expose-gc --inspect=9230" DEV=1 js1 run --prefetch=false` - WildeBundle URL: `GET http://localhost:8081/xplat/js/RKJSModules/EntryPoints/WildeBundle.bundle?platform=ios&dev=true&app=com.facebook.Wilde` - RSS profiling via /proc, heap snapshots via Chrome DevTools Protocol - Graph freed via DELETE to the bundle URL (same as fill-http-cache) **Scripts added:** - `fb-metro-cli/memory-investigation/heap-profile.js` — Automated CDP-based profiler: captures 3 heap snapshots (baseline, post-build, post-delete) and compares them - `fb-metro-cli/memory-investigation/heap-compare.js` — Standalone snapshot comparator with streaming parser for multi-GB .heapsnapshot files - `fb-metro-cli/memory-investigation/heap-injector.js` — Optional in-process module exposing /memory, /gc, /snapshot HTTP endpoints - `metro/scripts/profile-memory.sh` — Quick RSS-only profiling via /proc - `fb-metro-cli/memory-investigation/compact-bench-measure.js` — One measurement cycle: builds WildeBundle, then requests WildeBundle.map, recording memory (RSS/heap) + build CPU + .map serialize CPU via CDP - `fb-metro-cli/memory-investigation/run-compact-bench.sh` — Orchestrator: fresh Metro per repeat across three configs (base / compact_flat / compact_indexed), cold or warm cache - `fb-metro-cli/memory-investigation/compact-bench-stats.js` — Welch t-test analysis between any two configs - `fb-metro-cli/memory-investigation/README.md`, `compact-sourcemaps-benchmark-results.md` — Full writeup of methodology and results **Baseline results (WildeBundle, June 2025):** - Startup: 819 MB RSS / 426 MB heap used - Post-build: 2,338 MB RSS / 1,549 MB heap used (+1,122 MB heap) - Post-delete: 507 MB heap used (DELETE frees 93% of build growth) - Arrays dominate: 10M Array objects + backing stores = 858 MB (77% of growth) - Source maps stored as decoded number-tuple arrays are the primary consumer: ~678 MB, 60% of build growth (9,866,476 tuples across 16,562 modules) **Compact source maps — end-to-end benchmark (n=3, WildeBundle):** Three configs: `base` (decoded tuples), `compact_flat` (VLQ storage, flat .map), `compact_indexed` (VLQ storage, indexed passthrough .map). - Memory (both compact configs): heap −51% cold / −53% warm; RSS −48% (1654→810 MB heap cold; all Welch p < 1e-5). - Build CPU: unchanged cold; ~20% faster warm with compact storage. - Serialize CPU (`.map` request): `compact_flat` +18% vs base (decode + re-encode), `compact_indexed` −49% vs base (passthrough). Flat .map is byte-identical to base; indexed .map is +3.4% larger. Bundle output byte-identical across all configs. Full tables in `compact-sourcemaps-benchmark-results.md`. Differential Revision: D107879392 --- scripts/profile-memory.sh | 223 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100755 scripts/profile-memory.sh diff --git a/scripts/profile-memory.sh b/scripts/profile-memory.sh new file mode 100755 index 0000000000..f89d7111c2 --- /dev/null +++ b/scripts/profile-memory.sh @@ -0,0 +1,223 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# Metro Memory Profiling Harness +# +# Measures RSS before, during, and after building a bundle. +# +# Usage: +# 1. Start Metro in another terminal: +# NODE_ARGS="--expose-gc" DEV=1 js1 run --prefetch=false +# +# 2. Run this script (default: WildeBundle on port 8081): +# ./profile-memory.sh +# +# Options: +# --port=PORT Metro port (default: 8081) +# --bundle=PATH Bundle path (default: WildeBundle) +# --platform=PLAT Platform (default: ios) +# --app=APP App identifier (default: com.facebook.Wilde) +# --no-delete Skip the DELETE request (keep graph in memory) +# --repeat=N Build N times to measure steady-state (default: 1) + +set -euo pipefail + +PORT=8081 +BUNDLE_PATH="xplat/js/RKJSModules/EntryPoints/WildeBundle.bundle" +PLATFORM="ios" +APP="com.facebook.Wilde" +DO_DELETE=true +REPEAT=1 + +for arg in "$@"; do + case $arg in + --port=*) PORT="${arg#*=}" ;; + --bundle=*) BUNDLE_PATH="${arg#*=}" ;; + --platform=*) PLATFORM="${arg#*=}" ;; + --app=*) APP="${arg#*=}" ;; + --no-delete) DO_DELETE=false ;; + --repeat=*) REPEAT="${arg#*=}" ;; + --help) + sed -n '2,/^$/p' "$0" + exit 0 + ;; + *) echo "Unknown option: $arg"; exit 1 ;; + esac +done + +BUNDLE_URL="http://localhost:$PORT/$BUNDLE_PATH?platform=$PLATFORM&dev=true&app=$APP" +STATUS_URL="http://localhost:$PORT/status" + +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +find_metro_pid() { + pgrep -f "[f]b-metro-cli/index.js" 2>/dev/null | head -1 || true +} + +read_rss_mb() { + awk '/^VmRSS:/ {printf "%d", $2/1024}' /proc/"$1"/status 2>/dev/null +} + +read_hwm_mb() { + awk '/^VmHWM:/ {printf "%d", $2/1024}' /proc/"$1"/status 2>/dev/null +} + +print_proc_memory() { + local pid=$1 + echo " VmRSS (current resident): $(awk '/^VmRSS:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)" + echo " VmHWM (peak resident): $(awk '/^VmHWM:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)" + echo " VmSize (virtual): $(awk '/^VmSize:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)" + echo " VmData (heap+data): $(awk '/^VmData:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)" +} + +echo -e "${BOLD}Metro Memory Profiler${NC}" +echo "" + +# Find Metro +METRO_PID=$(find_metro_pid) +if [ -z "$METRO_PID" ]; then + echo "Metro is not running. Start it first:" + echo "" + echo ' NODE_ARGS="--expose-gc" DEV=1 js1 run --prefetch=false' + echo "" + echo "For V8 heap inspection via Chrome DevTools, add --inspect:" + echo "" + echo ' NODE_ARGS="--expose-gc --inspect=9230" DEV=1 js1 run --prefetch=false' + echo " Then open chrome://inspect and connect to the Metro process." + exit 1 +fi +echo "Metro PID: $METRO_PID" + +# Wait for ready +echo -n "Waiting for Metro... " +READY=false +for _ in $(seq 1 120); do + if curl -s --connect-timeout 2 "$STATUS_URL" 2>/dev/null | grep -q "packager-status:running"; then + READY=true + echo "ready" + break + fi + sleep 1 +done +if [ "$READY" = false ]; then + echo "timed out after 120s" + exit 1 +fi + +# Baseline +echo "" +echo -e "${BOLD}Baseline (startup complete, no bundles loaded)${NC}" +BASELINE_RSS=$(read_rss_mb "$METRO_PID") +print_proc_memory "$METRO_PID" + +# Start background sampler +SAMPLE_FILE=$(mktemp /tmp/metro-mem-XXXXXX.csv) +echo "epoch_s,rss_mb" > "$SAMPLE_FILE" +( + while kill -0 "$METRO_PID" 2>/dev/null; do + rss=$(read_rss_mb "$METRO_PID") + [ -n "$rss" ] && echo "$(date +%s),$rss" >> "$SAMPLE_FILE" + sleep 1 + done +) & +SAMPLER_PID=$! +trap 'kill "$SAMPLER_PID" 2>/dev/null; wait "$SAMPLER_PID" 2>/dev/null || true' EXIT + +for iteration in $(seq 1 "$REPEAT"); do + if [ "$REPEAT" -gt 1 ]; then + echo "" + echo -e "${BOLD}=== Iteration $iteration / $REPEAT ===${NC}" + fi + + # Build + echo "" + echo -e "${BOLD}Building bundle${NC}" + echo -e "${DIM} $BUNDLE_URL${NC}" + BUILD_START=$(date +%s) + HTTP_OUT=$(curl -sS -o /dev/null -w "%{http_code}\t%{time_total}\t%{size_download}" "$BUNDLE_URL" 2>&1) + BUILD_END=$(date +%s) + + HTTP_CODE=$(echo "$HTTP_OUT" | cut -f1) + HTTP_TIME=$(echo "$HTTP_OUT" | cut -f2) + HTTP_SIZE=$(echo "$HTTP_OUT" | cut -f3) + + echo " HTTP $HTTP_CODE in ${HTTP_TIME}s, $(echo "$HTTP_SIZE" | awk '{printf "%.1f MB", $1/1048576}') (wall: $((BUILD_END - BUILD_START))s)" + + if [ "$HTTP_CODE" != "200" ]; then + echo " Bundle build failed (HTTP $HTTP_CODE). Check Metro logs." + echo " Try the URL in a browser to see the error:" + echo " $BUNDLE_URL" + kill "$SAMPLER_PID" 2>/dev/null + exit 1 + fi + + sleep 2 + echo "" + echo -e "${BOLD}Post-build${NC}" + POSTBUILD_RSS=$(read_rss_mb "$METRO_PID") + print_proc_memory "$METRO_PID" + + # Delete graph + if [ "$DO_DELETE" = true ]; then + echo "" + echo -e "${BOLD}After DELETE (graph freed)${NC}" + curl -sS -X DELETE "$BUNDLE_URL" > /dev/null 2>&1 + sleep 2 + POSTDELETE_RSS=$(read_rss_mb "$METRO_PID") + print_proc_memory "$METRO_PID" + else + POSTDELETE_RSS=$POSTBUILD_RSS + fi +done + +# Stop sampler +kill "$SAMPLER_PID" 2>/dev/null || true +wait "$SAMPLER_PID" 2>/dev/null || true +trap - EXIT + +# Peak from samples (HWM from /proc is more reliable than 1s polling) +PEAK_RSS=$(read_hwm_mb "$METRO_PID") +[ -z "$PEAK_RSS" ] && PEAK_RSS=$POSTBUILD_RSS + +# Summary +echo "" +echo -e "${BOLD}Summary${NC}" +echo "-------" +printf " %-30s %6s MB\n" "Baseline RSS:" "$BASELINE_RSS" +printf " %-30s %6s MB\n" "Peak RSS (sampled @1s):" "$PEAK_RSS" +printf " %-30s %6s MB\n" "Post-build RSS:" "$POSTBUILD_RSS" +if [ "$DO_DELETE" = true ]; then + printf " %-30s %6s MB\n" "Post-delete RSS:" "$POSTDELETE_RSS" +fi +echo "" +printf " %-30s %+6d MB\n" "Growth (build):" "$((POSTBUILD_RSS - BASELINE_RSS))" +if [ "$DO_DELETE" = true ]; then + printf " %-30s %+6d MB\n" "Retained after delete:" "$((POSTDELETE_RSS - BASELINE_RSS))" +fi +echo "" + +# Save report +REPORT="/tmp/metro-memory-$(date +%Y%m%d-%H%M%S).txt" +{ + echo "Metro Memory Profile — $(date)" + echo "Bundle: $BUNDLE_PATH ($PLATFORM, app=$APP)" + echo "PID: $METRO_PID" + echo "" + echo "Baseline RSS: ${BASELINE_RSS} MB" + echo "Peak RSS: ${PEAK_RSS} MB" + echo "Post-build RSS: ${POSTBUILD_RSS} MB" + echo "Post-delete RSS: ${POSTDELETE_RSS} MB" + echo "Build growth: $((POSTBUILD_RSS - BASELINE_RSS)) MB" + echo "Retained: $((POSTDELETE_RSS - BASELINE_RSS)) MB" + echo "" + echo "Samples (${SAMPLE_FILE}):" + cat "$SAMPLE_FILE" 2>/dev/null || echo "(no samples)" +} > "$REPORT" + +echo "Report: $REPORT" +echo "Samples: $SAMPLE_FILE" From 72c909c1424ea16a2c6f11b2d82d47f8d45fae06 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 23 Jun 2026 07:54:16 -0700 Subject: [PATCH 2/2] Derive source-map tuples from Babel's `decodedMap`, reduce cold build CPU ~2.5% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Metro's transform worker currently returns source maps from Babel's tranform result via `result.rawMappings.map(toSegmentTuple)`. This *used* to be (as the name suggests) Babel's own source map representation, and was therefore free to access. However, since https://github.com/babel/babel/pull/14497 (`babel/generator` since `v7.17.10`), `rawMappings` is now a getter providing the old structure for backwards compatibility. Accessing `result.rawMappings` forces `babel/generator` to run a second decode (`allMappings`) that allocates a flat array of ~4-5 objects per segment. The better alternative now is to use `result.decodedMap`, which is eagerly computed and free to access. To accommodate the different structure, we introduce `tuplesFromBabelDecodedMap` (decoded source lines are 0-based -> +1, name indices resolved against `decodedMap.names`). Transformer output is byte-identical to `result.rawMappings.map(toSegmentTuple)`, and is simply more efficient. ## Microbenchmark - Real `babel/generator` 7.29.1 over 133 modules / ~30.6K segments, `--expose-gc`, taking median of 11 repeats to discount GC outliers, etc. | Path | CPU (ms/pass) | Transient heap | Notes | |---|---|---|---| | New: `generate()` + `decodedMap` | 19.2 | 13.9 MB | eager, already computed — free | | Old: `generate()` + `rawMappings` | 28.8 | 19.5 MB | triggers `allMappings` decode | | **Saving** | **−9.6 ms (−33%)** | **−5.6 MB (−29%)** | per pass over 30.6K segments | ## E2E benchmark - large bundle, cold build (*AI driven benchmarks and analysis, real numbers*) - Interleaved, paired A/B: each of 12 rounds runs one cold build per cell — {baseline, this diff} x {child-process workers, worker threads}. - Fresh Metro per build, transform cache wiped (cold), `maxWorkers=16` - "Transform CPU" = total user+sys CPU across the whole worker process tree - "tree RSS" = whole-tree resident set (captures workers in both modes) - "graph heap" = main-isolate heapUsed post-build (the retained module graph). - base/this-diff columns are medians; Δ is the paired mean with a 95% CI (Student-t, 11 df) - "n.s." (not significant) = CI includes 0. Child-process workers (Metro default; 12 paired rounds): | metric | baseline | this diff | Δ (95% CI) | |---|---|---|---| | transform CPU (s) | 625 | 612 | **-16.6 (-2.6%) [-24.7, -8.5]** | | build wall (s) | 65.9 | 65.6 | -0.5 (-0.7%) n.s. | | transient tree RSS (GB) | 15.8 | 16.0 | +0.06, n.s. | | post-build tree RSS (GB) | 15.1 | 15.1 | +0.08, n.s. | | graph heap, main isolate (GB) | 1.59 | 1.59 | ~0, n.s. | Worker threads (`unstable_workerThreads`; 12 paired rounds): | metric | baseline | this diff | Δ (95% CI) | |---|---|---|---| | transform CPU (s) | 664 | 653 | -18.6 (-2.8%) [-37.5, +0.3] | | build wall (s) | 59.8 | 59.5 | -1.2 (-1.9%) n.s. | | transient RSS (GB) | 13.2 | 12.7 | -0.46 (-3.5%) [-0.81, -0.11] | | post-build RSS (GB) | 12.3 | 11.9 | -0.45 (-3.7%) [-0.80, -0.10] | | graph heap, main isolate (GB) | 1.60 | 1.60 | ~0, n.s. | Takeaways: - **Transform CPU drops ~2.6-2.8%, equally in both worker modes** — the point estimates (-16.6 s child-process, -18.6 s threads) agree to within 2 s and their CIs overlap almost entirely, so there is no real asymmetry. This is exactly what the mechanism predicts: the optimization runs *inside* the worker (consume `decodedMap` instead of forcing the `rawMappings`/`allMappings` decode), so the saving is identical whether the worker is a child process or a thread. (An earlier small-n pass suggested a child-process-only win; that was sampling noise — threads-mode CPU is just noisier, SD 30 s vs 13 s, which only widens its CI without moving the point estimate.) - Build wall time is ~1-2% lower in both modes but within noise — the CPU saving is spread across 16 workers, so it moves the critical path little. - Main-isolate post-build heap (the retained graph of stored tuples) is unchanged in every config — no memory regression, byte-identical output. Changelog: ``` - **[Performance]**: Use Babel's `decodedMap` for ~2.5% faster transforms Reviewed By: huntie, GijsWeterings Differential Revision: D108506323 --- packages/metro-source-map/src/source-map.js | 64 +++++++++++++++++ .../metro-source-map/types/source-map.d.ts | 24 ++++++- .../tuplesFromBabelDecodedMap-test.js | 68 +++++++++++++++++++ packages/metro-transform-worker/src/index.js | 8 ++- 4 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 packages/metro-transform-worker/src/__tests__/tuplesFromBabelDecodedMap-test.js diff --git a/packages/metro-source-map/src/source-map.js b/packages/metro-source-map/src/source-map.js index 3a5f282111..b0f98553e9 100644 --- a/packages/metro-source-map/src/source-map.js +++ b/packages/metro-source-map/src/source-map.js @@ -35,6 +35,24 @@ export type MetroSourceMapSegmentTuple = | SourceMapping | GeneratedCodeMapping; +// A single segment of a standard "decoded" source map (as produced by +// `@babel/generator`'s `result.decodedMap` / `@jridgewell/gen-mapping`), +// grouped by generated line. All fields are 0-based, including the source line +// (unlike Metro's `MetroSourceMapSegmentTuple`, whose source line is 1-based): +// [generatedColumn] +// [generatedColumn, sourceIndex, sourceLine, sourceColumn] +// [generatedColumn, sourceIndex, sourceLine, sourceColumn, nameIndex] +type BabelDecodedMapSegment = + | [number] + | [number, number, number, number] + | [number, number, number, number, number]; + +export type BabelDecodedMap = { + readonly mappings: ReadonlyArray>, + readonly names: ReadonlyArray, + ... +}; + export type HermesFunctionOffsets = {[number]: ReadonlyArray, ...}; export type FBSourcesArray = ReadonlyArray; @@ -279,6 +297,51 @@ function toSegmentTuple( return [line, column, original.line, original.column, name]; } +/** + * Converts a Babel/gen-mapping "decoded" source map (`result.decodedMap` from + * `@babel/generator`) into raw mapping tuples, byte-identical to + * `result.rawMappings.map(toSegmentTuple)`. + * + * Preferred over `result.rawMappings` because `decodedMap` is computed eagerly + * during generation, whereas accessing `rawMappings` triggers a second decode + * (`allMappings`) that allocates ~4-5 objects per segment. No terminating + * mapping is appended (callers that need one use `countLinesAndTerminateMap`). + */ +function tuplesFromBabelDecodedMap( + decodedMap: BabelDecodedMap, +): Array { + const {mappings, names} = decodedMap; + const tuples: Array = []; + for (let line = 0, n = mappings.length; line < n; ++line) { + // Decoded mappings are grouped by generated line (0-based); tuples use + // 1-based generated lines. + const generatedLine = line + 1; + const segments = mappings[line]; + for (let i = 0, m = segments.length; i < m; ++i) { + const segment = segments[i]; + switch (segment.length) { + case 1: + tuples.push([generatedLine, segment[0]]); + break; + case 4: + // Decoded source lines are 0-based; tuples use 1-based source lines. + tuples.push([generatedLine, segment[0], segment[2] + 1, segment[3]]); + break; + case 5: + tuples.push([ + generatedLine, + segment[0], + segment[2] + 1, + segment[3], + names[segment[4]], + ]); + break; + } + } + } + return tuples; +} + function addMappingsForFile( generator: Generator, mappings: Array, @@ -349,6 +412,7 @@ export { normalizeSourcePath, toBabelSegments, toSegmentTuple, + tuplesFromBabelDecodedMap, }; /** diff --git a/packages/metro-source-map/types/source-map.d.ts b/packages/metro-source-map/types/source-map.d.ts index cadc3b0109..e7f58e0c6c 100644 --- a/packages/metro-source-map/types/source-map.d.ts +++ b/packages/metro-source-map/types/source-map.d.ts @@ -6,7 +6,7 @@ * * @noformat * @oncall react_native - * @generated SignedSource<<7303fe7149cb12d764c6106cdf4f49ee>> + * @generated SignedSource<> * * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js * Original file: packages/metro-source-map/src/source-map.js @@ -35,6 +35,14 @@ export type MetroSourceMapSegmentTuple = | SourceMappingWithName | SourceMapping | GeneratedCodeMapping; +type BabelDecodedMapSegment = + | [number] + | [number, number, number, number] + | [number, number, number, number, number]; +export type BabelDecodedMap = { + readonly mappings: ReadonlyArray>; + readonly names: ReadonlyArray; +}; export type HermesFunctionOffsets = { [$$Key$$: number]: ReadonlyArray; }; @@ -125,6 +133,19 @@ declare function toBabelSegments( declare function toSegmentTuple( mapping: BabelSourceMapSegment, ): MetroSourceMapSegmentTuple; +/** + * Converts a Babel/gen-mapping "decoded" source map (`result.decodedMap` from + * `@babel/generator`) into raw mapping tuples, byte-identical to + * `result.rawMappings.map(toSegmentTuple)`. + * + * Preferred over `result.rawMappings` because `decodedMap` is computed eagerly + * during generation, whereas accessing `rawMappings` triggers a second decode + * (`allMappings`) that allocates ~4-5 objects per segment. No terminating + * mapping is appended (callers that need one use `countLinesAndTerminateMap`). + */ +declare function tuplesFromBabelDecodedMap( + decodedMap: BabelDecodedMap, +): Array; export { BundleBuilder, composeSourceMaps, @@ -137,6 +158,7 @@ export { normalizeSourcePath, toBabelSegments, toSegmentTuple, + tuplesFromBabelDecodedMap, }; /** * Backwards-compatibility with CommonJS consumers using interopRequireDefault. diff --git a/packages/metro-transform-worker/src/__tests__/tuplesFromBabelDecodedMap-test.js b/packages/metro-transform-worker/src/__tests__/tuplesFromBabelDecodedMap-test.js new file mode 100644 index 0000000000..eae81bf8a2 --- /dev/null +++ b/packages/metro-transform-worker/src/__tests__/tuplesFromBabelDecodedMap-test.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +'use strict'; + +import generate from '@babel/generator'; +import * as babylon from '@babel/parser'; +import {toSegmentTuple, tuplesFromBabelDecodedMap} from 'metro-source-map'; + +// The transform worker derives source-map tuples from Babel's eagerly-computed +// `result.decodedMap` instead of triggering the more expensive `rawMappings` +// (`allMappings`) decode. This must be byte-identical to the previous +// `result.rawMappings.map(toSegmentTuple)`. +const SAMPLES = [ + `function foo(aaa, bbb) { + const ccc = aaa + bbb; + return ccc * 2; +} +class Bar extends Foo { + method(xxx) { + return this.value + xxx; + } +} +export default function entry(items) { + const obj = {a: 1, b: 2, c: [1, 2, 3]}; + return items.map(x => x.value).filter(Boolean); +} +`, + `const x = require('foo');\nmodule.exports = (a, b) => { let s = 0; for (let i = 0; i < a.length; i++) { s += a[i] * b; } return s; };\n`, + `// header\nconst y = 1;\n\n\nfunction z() { return y; }\n`, + `const w = 42; const v = w + 1; export {w, v};`, + `1 + 1;\n`, +]; + +describe('tuplesFromBabelDecodedMap', () => { + test.each(SAMPLES.map((code, i) => [i, code]))( + 'is byte-identical to rawMappings.map(toSegmentTuple) [sample %i]', + (_i, code) => { + const ast = babylon.parse(code, {sourceType: 'unambiguous'}); + const result = generate( + ast, + {sourceMaps: true, sourceFileName: 'file.js'}, + code, + ); + const fromRaw = (result.rawMappings ?? []).map(toSegmentTuple); + const fromDecoded = tuplesFromBabelDecodedMap( + nullthrowsLocal(result.decodedMap), + ); + expect(fromDecoded).toEqual(fromRaw); + expect(fromDecoded.length).toBeGreaterThan(0); + }, + ); +}); + +function nullthrowsLocal(x: ?T): T { + if (x == null) { + throw new Error('Expected decodedMap to be present'); + } + return x; +} diff --git a/packages/metro-transform-worker/src/index.js b/packages/metro-transform-worker/src/index.js index e6fb462232..ea93e87de4 100644 --- a/packages/metro-transform-worker/src/index.js +++ b/packages/metro-transform-worker/src/index.js @@ -46,6 +46,7 @@ import { functionMapBabelPlugin, toBabelSegments, toSegmentTuple, + tuplesFromBabelDecodedMap, } from 'metro-source-map'; import metroTransformPlugins from 'metro-transform-plugins'; import collectDependencies from 'metro/private/ModuleGraph/worker/collectDependencies'; @@ -471,7 +472,12 @@ async function transformJS( file.code, ); - let map = result.rawMappings ? result.rawMappings.map(toSegmentTuple) : []; + // Derive tuples from Babel's eagerly-computed decoded map rather than + // `result.rawMappings`, which would trigger a second, more expensive decode + // (`allMappings`). Byte-identical to `result.rawMappings.map(toSegmentTuple)`. + let map = result.decodedMap + ? tuplesFromBabelDecodedMap(result.decodedMap) + : []; let code = result.code; if (minify) {