Skip to content

build: size lerna prepare and gradle workers to the CPUs and memory the build can use - #9446

Merged
mikehardy merged 4 commits into
mainfrom
build-concurrency-cap
Nov 8, 2026
Merged

mikehardy merged 4 commits into
mainfrom
build-concurrency-cap

Conversation

@mikehardy

@mikehardy mikehardy commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Stack position: bottom (1 of 4). Base: main. Merge this first. Stack order: this PR → #9447 (tooling and test fixes) → #9444 (recorded-only e2e release) → #9448 (shared-host e2e hardening).

Description

Two build entry points sized themselves from every core on the machine, even when the build was limited to fewer CPUs (Docker --cpuset-cpus, cgroup cpusets, CPU affinity) or had little memory per CPU:

  • yarn lerna:prepare used lerna's default of one prepare task per CPU. It now passes --concurrency from scripts/lerna-prepare-concurrency.js, which only prints a number:
    • RNFB_LERNA_CONCURRENCY when it is a positive integer.
    • Otherwise the smaller of os.availableParallelism() (honours CPU affinity) and one task per 2.5 GiB of memory (the cgroup limit when it is lower than total RAM, else total RAM), and at least 1.
  • tests/android/gradlew-with-worker-cap.sh derived --max-workers from lscpu / /proc/cpuinfo, which list every host core even under a cpuset. It now also reads the online CPU count (nproc with OMP_* limits unset, then getconf _NPROCESSORS_ONLN, then sysctl hw.logicalcpu on macOS) and uses the smaller of physical and online. The log line now prints both values.

Why the memory cap. On a plain Linux machine or container with 16 CPUs and 8 GB, a cold yarn install --immutable exited 1 with no error text. Each prepare task runs bob build (tsc and babel) and can hold well over 1 GB, and during a cold install the Yarn process itself holds about 4 GB. With 16 tasks the kernel OOM killer killed Yarn partway through lerna:prepare. Yarn's yarnPath launcher reports a child killed by a signal as a bare exit 1, so nothing was printed. GitHub runners (4 CPUs, 16 GB, Nx cache restored) never hit this.

Hosts with at least 2.5 GiB per CPU (GitHub Linux runners, Macs with 64 GiB or more) keep one task per CPU. Hosts with less run fewer prepare tasks when the Nx cache misses. Examples: a 16-CPU / 32 GiB Mac (12 tasks), a 10-CPU / 16 GiB Mac (6 tasks), a 3-CPU / 7 GiB macOS runner (2 tasks). The gradle worker cap is unchanged on an unrestricted host.

okf-bundle/monorepo-tooling/prepare-and-cache.md documents how lerna:prepare picks its concurrency. New Jest tests: gradlewWorkerCap.test.js and lernaPrepareConcurrency.test.js.

Related issues

None. Related PRs in this stack:

Release Summary

None (build tooling only).

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
    • Other (macOS, web)
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

Build scripts only; no package source changes.

First commit, full ts-js-validation sequence from the repo root, each exit 0:

Command Exit Notes
yarn lerna:prepare 0 runs with the new --concurrency value
yarn tsc:compile 0
yarn tsc:compile:consumer 0
yarn reference:api 0
yarn lint 0
yarn tests:jest 0 117 suites, 1878 tests
yarn format:js 0 no diff
yarn compare:types 0

Second commit (gradle cap test and docs): yarn lint exit 0. gradlewWorkerCap.test.js passes 4/4 on macOS and in a Linux node:24 container. It stubs uname, lscpu, nproc and getconf and checks that physical cores win on an unrestricted host, that a cpuset's lower online count wins, that OMP_NUM_THREADS is ignored, and that GRADLE_WORKER_CAP (default 6) caps the result.

Third commit (memory cap):

  • yarn lint exit 0. lernaPrepareConcurrency.test.js (6 tests) and gradlewWorkerCap.test.js (4 tests) pass.
  • Cold yarn install --immutable in an ubuntu:26.04 + Node 24 container with 16 CPUs and 7.9 GB, from a clean git archive:
    • before (main): exit 1, no error text, cgroup oom_kill 1.
    • after: exit 0, concurrency 3, oom_kill 0, Done with warnings in 1m 16s.
  • A cold install failed at 4 GB even at concurrency 1, because Yarn alone peaks near 4 GB. 8 GB works. That is an environment floor, not something this PR changes.

Fourth commit (review follow-ups):

  • The CPU, memory and floor test cases pass override: '', so an exported RNFB_LERNA_CONCURRENCY no longer affects them.
  • memoryBytes() is exported, with four new cases: no limit, an unlimited cgroup value, a limit above total RAM, and a limit below it.
  • yarn lint exit 0. Both Jest files pass 14/14, and the lerna file also passes 10/10 with RNFB_LERNA_CONCURRENCY=5 set.
  • Revert proof: making memoryBytes() always return total RAM fails the below-total case (1 failed, 9 passed).

Think react-native-firebase is great? Please consider supporting the project with any of the below:

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.10%. Comparing base (2927a47) to head (1bad8cb).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9446      +/-   ##
============================================
+ Coverage     71.37%   75.10%   +3.74%     
- Complexity        0     2298    +2298     
============================================
  Files           347      621     +274     
  Lines         23077    41253   +18176     
  Branches       2808     5242    +2434     
============================================
+ Hits          16470    30981   +14511     
- Misses         5989     8694    +2705     
- Partials        618     1578     +960     
Flag Coverage Δ
android-native 70.69% <ø> (?)
e2e-ts-android 53.57% <ø> (?)
e2e-ts-ios 52.99% <ø> (ø)
e2e-ts-macos 49.32% <ø> (?)
ios-native 79.10% <ø> (ø)
ios-ruby 100.00% <ø> (ø)
jest 51.87% <80.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…URRENCY

A Jest test runs gradlew-with-worker-cap.sh against stub uname, lscpu, nproc and getconf. It checks that physical cores win on an unrestricted host, that a cpuset's lower online count wins, that OMP_NUM_THREADS is ignored, and that GRADLE_WORKER_CAP caps the result. prepare-and-cache.md now says where lerna:prepare takes its concurrency from.
Lerna runs one prepare task per CPU by default. Each task runs bob build (tsc and babel) and can hold well over 1 GiB, and during a cold yarn install the Yarn process itself holds several GiB. On a 16-CPU / 8 GiB Linux machine or container the kernel OOM killer then kills Yarn mid-prepare. Yarn's yarnPath launcher reports a signal-killed child as a bare exit 1, so yarn install --immutable failed with no error text.

scripts/lerna-prepare-concurrency.js replaces the inline node -p in lerna:prepare. It keeps RNFB_LERNA_CONCURRENCY and os.availableParallelism(), and adds a cap of one task per 2.5 GiB of memory (the cgroup limit when one is set), with a floor of 1. Machines with plenty of memory per CPU keep one task per CPU.
…roup memory choice

The CPU, memory and floor cases now pass override: '' so an RNFB_LERNA_CONCURRENCY exported in the developer's shell no longer changes their result. memoryBytes() is exported with injectable total and constrained values, and four cases cover no limit, an unlimited cgroup value, a limit above total RAM, and a limit below it. prepare-and-cache.md notes where the 2.5 GiB per task comes from.
@mikehardy mikehardy changed the title build: respect cpuset limits in lerna prepare and gradle worker cap build: size lerna prepare and gradle workers to the CPUs and memory the build can use Nov 8, 2026
@mikehardy
mikehardy marked this pull request as ready for review November 8, 2026 18:58
@mikehardy

Copy link
Copy Markdown
Collaborator Author

Quite a few changes stacked up - all infra + test - need to get them through, merging this

@mikehardy
mikehardy merged commit ca57548 into main Nov 8, 2026
26 checks passed
@mikehardy
mikehardy deleted the build-concurrency-cap branch November 8, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant