Xcode Build Time Optimization - Part 1


February 28, 2020

#xcode #workflow #optimization

Almost every developer once in a while suffers from quite long Xcode build time. It results in lower productivity and slows down the development process of the whole team. As you can see, enhancing build time is crucial because it has direct impact on time to market for shipping new features faster.

In this post, we’ll learn how to profile the Xcode build and get its metrics. In the next article, I’ll go through techniques to resolve bottlenecks and speed up the build. It should be mentioned as well, that we’ll be using Kickstarter iOS project, that can be found on Github. So let’s get started!

What to measure

The first thing we should do is to define what are we trying to measure and optimize. There are two options to consider:

  • Clean build - cleaning and rebuilding a project from scratch. Frequently clean build is done on CI to validate a pull request for correctness and run unit tests.
  • Incremental build - rebuilding a project after some source code changes. This build is created by a developer while working on a new feature.

In most of the cases, Clean build time improvements should speedup the Incremental build as well. The best option would be to generate metrics for both build types and keep track of them. We’ll be measuring builds created with Debug configuration only because it’s used most of the times and has a higher impact on development.

Measuring your build time

The most efficient way of improving the build time should be a data-driven approach when you introduce and verify changes based on the build metrics. Let’s dive into it and have a look at the tools we can use to get insights about the project’s build time.

Xcode build report

We can get the build time easily via Xcode. It keeps track of all your builds by default and you can examine the times and logs from the Report Navigator.

There is an option to display similar information in the Xcode activity viewer. You can enable it from the command line:

defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

The builds duration appears after a build, alongside with the “Succeeded” message.

These are just two basic options that should give you a rough idea about Clean and Incremental build time.

Xcode build timing summary

Xcode Build Timing Summary is your first friend in getting build time insights and finding bottlenecks. You can run one via Product->Perform Action->Build With Timing Summary. Now you should see a great breakdown of times spent on different tasks:

It should be a nice starting point to find out the most time-consuming tasks within your build process. As you can see from the screenshot above, CompileStoryboard, CompileXIB, CompileSwiftSources and PhaseScriptExecution phases took most of the build time. Xcode managed to run some of the tasks in parallel that’s why the build is finished much faster than the time it took to run each of the commands.

We can get build timing summary for clean build using xcodebuild with -buildWithTimingSummary option:

xcodebuild -project 'Kickstarter.xcodeproj' \
-scheme 'Kickstarter-iOS' \
-configuration 'Debug' \
-sdk 'iphonesimulator' \
-showBuildTimingSummary \
clean build | sed -n -e '/Build Timing Summary/,$p'

Build Timing Summary
CompileStoryboard (29 tasks) | 87.128 seconds
CompileSwiftSources (4 tasks) | 54.144 seconds
PhaseScriptExecution (14 tasks) | 18.167 seconds
CompileAssetCatalog (2 tasks) | 6.532 seconds
CompileXIB (21 tasks) | 6.293 seconds
CodeSign (7 tasks) | 3.069 seconds
Ld (4 tasks) | 2.342 seconds
LinkStoryboards (2 tasks) | 0.172 seconds
CompileC (3 tasks) | 0.122 seconds
Ditto (20 tasks) | 0.076 seconds
Touch (4 tasks) | 0.007 seconds
** BUILD SUCCEEDED ** [92.620 sec]


Now let’s get similar metrics for the incremental build. It should be mentioned that incremental build time fully depends on the files being changed in your project. To get consistent results you can change a single file and rebuilt the project. Unlike Buck or Bazel, Xcode uses timestamps to detect what has changed and what needs to be rebuilt. We can update a timestamp using touch then:

touch KsApi/mutations/CancelBackingMutation.swift && \
xcodebuild -project 'Kickstarter.xcodeproj' \
-scheme 'Kickstarter-iOS' \
-configuration 'Debug' \
-sdk 'iphonesimulator' \
-showBuildTimingSummary \
build | sed -n -e '/Build Timing Summary/,$p'

Build Timing Summary
PhaseScriptExecution (14 tasks) | 18.089 seconds
CodeSign (7 tasks) | 2.990 seconds
CompileSwiftSources (1 task) | 1.245 seconds
Ld (1 task) | 0.361 seconds
** BUILD SUCCEEDED ** [23.927 sec]

Type checking warnings

If Swift compile time is the bottleneck, we can get more information by setting Other Swift Flags from the Xcode build settings. With these flags enabled Xcode will generate a warning for any function or expression that took longer than 100ms to type-check:

  • -Xfrontend -warn-long-function-bodies=100
  • -Xfrontend -warn-long-expression-type-checking=100

Now you know the code Swift compiler has problems with and can come up with some improvements.

Compiler diagnostic options

The Swift compiler has a variety of built-in diagnostic options you can use to profile the build.

  • -driver-time-compilation - high-level timing of the jobs that the driver executes.
  • -Xfrontend -debug-time-compilation - timers for each phase of frontend job execution.
  • -Xfrontend -debug-time-function-bodies - time spent typechecking every function in the program.
  • -Xfrontend -debug-time-expression-type-checking - time spent typechecking every expression in the program.

Let’s use -debug-time-compilation flag to get the top slowest files to compile:

xcodebuild -project 'Kickstarter.xcodeproj' \
-scheme 'Kickstarter-iOS' \
-configuration 'Debug' \
-sdk 'iphonesimulator' \
clean build \
OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-compilation" |
    awk '/CompileSwift normal/,/Swift compilation/{print; getline; print; getline; print}' |
    grep -Eo "^CompileSwift.+\.swift|\d+\.\d+ seconds" |
    sed -e 'N;s/\(.*\)\n\(.*\)/\2 \1/' |
    sed -e "s|CompileSwift normal x86_64 $(pwd)/||" |
    sort -rn |
    head -3

25.6026 seconds Library/ViewModels/SettingsNewslettersCellViewModel.swift
24.4429 seconds Library/ViewModels/PledgeSummaryViewModel.swift
24.4312 seconds Library/ViewModels/PaymentMethodsViewModel.swift

As you can see, it took 25s to compile SettingsNewslettersCellViewModel.swift. From the build log, we can get more information about file compilation time:

===-------------------------------------------------------------------------===
                               Swift compilation
===-------------------------------------------------------------------------===
  Total Execution Time: 25.6026 seconds (26.6593 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
  24.4632 ( 98.3%)   0.5406 ( 76.5%)  25.0037 ( 97.7%)  26.0001 ( 97.5%)  Type checking and Semantic analysis
   0.0981 (  0.4%)   0.1383 ( 19.6%)   0.2364 (  0.9%)   0.2872 (  1.1%)  Name binding
   0.1788 (  0.7%)   0.0043 (  0.6%)   0.1831 (  0.7%)   0.1839 (  0.7%)  IRGen
   0.0508 (  0.2%)   0.0049 (  0.7%)   0.0557 (  0.2%)   0.0641 (  0.2%)  Parsing
   0.0599 (  0.2%)   0.0020 (  0.3%)   0.0619 (  0.2%)   0.0620 (  0.2%)  SILGen
   0.0285 (  0.1%)   0.0148 (  2.1%)   0.0433 (  0.2%)   0.0435 (  0.2%)  SIL optimization
   0.0146 (  0.1%)   0.0015 (  0.2%)   0.0161 (  0.1%)   0.0162 (  0.1%)  Serialization, swiftmodule
   0.0016 (  0.0%)   0.0006 (  0.1%)   0.0022 (  0.0%)   0.0022 (  0.0%)  Serialization, swiftdoc
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0001 (  0.0%)  SIL verification, pre-optimization
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)  AST verification
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)  SIL verification, post-optimization
  24.8956 (100.0%)   0.7069 (100.0%)  25.6026 (100.0%)  26.6593 (100.0%)  Total

Now it is clear that Type checking and Semantic analysis is the most time-consuming job. Let’s move forward and list top slowest function bodies and expressions in the Type Checking stage:

xcodebuild -project 'Kickstarter.xcodeproj' \
-scheme 'Kickstarter-iOS' \
-configuration 'Debug' \
-sdk 'iphonesimulator' \
clean build \
OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-expression-type-checking \
    -Xfrontend -debug-time-function-bodies" |
  grep -o "^\d*.\d*ms\t[^$]*$" |
  awk '!visited[$0]++' |
  sed -e "s|$(pwd)/||" |
  sort -rn |
  head -5

16226.04ms	Library/Styles/UpdateDraftStyles.swift:31:3
10551.24ms	Kickstarter-iOS/Views/RewardCardContainerView.swift:171:16	instance method configureBaseGradientView()
10547.41ms	Kickstarter-iOS/Views/RewardCardContainerView.swift:172:7
8639.30ms	Kickstarter-iOS/Views/Controllers/AddNewCardViewController.swift:396:67
8233.27ms	KsApi/models/templates/ProjectTemplates.swift:94:5

Just like that, we’ve profiled our build and found out that we have quite some bottlenecks in the Type Checking stage. As the next step you can have a look at the functions and expressions listed above and try to optimize type inference.

Target’s build times

It should be handy to measure the targets build times separately and show them on a chart. One can help understanding which targets are built or can be built in parallel. We can use xcode-build-times-rendering tool for this. Let’s install one as a RubyGem:

gem install xcode-build-times

After installation is complete, run the following command that injects timestamp logging in Run Script Build Phase of your targets:

xcode-build-times install

Then build the project and generate a report via:

xcode-build-times generate

As a result, you should get a nice build-time Gantt chart, that shows the build times of all your targets:

Aggregated metrics

It would be great to aggregate different metrics mentioned above. XCLogParser is a great tool that can help you with this. It is a log parser for Xcode-generated xcactivitylog and gives a lot of insights in regards to build times for every module and file in your project, warnings, errors and unit tests results. You can install it by cloning the repository and run via command line:

git clone https://github.com/spotify/XCLogParser
rake build
xclogparser parse --project Kickstarter --reporter html

This is the report created for the Kickstarter iOS project:

Automation

It should be pointed out that build time metrics are dependent on hardware and its utilization. You can use your machine for experiments. The better option would be automating the process as much as you can and using a dedicated CI machine to get metrics daily. Eventually, you can keep track of them on a dashboard and notify about the build time degradation via Slack.

Conclusion

The build time speedup is crucial for developers productivity and the product’s time to market. Today we’ve learned how to measure the Xcode build time and get some metrics to analyze 🎉.

In the next post, we’ll go through techniques you can use to speed up the build.

Thanks for reading!