Skip to content

fix: classify Swift compilation steps from Xcode 14 and later - #256

Open
AgapovOne wants to merge 2 commits into
MobileNativeFoundation:masterfrom
AgapovOne:fix-swift-step-classification
Open

fix: classify Swift compilation steps from Xcode 14 and later#256
AgapovOne wants to merge 2 commits into
MobileNativeFoundation:masterfrom
AgapovOne:fix-swift-step-classification

Conversation

@AgapovOne

@AgapovOne AgapovOne commented Aug 12, 2026

Copy link
Copy Markdown

What is broken

getDetailType matches CompileSwift and CompileSwiftSources , but the Swift driver signs the same work SwiftCompile and SwiftDriver . Those steps come out as .other, so a reporter that groups by step type loses the Swift compilation — nearly the whole build.

Both spellings appear in one log: in a 457-target build, every target emits a SwiftDriver planning job and 423 of them are also signed CompileSwift. So this PR adds the driver's names, it does not replace the existing ones.

The fix

Two prefixes in getDetailType, plus the same two spellings in the guard of getSwiftIndividualSteps — that guard is keyed to CompileSwift too, and fixing only the classification duplicates every file (see below).

Two logs of the same app, Xcode 26.5, upstream 0.2.49 vs this branch:

log A before log A after log B before log B after
other 314.4 min 9.0 min 99.5 min 4.0 min
swiftCompilation 282.8 min 17962.0 min 18012.9 min
swiftAggregatedCompilation 22.6 min 44.6 min
detail steps 24869 24869 95822 95822
sum of step durations 318.7 min 318.7 min 18070.7 min 18070.7 min

Step counts and totals are unchanged — nothing is duplicated or dropped, only the type of existing steps changes.

What the driver changes about the log

Without the integrated driver, a target's Swift compilation is one build task that shells out to swiftc and decides internally what to compile — hence one section per target, and getSwiftIndividualSteps recovering the per-file detail from the command line.

With the driver, Xcode's build engine calls its planning phase and turns every planned job into a build task. One target then produces five kinds of section, none of which getDetailType knows (paths and module names pseudonymized):

SwiftDriver ModuleA normal arm64 com.apple.xcode.tools.swift.compiler (in target 'ModuleA' from project 'ModuleA')
SwiftCompile normal arm64 Compiling\ File2.swift,\ File3.swift (in target 'ModuleA' from project 'ModuleA')
SwiftCompile normal arm64 /path/to/Source/File1.swift (in target 'ModuleA' from project 'ModuleA')
SwiftEmitModule normal arm64 Emitting\ module\ for\ ModuleA (in target 'ModuleA' from project 'ModuleA')
SwiftDriverJobDiscovery normal arm64 Emitting module for ModuleA (in target 'ModuleA' from project 'ModuleA')

This is not a project opting into something unusual: SWIFT_USE_INTEGRATED_DRIVER, the flag that used to control it, appears nowhere in an Xcode 26.5 installation — neither that name nor IntegratedDriver is present in the app bundle, so there is nothing left to switch off.

Why the guard has to change together with the classification

A batch compilation names its files in commandDetailDesc and carries a subSection per file. getSwiftIndividualSteps synthesizes per-file steps for whole-module builds that lack them, and skips that when commandDetailDesc already names a file — via ^CompileSwift\s\w+\s\w+\s.+\.swift\s, the old spelling again. Recognize the step without fixing the guard and every file is reported twice. Log A, three runs:

                              upstream   getDetailType only   both fixes
detail steps                     24869                45413        24869
sum of step durations        318.7 min            833.3 min    318.7 min

Checked against a real batch section from log A:

  title:       Compiling File4.swift, File5.swift, File6.swift, ...
  signature:   SwiftCompile normal arm64 Compiling\ File4.swift,\ File5.swift,\ ...
  subSections: 18

  upstream regex  ^CompileSwift\s\w+\s\w+\s.+\.swift\s                    matches: False
  patched regex   ^(?:CompileSwift|SwiftCompile)\s\w+\s\w+\s.+\.swift\s   matches: True

A match means "the command already names the files, do not synthesize". A single-file command matches too (nothing to synthesize), and a whole-module swiftc command still does not, so that synthesis is untouched.

Per-log measurements

Same app, same toolchain (Xcode 26.5.0, verified from the toolchain path recorded inside each log), assembled two different ways. Aggregated by the first word of the signature, as classified by upstream 0.2.49.

Log A — only the driver's spellings appear:

signature prefix                 minutes    steps  classified as
SwiftCompile                       282.8    13617  other
SwiftDriver                         22.6      350  other
Ld                                   3.7      465  linker
SwiftEmitModule                      3.5      350  other
EmitSwiftModule                      3.2      340  other
CodeSign                             0.8     1135  other
SwiftDriverJobDiscovery              0.4     3686  other
WriteAuxiliaryFile                   0.2      453  writeAuxiliaryFile

Log B — both spellings appear, and the change is purely additive: the 95.5 minutes that leave other are exactly the SwiftCompile and SwiftDriver steps.

signature prefix                 minutes    steps  classified as
CompileSwift                     17962.0    86884  swiftCompilation
SwiftCompile                        50.9      666  other
SwiftDriver                         44.6      457  other
Ld                                   6.0      495  linker
CompileAssetCatalogVariant           2.3        7  compileAssetsCatalog
ScanDependencies                     0.9      146  other
PhaseScriptExecution                 0.7      403  scriptExecution
CodeSign                             0.6      505  other

Grouped by the target named in the signature, log B:

targets emitting `SwiftDriver` planning jobs:       457
targets emitting `SwiftCompile`:                    457
targets emitting `CompileSwift`:                    423
targets emitting BOTH spellings:                    423

The sums exceed the build durations — 5.9 min for log A, 12.4 for log B — because every file in a batch reports the batch's span. That is Xcode's own accounting, not something this change introduces; it does become possible to tell the aggregate apart from the per-file steps and pick one level, which was impossible while both were .other.

Full aggregates for every run are attached in the thread.

Tests
  • DetailStepTypeTests — both driver spellings, both legacy spellings as regression guards, and SwiftDriverJobDiscovery asserted to stay .other. That one is why the prefix keeps its trailing space: SwiftDriver without it also swallows SwiftDriverJobDiscovery, which is not a compilation step.
  • SwiftIndividualStepsTests — no synthesis when the command names the files, synthesis still happens for a whole-module command.

swift test — 96 tests, 0 failures. swiftlint --strict reports nothing on the touched files.

I left Tests/XCLogParserTests/XCTestManifests.swift alone: it is marked autogenerated and already omits several existing test classes. Happy to add entries if you would rather keep it in sync.

Left out: SwiftEmitModule, EmitSwiftModule and CodeSign, most of the 9 minutes still unclassified in log A. None has a matching case in DetailStepType, and adding public enum cases felt like a separate decision — glad to follow up.

🤖 Generated with Claude Code

Xcode renamed the Swift compilation tasks: `CompileSwift` became `SwiftCompile`
and `CompileSwiftSources` became `SwiftDriver`. `getDetailType` still matches
only the old spellings, so on a current log every Swift step is reported as
`.other`, and reporters that group by step type lose the compilation entirely.

The guard in `getSwiftIndividualSteps` is keyed to the same old name. Fixing the
classification alone would make it worse: a batch compilation names its files in
the command and the log already carries a subSection per file, so with the step
recognized as `.swiftCompilation` and the guard missing the modern name, every
file gets reported twice. Both places are updated together.

Measured on an Xcode 26.5 log of a ~400 module iOS app (5.9 min build), same log
before and after, release build from source:

  other                      314.4 min -> 9.0 min
  swiftCompilation               0 min -> 282.8 min
  swiftAggregatedCompilation      0 min -> 22.6 min
  detail steps                   24869 -> 24869

The step count and the sum of durations stay identical, so nothing is duplicated
or dropped; only the type of the existing steps changes.

`SwiftEmitModule`, `EmitSwiftModule` and `CodeSign` account for the 9 minutes
that remain unclassified. They have no matching case in `DetailStepType`, so
they are left for a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A single Xcode 26.5 log carries both: 86884 steps signed `CompileSwift` from
targets that compile whole module, next to 666 `SwiftCompile` and 457
`SwiftDriver` steps from the integrated driver. Calling the old spelling a
leftover of older Xcode versions was wrong, and the comments said so.

Behaviour is unchanged; this only rewrites comments and two test names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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