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) { 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"