Skip to content

Cut Metro's memory and CPU footprint during harness runs - #173

Open
V3RON wants to merge 2 commits into
mainfrom
tune-metro-resource-usage
Open

Cut Metro's memory and CPU footprint during harness runs#173
V3RON wants to merge 2 commits into
mainfrom
tune-metro-resource-usage

Conversation

@V3RON

@V3RON V3RON commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What is this?

Metro is the largest memory consumer in a Harness run, and it was sized for the wrong job. Its transform pool is dimensioned for bundling a whole app, but a Harness run transforms far fewer modules and reuses a persistent transform cache between runs — so on a 12-core host it spawned 8 child processes, each carrying its own Node runtime and Babel instance, and peaked at ~3.5 GB of RSS for a cold bundle. This tunes the pool to the work actually being done, bringing that peak to ~1.8 GB with no increase in build time.

It also fixes an unrelated problem in the same config: resolver.blockList was being cleared outright, silently discarding whatever the project had configured in its own metro.config.js.

How does it work?

getCappedMaxWorkers now starts from Metro's resolved default and takes the lower of two reductions: the existing device-core reservation, and halving. It still only ever lowers the value, so an explicit lower maxWorkers is respected. The halving is what makes the cap bite on large hosts — previously, at 12 cores the reservation computed 8 and Metro's own default was also 8, so it never engaged at all; at 16 cores the cap was 12 against a default of 10. On a 3-vCPU runner the reservation alone lands on a single worker, which takes Metro's in-band transform path and spawns no child process, so nothing competes with the simulator.

The pool also runs as worker threads rather than child processes (transformer.unstable_workerThreads). Threads share one Node runtime and one Babel instance instead of duplicating both per worker. RN_HARNESS_DEBUG_WORKER_THREADS=0 restores child processes, following the existing RN_HARNESS_DEBUG_USE_WATCHMAN convention.

blockList was cleared because Metro's stock value reduces to __tests__, which would hide every Harness test. Rather than dropping the project's patterns to achieve that, Harness now inherits them and carves the protected paths back out, expressed as one regex because metro-file-map rebuilds whatever it is given via new RegExp(source, flags). This matters for the common exclusionList([...]) case, where the project's own exclusions and the __tests__ rule are fused into a single alternation and cannot be separated by inspecting the source. Combining also lets a flag conflict drop one pattern instead of throwing, which is what Metro's own array handling does.

On its own behalf Harness excludes exactly one directory: .harness/cache. The scoping is load-bearing — excluding all of .harness fails every build with Failed to get the SHA-1 for: .harness/manifest.js, because the manifest is served as a polyfill and must stay in the file map. Nothing else is added; excluding build output or tool caches is the project's call. Accordingly the playground declares its own .nx exclusion, since withNxMetro puts the monorepo root in watchFolders and that cache is an artifact of this repo rather than of Harness.

Two smaller trims: Metro's stdin hotkey is off, and the file watcher now starts only when Jest is actually in watch mode (--watch/--watchAll), which previously ran on every local one-shot run. The file-map cache still persists either way, since it is written on changedSinceCacheRead independently of the watch-only auto-save.

Worth flagging for review: worker threads share a V8 heap with the Metro server, so a leaky Babel plugin now leaks into the long-lived process rather than an isolable child, and a native crash is not contained. Unit and integration coverage is green, but the device e2e suite has not been run against this branch — that is the check worth having before relying on threads as the default, hence the escape hatch.

Why is this useful?

Halving Metro's peak memory makes Harness runs materially cheaper to host, and it matters most where headroom is smallest: constrained CI runners were exactly the case the previous cap failed to cover, and a 3-vCPU macOS runner now leaves its cores to the simulator instead of to eight Babel processes. Projects also get the Metro configuration they actually wrote — a blockList narrowing a large monorepo's crawl is now honoured instead of being silently thrown away.

V3RON added 2 commits July 28, 2026 22:47
Metro's transform pool is sized for bundling a whole app, but a harness run
transforms far fewer modules and reuses a persistent transform cache. Run the
pool as worker threads instead of child processes and halve Metro's own default
worker count, which cuts peak RSS for a cold bundle from ~3.5 GB to ~1.8 GB on
a 12-core host at no cost to build time. On a 3-vCPU runner the device-core
reservation now lands on a single worker, taking Metro's in-band path so
nothing competes with the simulator.

Stop discarding `resolver.blockList`. It was cleared outright because Metro's
stock value reduces to `__tests__`, which would hide every harness test, but
that also silently dropped whatever the project had configured. Inherit the
project's patterns and carve the protected paths out of them instead, which
works even for `exclusionList([...])`, where the project's own exclusions and
the `__tests__` rule are fused into a single regex.

Harness adds only `.harness/cache` on its own behalf -- scoped to the cache
rather than all of `.harness`, since `.harness/manifest.js` is served as a
polyfill and must stay in the file map. The playground declares its own `.nx`
exclusion, as befits an artifact of this repo rather than of Harness.

Also drop Metro's stdin hotkey, and only start the file watcher when Jest is
actually in watch mode.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-native-harness Ready Ready Preview, Comment Jul 28, 2026 8:50pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant