Hive Hive
Sign in

fix(cli): avoid per-target swift --version subprocess storm when mapping large SwiftPM graphs

GitHub issue · Closed

Metadata
Source
tuist/tuist #11205
Updated
Jun 24, 2026
Domains
Generated projects
Details

Reported internally: tuist generate hangs for a very long time on a project that depends on the AWS SDK for Swift (~800 targets in one SwiftPM package). The last visible output is the per-target Target … of type test ignored lines, then a long silent stall. It was fast on 4.152.0 and slow on 4.192.0+.

What changed

Two fixes in PackageInfoMapper’s mapping path, both of which scale with the number of targets in an external SwiftPM package:

  1. SwiftVersionProvider — dedupe concurrent swiftVersion() calls (the dominant fix). map(target:) runs over every package target via concurrentCompactMap, and each regular/test target calls SwiftVersionProvider.current.swiftVersion(). That value is cached by the AsyncThrowableCaching actor, but its value() checked cachedValue and then awaited builder() — the cache is only populated after the await resolves, so all concurrent first-callers pass the nil check before any of them caches. With ~800 concurrently-mapped targets this fans out into ~800 concurrent xcrun swift --version launches for one invariant value. The actor now coalesces callers onto a single Task stored synchronously before the first suspension point, so only one subprocess runs.

  2. PackageInfoMapper — reuse the package targets dictionary in name resolution (secondary). wrapsProductNamedFramework (called from effectiveModuleName/effectiveProductName, 2-3× per target) rebuilt a Dictionary over all package targets on every call — O(T²) per package, copying the heavy PackageInfo.Target value type repeatedly. The dictionary that map(packageInfo:) already builds is now threaded through; the traversal logic and resolved names are unchanged.

Why / root cause

The regression came from swiftVersion() being converted from synchronous (which deduplicated naturally) to async behind an actor cache that doesn’t coalesce in-flight requests. The signature is a huge sys time from process spawning (forking ~800 swift --version). The O(T²) dictionary rebuild was a real-but-minor second contributor that PR #10309 amplified by removing the early-return guards in the common target == product case.

The “obvious” suspect (the O(T²) mapper) turned out to be the minor one — profiling the reproducer is what surfaced the subprocess storm as the dominant cost.

How to test locally

Reproduced and validated with a synthetic local SwiftPM package of 800 single-target/single-product modules (each module’s target name equals its product name, all depending on a shared Runtime), with the app depending on a single external so that mapping processes all 800 targets while generation stays small. Measured with /usr/bin/time -l tuist generate --no-open and sample:

wall user CPU sys
before (4.198.1) 14.6s 70.9s 31.1s
after 2.6s 1.75s 1.1s

~40× less user CPU and ~29× less sys time, back to the 4.152.0 baseline, with all 800 modules still mapped and the project generated identically. A profiler confirms the wrapsProductNamedFramework, SwiftVersionProvider.swiftVersion, and CommandRunner.run frames are gone after the change.

Notes for reviewers

  • A regression test asserting SwiftVersionProvider invokes its builder only once under N concurrent swiftVersion() calls would be a good guard; happy to add it.
  • A smaller secondary cost remains (Regex-based FileSystem.glob/expandBraces runs per target, from the FileHandler→FileSystem migration). It lives in the external tuist/FileSystem package and is now a minor relative contributor — worth a separate look, out of scope here.
Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

Comments
T
tuist[bot] Jun 10, 2026

🛠️ Tuist Run Report 🛠️

Tests 🧪
Scheme Status Cache hit rate Tests Skipped Ran Commit
TuistAcceptanceTests 0 % 0 0 0 56434b8a8
TuistUnitTests 80 % 2949 4 2945 56434b8a8
Flaky Tests ⚠️
  • TuistUnitTests: 3 flaky tests (View all)
Test case Module Suite
parseTestStatuses_returnsPassingModuleNames() TuistXCResultServiceTests XCResultServiceTests
parseTestStatuses_returnsCorrectStatuses() TuistXCResultServiceTests XCResultServiceTests
parseTestStatuses_extractsModuleAndSuiteNames() TuistXCResultServiceTests XCResultServiceTests
Builds 🔨
Scheme Status Duration Commit
TuistAcceptanceTests 2m 0.5s 56434b8a8
TuistUnitTests 3m 32s 56434b8a8