Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Build performance analysis for speeding up Xcode builds

Build performance can be analyzed in Xcode to speed up your builds. This can quickly speed up your workflow and save a lot of time during the day for all developers working on the project. Slow builds often distract us as they enable us to focus on distractions like social media 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. I’ve made our WeTransfer’s incremental build time 4x faster using several optimizations. Let’s dive into the options we have today.

Measuring build performance using the Xcode Build Timeline

Before we start diving into optimizations, it’s essential to measure your project’s baseline. You can compare the baseline continuously along the way of implementing improvements.

Start by running a clean build using CMD ⌘ + SHIFT + K or selecting Product ➔ Clean Build Folder... from the menu. The build folder contains cached build files that will speed up incremental builds but lead to an incorrect baseline for performance optimizations. It’s essential to compare builds using the same environment you’ll create by cleaning your build folder.

You can start a fresh build after Xcode completes the cleanup. Select your build inside the Report Navigator once the build completes, and make sure to open the assistant to show the Build Timeline:

The Xcode Build Timeline gives you details about your build performance.
The Xcode Build Timeline gives you details about your build performance.

The assistant shows you where all time is spent during the clean build. In the above example, you’re looking at the optimized version of the WeTransfer project with optimized parallelization. The latter means that my MacBook’s cores are used optimally since every row is filled constantly with build processes. I encourage you to watch the WWDC 2022 session Demystify parallelization in Xcode builds to learn all about this concept.

We will use this Build Timeline as our starting point for investigating build performance. You can find potential improvements by zooming into specific blocks that execute serially. For example, the above timeline still shows the following isolated run shell script:

The Build Timeline shows a run script that we can potentially optimize.
The Build Timeline shows a run script that we can potentially optimize.

After clicking the specific build block, you can find more information about the executed process. In my case, I found out which run script we were looking at and concluded we could not optimize further for clean builds. I’ll dive deeper into these specific optimizations later.

Build performance insights with Build Timing Summary

The Build Timing Summary is another baseline insight you can use during build performance optimizations. It builds your project once and summarizes time spent per category.

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

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

The Build Timing Summary shows which build phases take the most time.
The Build Timing Summary shows which build phases take the most time.

This can be another excellent starting point for investigating where you should improve your project. In most cases, most time will be spent on compiling Swift files, for which you should focus on parallelization optimizations. Also, I recommend you look at the run script execution times to see if there’s room for improvement.

Note that I did a clean build here which is different from doing an incremental build. It’s worth executing the action directly again, resulting in a different Build Timing Summary. You’ll run incremental builds most of the time, so it’s worth finding slow parts that influence both clean and incremental builds.

The incremental build timing summary shows a much shorter list.
The incremental build timing summary shows a much shorter list.

The above example represents an incremental build after completing optimizations for the WeTransfer app. We’re nearly running any scripts and completed the build in an acceptable 5 seconds.

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

Before we dive into optimizations, I want to point out that compiling the Swift sources takes longer than the total build time. The above build took 71 seconds to succeed, while compiling the Swift sources took 352 seconds.

I’ve asked Rick Ballard from the Xcode Build System team for clarification, and he 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. Some of our run scripts might not be required for debug builds and should only be configured to run for release builds.

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. Our real significant improvement was filtering out files that weren’t changed. As we run SwiftLint in many of our submodules, we easily gained ~15 seconds of improvement per build, including all targets.

The code related to SwiftLint is quite specific to projects that use this linter tool. If you want 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 will 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 was a common technique to quickly gain seconds on an incremental build. Nowadays, Xcode has most of these settings set by default, so there’s little to cover. However, it could be that you’re maintaining an old project in which these settings still need to be set or are overwritten by the wrong values. Therefore, here is a short overview of the recommended settings of today.

Compilation mode

  • Debug: Incremental
  • Release: Whole Module

Optimization Level

  • Debug: No Optimization [-O0]
  • Release: Fastest, Smallest [-Os]

Build Active Architecture Only

  • Debug: Yes
  • Release: No

Debug Information Format (DWARF)

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

Enabling Eager Linking

You can enable eager linking for your project to see if it results in better build times. If enabled, the build system will emit a so-called TBD file for Swift-only framework and dynamic library targets to unblock the linking of dependent targets before their dependency has finished linking. You can learn more about linking by watching WWDC 2022 session Link fast: Improve build and launch times.

Run Build Script Phases in Parallel

Running build script phases in parallel can potentially lead to improved build times. I recommend combining it with User Script Sandboxing to disallow undeclared input/output dependencies. Only scripts with specified inputs and outputs and those configured to run based on dependency analysis will be attempted to run in parallel.

Swift Package Build Plugins

Swift Package Build Plugins influence build times quite a bit. For the WeTransfer app, I discovered that a pre-build plugin causes build-cache invalidation and increased build times for incremental builds. We had the opportunity to switch to a regular build plugin, resulting in fewer cache invalidations and faster incremental builds.

It’s also important to know that a package build plugin always takes about a second to run:

Swift Package Build Plugins will impact your build times.
Swift Package Build Plugins will impact your build times.

In the above example, there were no files to lint, and the build plugin did not return any build commands. I would’ve expected close to zero seconds impact on overall build times, but there’s a particular minimum work to be done.

Conclusion

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

 

Stay Updated with the Latest in Swift & SwiftUI

Let me do the hard work and join 18,250 developers that stay up to date using my weekly newsletter:


Featured SwiftLee Jobs

Find your next Swift career step at world-class companies with impressive apps by joining the SwiftLee Talent Collective. I'll match engineers in my collective with exciting app development companies. SwiftLee Jobs