fastred / Optimizing-Swift-Build-Times
- четверг, 9 ноября 2017 г. в 03:14:21
Collection of advice on optimizing compile times of Swift projects.
Collection of advice on optimizing compile times of Swift projects.
Swift is constantly improving
👷🏻 Maintainer: Arek Holko. Anything missing? Issues and pull requests welcomed!
Swift build times are slow mostly because of expensive type checking. By default Xcode doesn't show code that's slow to compile. You can instruct it to show slowly compiling functions and expressions, though by adding:
-Xfrontend -warn-long-function-bodies=100
(100
means 100ms here, you should experiment with this value depending on your computer speed and project)-Xfrontend -warn-long-expression-type-checking=100
to Other Swift Flags
in build settings:
Build again and you should now see warnings like these:
Next step is to address code that Swift compiler has problems with. John Sundell and Robert Gummesson are here to help you with that.
The previous section described working on an expression- and function-level but it’s often interesting to know compile times of whole files too.
There’s no UI in Xcode for that, though, so you have to build the project from the CLI with correct flags set:
xcodebuild -destination 'platform=iOS Simulator,name=iPhone 8' \
-sdk iphonesimulator -project YourProject.xcodeproj \
-scheme Chuck -configuration Debug \
clean build \
OTHER_SWIFT_FLAGS="-driver-time-compilation \
-Xfrontend -debug-time-function-bodies \
-Xfrontend -debug-time-compilation" |
tee profile.log
(Replace -project YourProject.xcodeproj
with -workspace YourProject.xcworkspace
if you use a workspace.)
Then extract the interesting statistics using:
awk '/Driver Compilation Time/,/Total$/ { print }' profile.log |
grep compile |
cut -c 55- |
sed -e 's/^ *//;s/ (.*%) compile / /;s/ [^ ]*Bridging-Header.h$//' |
sed -e "s|$(pwd)/||" |
sort -rn |
tee slowest.log
You’ll end up with slowest.log
file containing list of all files in the project, along with their compile times. Example:
2.7288 ( 0.3%) {compile: Account.o <= Account.swift }
2.7221 ( 0.3%) {compile: MessageTag.o <= MessageTag.swift }
2.7089 ( 0.3%) {compile: EdgeShadowLayer.o <= EdgeShadowLayer.swift }
2.4605 ( 0.3%) {compile: SlideInPresentationAnimator.o <= SlideInPresentationAnimator.swift }
This setting is a default but you should double check that it’s correct. Your project should build only active architecture in Debug configuration.
By default in new projects, dSYM files aren’t generated at all for Debug builds. However, it’s sometimes useful to have them available when running on a device – to be able to analyze crashes happening without the debugger attached.
Recommended setup:
Another common trick is to:
Optimization Level
to Fast, Whole Module Optimization
for Debug configuration-Onone
flag to Other Swift Flags
only for Debug configurationWhat this does is it instructs the compiler to:
It runs one compiler job with all source files in a module instead of one job per source file
Less parallelism but also less duplicated work
It's a bug that it's faster; we need to do less duplicated work. Improving this is a goal going forward
Note that incremental builds with minimal changes seem to be a bit slower under this setup. You should see a vast speedup (2x in many projects) in a worst-case scenario, though.
There are two ways you can embed third-party dependencies in your projects:
CocoaPods being the most popular dependency manager for iOS by design leads to longer compile times, as the source code of 3rd-party libraries in most cases gets compiled each time you perform a clean build. In general you shouldn’t have to do that often but in reality, you do (e.g. because of switching branches, Xcode bugs, etc.).
Carthage, even though it’s harder to use, is a better choice if you care about build times. You build external dependencies only when you change something in the dependency list (add a new framework, update a framework to a newer version, etc.). That may take 5 or 15 minutes to complete but you do it a lot less often than building code embedded with CocoaPods.
Incremental compilation in Swift isn’t perfect. There are projects where changing one string somewhere causes almost a whole project to get recompiled during an incremental build. It’s something that can be debugged and improved but a good tooling for that isn’t available yet.
To avoid issues like that, you should consider splitting your app into modules. In iOS, these are either: dynamic frameworks or static libraries (support for Swift was added in Xcode 9).
Let’s say your app target depends on an internal framework called DatabaseKit
. The main guarantee of this approach is that when you change something in your app project, DatabaseKit
won’t get recompiled during an incremental build.
XIBs/storyboards vs. code. UIView
subclass.
Let’s say we have a common project setup with 3 targets:
App
AppTests
AppUITests
Working with only one scheme is fine but we can do better. The setup we’ve been using recently consists of three schemes:
Builds only the app on cmd-B. Runs only unit tests. Useful for short iterations, e.g. on a UI code, as only the needed code gets built.
Builds both the app and unit test target. Runs only unit tests. Useful when working on code related to unit tests, because you find about compile errors in tests immediately after building a project, not even having to run them!
This scheme is useful when your UI tests take too long to run them often.
Builds the app and all test targets. Runs all tests. Useful when working on code close to UI which impacts UI tests.
Finally, to be able to actually know whether your build times are improving, you should enable showing them in Xcode’s UI. To do that, run this from the command line:
$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES
Once done, after building a project (cmd-B) you should see:
I recommend comparing build times under same conditions each time, e.g.
$ rm -rf ~/Library/Developer/Xcode/DerivedData
).Alternatively, you can time builds from the command line:
$ time xcodebuild other params