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.
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