Build performance analysis for speeding up Xcode builds

Build performance can be analysed in Xcode to speed up Xcode builds. This can easily speed up your workflow and save a lot of time during the day. Slow builds tempt to distract us as we have to wait and jump on distractions like Twitter and Slack.

By investigating your build performance and investing some time to improve where possible you’ll see that you can make progress with a few small steps. Let’s dive into the options we have today.

Build performance insights with Build Timing Summary

Xcode 10 introduced the Build with Timing summary action. It builds your project once and generates a summary of time spent per category. It’s important to start with getting these insights as you would otherwise not know whether you improved your build performance at all.

The action can be triggered in the product menu using Product ➔ Perform Action ➔ Build with Timing Summary or using xcodebuild -buildWithTimingSummary.

Analyse build performance using the Build Timing Summary Action.
Analyse build performance using the Build Timing Summary Action.

After executing this action you’ll see that Xcode start building your project on the selected target device or simulator.

Navigate to the Report Navigator after the build is finished and select the last build. Select “Recent” and scroll all the way down until you see the Build Timing Summary.

An example build performance analysis in the Build Timing Summary.
An example build performance analysis in the Build Timing Summary.

This is a great starting point for investigating where you should improve your project.

As you can see, most of the time is spent on CompileSwiftSources, PhaseScriptExecution, and asset-related tasks. Improving compile times for your Swift code might not be so easy but we can have a look at improving compile times for our build phases.

Note that I did a clean build here which is different compared to doing an incremental build. It’s worth executing the action directly again as that will result in a different Build Timing Summary, not including the asset generation categories. In other words: we don’t have to optimise that part as we mainly want to focus on improving the biggest part of performance which comes down to incremental builds.

A Build Timing Summary for an incremental build.
A Build Timing Summary for an incremental build.

In this example, we didn’t touch any code and we directly run the Build with Timing Summary action again. These are the plain tasks that run every time you do a single change. The Phase Script execution is the interesting part as that takes most of the time. Let’s see how we can improve this!

How can the duration of Compiling Swift Sources be longer than the total build time?

Before we dive into optimisations I want to point out that it could be that compiling the Swift sources turns out to take longer than the total build time. The above build took 107 seconds to succeed while compiling the Swift sources took 197 seconds.

I’ve asked Rick Ballard from the Xcode Build System team for a clarification which gave some great insights into how the system works:

Yes – many commands, especially compilation, are able to run in parallel with each other, so multicore machines will finish the build much faster than the time it took to run each of the commands.

Optimizing build phases

Optimizing build phases is a great way to speed up Xcode builds. It might be that some of our build phases aren’t required for debug builds and be configured to run for release builds only.

While writing this blog post I’ve been trying to improve the build times of the Collect by WeTransfer app which I built during my day to day job. I found out that most of our time was spent on executing SwiftLint. For our main target, it took 10 seconds to execute for every incremental build.

One small improvement we found was adding the --quiet parameter but we only gained less than a second per build. All bits helped, so we decided to keep this in. The real big improvement we made was by filtering out files that aren’t changed. As we run SwiftLint in a lot of our submodules as well, we easily gained ~15 seconds improvement per build including all targets.

The code related to SwiftLint is quite specific to projects that use this linter tool. If you’re interested to see our final solution, I encourage you to check out this pull request.

Only run a build phase if needed

If you have a build script you want to run only for debugging or release builds, you can include the configuration check:

Build phase optimization by only running for debug builds.
Build phase optimization by only running for debug builds.

In this case, we’re only running the SwiftLint script for debug builds. You can obviously do the same by checking for “Release” builds if you want to run a script only for release builds.

Type checking of functions and expressions

To narrow down the causes of slow build times you can enable swift-flags to gather more insights. These flags were already available before Xcode 10, but are still very useful.

The compiler can warn about individual expressions that take a long time to type check using two frontend flags:

  • -Xfrontend -warn-long-function-bodies=<limit>
  • -Xfrontend -warn-long-expression-type-checking=<limit>

The <limit> value can be replaced for the number of milliseconds that an expression must take to type check in order for the warning to be emitted.

To enable these warnings, go the Build Settings ➔ Swift Compiler - Custom Flags ➔ Other Swift Flags:

Swift flags to analyse compile times of code
Swift flags to analyse compile times of code

With this setting, Xcode would trigger a warning for any function that took longer than 100ms to type-check. This can point you to methods that are slowing down build times. Splitting up those methods as well as adding explicit types might result in better build performance.

An example of a method that is warned to have a slow type check.
An example of a method that is warned to have a slow type check.

The above method results in a slow type-check which is bad for build performance. In this case, the slow type check is caused by the shorthand enum case. By adding NSFetchedResultsChangeType in front of .delete and .insert we’ve fixed the warning:

Fixing the slow type-check by adding an explicit type.
Fixing the slow type-check by adding an explicit type.

Build settings to speed up build performance

Speeding up Xcode builds by altering a few Xcode build settings. This used to be a common technique to easily gain seconds on an incremental build. Nowadays, Xcode has most of these settings set by default so there’s not much to cover. However, it could be that you’re maintaining an old project in which either these settings are not yet set or overwritten by the wrong values. Therefore, a short overview of the recommended settings of today.

Compilation mode

  • Debug: Incremental
  • Release: Whole Module

Optimization Level

  • Debug: No Optimization [-Onone]
  • Release: Optimize for Speed [-O]

Build Active Architecture Only

  • Debug: Yes
  • Release: No

Debug Information Format (DWARF)

  • Debug – Any iOS Simulator SDK: DWARF
  • Debug – Any iOS SDK : DWARF with DSYM File

Project Settings

Go into File ➔ Project Settings... and make sure to use the New Build System:

Make use of the New Build System in Xcode.
Make use of the New Build System in Xcode.

Scheme Settings

Lastly, it’s important to check whether you’re building in parallel:

Enable parallel building in the Xcode Scheme Settings.
Enable parallel building in the Xcode Scheme Settings.

Conclusion

Every now and then it’s good to revisit your Xcode build times. You can benefit from every second you gain and remember that it builds up over time: a second for every build is a minute for every 60 builds you do. Improvements can be made throughout your project settings, build phases, and code type checking improvements.

If you like to prepare and optimize yourself, even more, check out the optimization category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!