Skip to content

perf(platform-ios): overlap XCTest agent build with simulator boot - #171

Closed
V3RON wants to merge 2 commits into
mainfrom
perf/ios-parallel-xctest-build
Closed

perf(platform-ios): overlap XCTest agent build with simulator boot#171
V3RON wants to merge 2 commits into
mainfrom
perf/ios-parallel-xctest-build

Conversation

@V3RON

@V3RON V3RON commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What is this?

On iOS simulator runs with permissions enabled, Harness built the XCTest permission agent strictly after the simulator finished booting, even though the build phase never touches the simulator. This serialized two independent, often lengthy operations for no reason—most noticeably on a cold xcodebuild cache (e.g. right after an Xcode version bump).

How does it work?

In getAppleSimulatorPlatformInstance, the XCTest agent controller is now constructed as soon as the simulator's udid is known (before boot), and prepare() (the build-only phase, xcodebuild build-for-testing against generic/platform=iOS Simulator) is kicked off immediately without being awaited. The simulator boot/waitForBoot/install/harness-JS-override sequence then runs as before, unaffected by the build. Once that sequence finishes, the outstanding preparePromise is awaited first (so a build failure still surfaces and fails startup), followed by ensureStarted(), which internally re-calls prepare()—a no-op since it's idempotent—before launching the agent process against the now-booted device.

This is safe because:

  • prepare() only depends on the udid, which is already known before boot, and never touches the booted device (the run phase, test-without-building against id=<udid>, is untouched and still runs after boot).
  • prepare() is idempotent (if (prepared) return), so calling it early and letting ensureStarted() call it again is a no-op.
  • The build promise is given a .catch at creation to avoid an unhandled-rejection warning if boot fails first, while still being awaited inside the try block so the real error is surfaced and the existing finally cleanup (dispose agent, clear the harness JS override, shut down the simulator if Harness booted it) still runs correctly on any failure path.
  • When permissions is disabled, xctestAgent stays null and preparePromise is undefined, so behavior is byte-for-byte unchanged.

getApplePhysicalDevicePlatformInstance is untouched—physical device runs don't boot a simulator, so there's nothing to overlap.

Why is this useful?

The two operations are independent, so running them concurrently removes dead time from Harness startup on simulator runs with permission auto-acceptance enabled. The win is largest on cold build caches, such as right after an Xcode version bump, where the XCTest agent build can take a while and previously ran back-to-back with the simulator boot instead of alongside it.

The XCTest agent build (xcodebuild build-for-testing, destination
generic/platform=iOS Simulator) does not need a booted device, only the
later launch phase does. Kick off the build immediately after resolving
the simulator udid so it overlaps with bootSimulator/waitForBoot instead
of running strictly after it, cutting startup time on simulator runs
with permissions enabled, especially on a cold build cache.

prepare() is idempotent and ensureStarted() already calls it internally,
so calling it early and awaiting it again before ensureStarted() is safe.
The permissions-disabled path is unchanged (xctestAgent stays null).
@vercel

vercel Bot commented Jul 22, 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 22, 2026 12:12pm

Request Review

@V3RON

V3RON commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #174, which keeps the parallel startup optimization on capable hosts while using build-first sequential startup on resource-constrained machines.

@V3RON V3RON closed this Jul 30, 2026
V3RON added a commit that referenced this pull request Jul 30, 2026
## What is this?

Harness now chooses how to prepare the iOS XCTest permission agent
according to host memory and CPU capacity. Constrained machines build
the agent before starting simulator preparation, while capable machines
overlap the two operations. This replaces the unconditional overlap
proposed in #171 and adds a reusable host-capabilities helper for future
resource-aware behavior.

The PR also makes the Metro block list consume the canonical cache root
produced by `createHarnessCache` instead of duplicating the cache
directory layout.

## How does it work?

`getHostCapabilities()` reports total memory and available CPU
parallelism from Node. An internal, argument-free startup policy enables
overlap only when the host has strictly more than 8 GiB of memory and
more than 6 available CPUs. The selected sequential or parallel strategy
is logged at debug level.

Sequential startup awaits the XCTest build before checking, booting, and
preparing the simulator. Parallel startup begins the build immediately,
prepares the simulator concurrently, then waits for both before starting
the agent. Both parallel branches are settled before cleanup so build
and simulator failures propagate without leaving the controller,
simulator override, or Harness-started simulator behind.

Metro receives `harnessCache.paths.root` when constructing its block
list and derives a cross-platform, exact-root exclusion from that value.
Cache-path construction therefore remains owned by
`@react-native-harness/cache`, and the cache boundary check stays strict
without an allowlist.

## Why is this useful?

Resource-constrained macOS runners avoid making `xcodebuild` and the
simulator compete for limited RAM and CPU, preserving a build-first
startup path when contention would likely make startup slower. More
powerful hosts retain the cold-start speedup from overlapping
independent work. Reusing the canonical cache root also prevents Metro
and the cache package from drifting onto different directory layouts.
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