Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Xcode 11 recompiles too much

Xcode 11 is recompiling (nearly?) my whole project, even if I just change a local private variable, or change a value of a constant in local scope, sometimes even in local private function scope. I sometime can get 2 or 3 changes with quick builds as expected, but soon enough it decides to recompile everything again (which takes too long).

Any ideas what might be going on? Is Xcode not able to determine what's changed, why does it recompile so much other stuff (even other modules).

Any advice is highly appreciated, thanks!

like image 277
Nikolay Suvandzhiev Avatar asked Mar 25 '20 18:03

Nikolay Suvandzhiev


People also ask

Why does Xcode build take so long?

Xcode has inbuilt features that allow you to identify functions and expressions that are causing longer compile times. You can specify a compile time limit and identify areas in your codebase that exceed this limit. Open up your project's build settings and add the following flags to your Other Swift Flags .

How do I see build time in Xcode?

If you use Xcode, Product->Perform Action->Build With Timing Summary . And see building time summary in the Xcode building log.


Video Answer


2 Answers

We had the same problem and we fixed it. Twice.

Incremental build (same build machine):

before: ~10m after: ~35s

HOW?

Let's start with our experience first. We had a massive Swift/Obj-C project and that was the main concern: build times were slow and you had to create a new project to implement a new feature (literally). Bonus points for never-working syntax highlighting.

Theory

To truly fix this you have to truly understand how build system works. For example, let's try this code snippet:

import FacebookSDK
import RxSwift
import PinLayout

and imagine you use all of these imports in your file. And also this file depends on another file, which depends on another libraries, which in turn uses another libraries etc.

So to compile your file Xcode has to compile every library you mentioned and every file it depends on, so if you change one of the "core" files Xcode has to rebuild literally whole project.

Dependency tree

Xcode build is multi-threaded, but it consists of many single-threaded trees.

So on the first step of every incremental build Xcode is deciding which files have to be re-compiled and builds an AST tree. If you change a file which is acting as "dependable" on other files, so every other file which acts as "dependent" has to be recompiled.

Coupling

So the first advice is to lower coupling. Your project parts have to be independent of each other.

Obj-C/Swift bridge

Problem with those trees if you're using a Obj-C/Swift bridge, Xcode has to go through more phases than usual:

Perfect world:

  1. Builds Obj-C code
  2. Build Swift code

Swift/Obj-C bridge

Obj-C/Swift bridge:

  1. [REPEATABLE STEP] Build Swift code, which is needed to compile Obj-C code
  2. [REPEATABLE STEP] Build Obj-C code, which is needed to compile Swift code
  3. Repeat 1 & 2 until you have only non-dependable Swift & Obj-C code left
  4. Build Obj-C code
  5. Build Swift code

Obj-C/Swift bridge

So if you change something from step 1 or 2, you're basically in a trouble. The best solution is to minimize Obj-C/Swift Bridge (and remove it from your project).

If you don't have an Obj-C/Swift Bridge, that's awesome and you're good to go to the next step:

Swift Package Manager

Time to move to SwiftPM (or at least configure your Cocoapods better).

Thing is, most frameworks with default Cocoapods configuration drag along with themselves a lot of stuff you don't need.

To test this create an empty project with only one dependency like PinLayout, for example and try to write this code with Cocoapods (default configuration) and SwiftPM.

import PinLayout

final class TestViewController: UIViewController {

}

Spoiler: Cocoapods will compile this code, because Cocoapods will import EVERY IMPORT of PinLayout (including UIKit) and SwiftPM will not because SwiftPM imports frameworks atomically.

Dirty hack

Do you remember Xcode build is multi-threaded?

Well, you can abuse it, if you are able to split your project to many independent pieces and import all of them as independent frameworks to your project. It does lower the coupling and that was actually the first solution we used, but it wasn't in fact very effective, because we could only reduce incremental build time to ~4-5m which is NOTHING compared to the first method.

like image 135
x0 z1 Avatar answered Oct 13 '22 17:10

x0 z1


There's no golden bullet here, but plenty of things to check:

  • Make sure you're actually using the Debug configuration in your schemeXcode Scheme Editor using the Debug configuration

  • See below for how to ensure you're using incremental builds versus whole module per matt's advice. Also make sure your Optimization Level for Debug builds is none. Xcode Build settings showing Incremental Builds

  • If you're using type-inference heavy frameworks like RxSwift, adding explicit type annotations can speed up build times.

  • If the project is very big, you could consider refactoring out logical groups of source files into frameworks, but that may be too drastic of a change than you'd prefer

It might help if you provided some more specifics about the project: are you statically linking any libraries? Is it a framework or app target? How big and what swift version are you using? Do you have any custom Build Phases like linters or code generation that could be skipped sometimes?

like image 43
nteissler Avatar answered Oct 13 '22 19:10

nteissler