Skip to content

Commit 47c8db8

Browse files
robhoganfacebook-github-bot
authored andcommitted
Memory profiling harness and baseline investigation
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
1 parent 16fa9ce commit 47c8db8

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

scripts/profile-memory.sh

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/bin/bash
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# Metro Memory Profiling Harness
8+
#
9+
# Measures RSS before, during, and after building a bundle.
10+
#
11+
# Usage:
12+
# 1. Start Metro in another terminal:
13+
# NODE_ARGS="--expose-gc" DEV=1 js1 run --prefetch=false
14+
#
15+
# 2. Run this script (default: WildeBundle on port 8081):
16+
# ./profile-memory.sh
17+
#
18+
# Options:
19+
# --port=PORT Metro port (default: 8081)
20+
# --bundle=PATH Bundle path (default: WildeBundle)
21+
# --platform=PLAT Platform (default: ios)
22+
# --app=APP App identifier (default: com.facebook.Wilde)
23+
# --no-delete Skip the DELETE request (keep graph in memory)
24+
# --repeat=N Build N times to measure steady-state (default: 1)
25+
26+
set -euo pipefail
27+
28+
PORT=8081
29+
BUNDLE_PATH="xplat/js/RKJSModules/EntryPoints/WildeBundle.bundle"
30+
PLATFORM="ios"
31+
APP="com.facebook.Wilde"
32+
DO_DELETE=true
33+
REPEAT=1
34+
35+
for arg in "$@"; do
36+
case $arg in
37+
--port=*) PORT="${arg#*=}" ;;
38+
--bundle=*) BUNDLE_PATH="${arg#*=}" ;;
39+
--platform=*) PLATFORM="${arg#*=}" ;;
40+
--app=*) APP="${arg#*=}" ;;
41+
--no-delete) DO_DELETE=false ;;
42+
--repeat=*) REPEAT="${arg#*=}" ;;
43+
--help)
44+
sed -n '2,/^$/p' "$0"
45+
exit 0
46+
;;
47+
*) echo "Unknown option: $arg"; exit 1 ;;
48+
esac
49+
done
50+
51+
BUNDLE_URL="http://localhost:$PORT/$BUNDLE_PATH?platform=$PLATFORM&dev=true&app=$APP"
52+
STATUS_URL="http://localhost:$PORT/status"
53+
54+
BOLD='\033[1m'
55+
DIM='\033[2m'
56+
NC='\033[0m'
57+
58+
find_metro_pid() {
59+
pgrep -f "[f]b-metro-cli/index.js" 2>/dev/null | head -1 || true
60+
}
61+
62+
read_rss_mb() {
63+
awk '/^VmRSS:/ {printf "%d", $2/1024}' /proc/"$1"/status 2>/dev/null
64+
}
65+
66+
read_hwm_mb() {
67+
awk '/^VmHWM:/ {printf "%d", $2/1024}' /proc/"$1"/status 2>/dev/null
68+
}
69+
70+
print_proc_memory() {
71+
local pid=$1
72+
echo " VmRSS (current resident): $(awk '/^VmRSS:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)"
73+
echo " VmHWM (peak resident): $(awk '/^VmHWM:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)"
74+
echo " VmSize (virtual): $(awk '/^VmSize:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)"
75+
echo " VmData (heap+data): $(awk '/^VmData:/ {printf "%d MB", $2/1024}' /proc/"$pid"/status)"
76+
}
77+
78+
echo -e "${BOLD}Metro Memory Profiler${NC}"
79+
echo ""
80+
81+
# Find Metro
82+
METRO_PID=$(find_metro_pid)
83+
if [ -z "$METRO_PID" ]; then
84+
echo "Metro is not running. Start it first:"
85+
echo ""
86+
echo ' NODE_ARGS="--expose-gc" DEV=1 js1 run --prefetch=false'
87+
echo ""
88+
echo "For V8 heap inspection via Chrome DevTools, add --inspect:"
89+
echo ""
90+
echo ' NODE_ARGS="--expose-gc --inspect=9230" DEV=1 js1 run --prefetch=false'
91+
echo " Then open chrome://inspect and connect to the Metro process."
92+
exit 1
93+
fi
94+
echo "Metro PID: $METRO_PID"
95+
96+
# Wait for ready
97+
echo -n "Waiting for Metro... "
98+
READY=false
99+
for _ in $(seq 1 120); do
100+
if curl -s --connect-timeout 2 "$STATUS_URL" 2>/dev/null | grep -q "packager-status:running"; then
101+
READY=true
102+
echo "ready"
103+
break
104+
fi
105+
sleep 1
106+
done
107+
if [ "$READY" = false ]; then
108+
echo "timed out after 120s"
109+
exit 1
110+
fi
111+
112+
# Baseline
113+
echo ""
114+
echo -e "${BOLD}Baseline (startup complete, no bundles loaded)${NC}"
115+
BASELINE_RSS=$(read_rss_mb "$METRO_PID")
116+
print_proc_memory "$METRO_PID"
117+
118+
# Start background sampler
119+
SAMPLE_FILE=$(mktemp /tmp/metro-mem-XXXXXX.csv)
120+
echo "epoch_s,rss_mb" > "$SAMPLE_FILE"
121+
(
122+
while kill -0 "$METRO_PID" 2>/dev/null; do
123+
rss=$(read_rss_mb "$METRO_PID")
124+
[ -n "$rss" ] && echo "$(date +%s),$rss" >> "$SAMPLE_FILE"
125+
sleep 1
126+
done
127+
) &
128+
SAMPLER_PID=$!
129+
trap 'kill "$SAMPLER_PID" 2>/dev/null; wait "$SAMPLER_PID" 2>/dev/null || true' EXIT
130+
131+
for iteration in $(seq 1 "$REPEAT"); do
132+
if [ "$REPEAT" -gt 1 ]; then
133+
echo ""
134+
echo -e "${BOLD}=== Iteration $iteration / $REPEAT ===${NC}"
135+
fi
136+
137+
# Build
138+
echo ""
139+
echo -e "${BOLD}Building bundle${NC}"
140+
echo -e "${DIM} $BUNDLE_URL${NC}"
141+
BUILD_START=$(date +%s)
142+
HTTP_OUT=$(curl -sS -o /dev/null -w "%{http_code}\t%{time_total}\t%{size_download}" "$BUNDLE_URL" 2>&1)
143+
BUILD_END=$(date +%s)
144+
145+
HTTP_CODE=$(echo "$HTTP_OUT" | cut -f1)
146+
HTTP_TIME=$(echo "$HTTP_OUT" | cut -f2)
147+
HTTP_SIZE=$(echo "$HTTP_OUT" | cut -f3)
148+
149+
echo " HTTP $HTTP_CODE in ${HTTP_TIME}s, $(echo "$HTTP_SIZE" | awk '{printf "%.1f MB", $1/1048576}') (wall: $((BUILD_END - BUILD_START))s)"
150+
151+
if [ "$HTTP_CODE" != "200" ]; then
152+
echo " Bundle build failed (HTTP $HTTP_CODE). Check Metro logs."
153+
echo " Try the URL in a browser to see the error:"
154+
echo " $BUNDLE_URL"
155+
kill "$SAMPLER_PID" 2>/dev/null
156+
exit 1
157+
fi
158+
159+
sleep 2
160+
echo ""
161+
echo -e "${BOLD}Post-build${NC}"
162+
POSTBUILD_RSS=$(read_rss_mb "$METRO_PID")
163+
print_proc_memory "$METRO_PID"
164+
165+
# Delete graph
166+
if [ "$DO_DELETE" = true ]; then
167+
echo ""
168+
echo -e "${BOLD}After DELETE (graph freed)${NC}"
169+
curl -sS -X DELETE "$BUNDLE_URL" > /dev/null 2>&1
170+
sleep 2
171+
POSTDELETE_RSS=$(read_rss_mb "$METRO_PID")
172+
print_proc_memory "$METRO_PID"
173+
else
174+
POSTDELETE_RSS=$POSTBUILD_RSS
175+
fi
176+
done
177+
178+
# Stop sampler
179+
kill "$SAMPLER_PID" 2>/dev/null || true
180+
wait "$SAMPLER_PID" 2>/dev/null || true
181+
trap - EXIT
182+
183+
# Peak from samples (HWM from /proc is more reliable than 1s polling)
184+
PEAK_RSS=$(read_hwm_mb "$METRO_PID")
185+
[ -z "$PEAK_RSS" ] && PEAK_RSS=$POSTBUILD_RSS
186+
187+
# Summary
188+
echo ""
189+
echo -e "${BOLD}Summary${NC}"
190+
echo "-------"
191+
printf " %-30s %6s MB\n" "Baseline RSS:" "$BASELINE_RSS"
192+
printf " %-30s %6s MB\n" "Peak RSS (sampled @1s):" "$PEAK_RSS"
193+
printf " %-30s %6s MB\n" "Post-build RSS:" "$POSTBUILD_RSS"
194+
if [ "$DO_DELETE" = true ]; then
195+
printf " %-30s %6s MB\n" "Post-delete RSS:" "$POSTDELETE_RSS"
196+
fi
197+
echo ""
198+
printf " %-30s %+6d MB\n" "Growth (build):" "$((POSTBUILD_RSS - BASELINE_RSS))"
199+
if [ "$DO_DELETE" = true ]; then
200+
printf " %-30s %+6d MB\n" "Retained after delete:" "$((POSTDELETE_RSS - BASELINE_RSS))"
201+
fi
202+
echo ""
203+
204+
# Save report
205+
REPORT="/tmp/metro-memory-$(date +%Y%m%d-%H%M%S).txt"
206+
{
207+
echo "Metro Memory Profile — $(date)"
208+
echo "Bundle: $BUNDLE_PATH ($PLATFORM, app=$APP)"
209+
echo "PID: $METRO_PID"
210+
echo ""
211+
echo "Baseline RSS: ${BASELINE_RSS} MB"
212+
echo "Peak RSS: ${PEAK_RSS} MB"
213+
echo "Post-build RSS: ${POSTBUILD_RSS} MB"
214+
echo "Post-delete RSS: ${POSTDELETE_RSS} MB"
215+
echo "Build growth: $((POSTBUILD_RSS - BASELINE_RSS)) MB"
216+
echo "Retained: $((POSTDELETE_RSS - BASELINE_RSS)) MB"
217+
echo ""
218+
echo "Samples (${SAMPLE_FILE}):"
219+
cat "$SAMPLE_FILE" 2>/dev/null || echo "(no samples)"
220+
} > "$REPORT"
221+
222+
echo "Report: $REPORT"
223+
echo "Samples: $SAMPLE_FILE"

0 commit comments

Comments
 (0)