Swiftのメソッド毎のコンパイル時間を計測してビルド時間を短縮する

iOSアプリのビルドに時間がかかるようになったので、ビルド時間を計測して問題の有る箇所を修正したのでその方法を記す。

## ビルド全体の時間の計測

まずビルド全体でどれだけ時間がかかっているかを計測した。
ターミナルで以下のコマンドを実行することでXcodeでビルドした時にかかった時間が表示されるようになる。

```
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
```

これを実行してXcodeをリスタートする。クリーンした後ビルドを行うと、以下のようにビルド時間が表示される。小さいサンプルアプリなので4.324秒である。
![Voila_Capture 2016-01-08_08-52-43_午前.png](https://qiita-image-store.s3.amazonaws.com/0/2009/00d8eef7-7349-e8d2-d1fe-af5441e9d090.png "Voila_Capture 2016-01-08_08-52-43_午前.png")

このビルド時間が長い場合はさらにファイル毎、メソッド毎の時間を計測する。


## ファイル毎のコンパイル時間の計測

(この後で説明するメソッド毎の計測方法でファイル毎の時間も得られるので、この項は読み飛ばしても問題ない)

ファイル毎のコンパイル時間を計測するにはxctoolを利用する。プロジェクトルートフォルダで以下のコマンドを実行する。


```
xctool -scheme SchemeName -jobs 1 clean build >> build.txt
```

Workspaceを利用している場合は`-workspace ProjectName.xcworkspace`を追加する。(SchemeNameやProjectNameはプロジェクトそれぞれの名前を指定する。)
完了するとbuild.txtが作成されている。build.txtには

```
      ~ Compile ViewController.swift (3314 ms)
      ~ Compile AppDelegate.swift (290 ms)
```

といったコンパイル時間が記録されているので、このファイルをテキスト処理してファイル名と時間を抜き出し、時間で降順ソートするとコンパイルに時間のかかるファイルが分かる。

時間のかかるファイルはこれで判明した。そのファイルのどの部分により時間がかかっているかの情報が欲しいのでメソッド毎のコンパイル時間を計測する。


## メソッド毎のコンパイル時間の計測

メソッドやプロパティ毎のコンパイル時間を計測するには、まずXcodeで計測したいターゲットのBuild SettingsのOther Swift Flagsに`-Xfrontend -debug-time-function-bodies`を追加する。
そしてXcodeでクリーンした後ビルドするか、ファイル毎の場合と同様に

```
xctool -scheme SchemeName -jobs 1 clean build >> build_in_detail.txt
```

を実行する。
するとそのログから以下のような情報が得られる。

```
--------------------------------------------------------------------------------
      ~ Compile ViewController.swift (513 ms)
--------------------------------------------------------------------------------
18.5ms  /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:13:14        @objc get {}
0.1ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:13:14        @objc set {}
0.2ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:5:19 @objc override func viewDidLoad()
0.1ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:9:19 @objc override func didReceiveMemoryWarning()
12.5ms  /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:17:10        @objc func createArray()
3.4ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:22:10        @objc func createDictionary()
0.0ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:3:7  @objc deinit 
0.3ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:3:40 @objc @objc override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?)
0.2ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:3:40 @objc @objc required override init?(coder aDecoder: NSCoder)
```

これは以下のファイルのビルド結果である。

```Swift
import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    lazy var strings: [String] = {
        return ["test"]
    }()
    
    func createArray() {
        let array = ["1", 2, 3.0, CGFloat(4.0)]
        print(array)
    }
    
    func createDictionary() {
        let dict = ["key1" : "1", "key2" : 2]
        print(dict)
    }

}
```

13行目のlazy var stringsプロパティに18.5msかかっている。
17行目のcreateArrayメソッドには12.5msかかっている。この例ではそれほど遅いわけではないが、配列のタイプヒンティングを行わないと非常に時間がかかる場合がある。

そこで、以下のようにタイプヒンティングを付けて再度ビルドしてみる。

```swift
import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    lazy var strings: [String] = {
        let result: [String] = ["test"]
        return result
    }()
    
    func createArray() {
        let array: [AnyObject] = ["1", 2, 3.0, CGFloat(4.0)]
        print(array)
    }
    
    func createDictionary() {
        let dict = ["key1" : "1", "key2" : 2]
        print(dict)
    }

}
```

結果は以下のようになった。

```
--------------------------------------------------------------------------------
      ~ Compile ViewController.swift (493 ms)
--------------------------------------------------------------------------------
1.3ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:13:34        (closure)
2.6ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:13:14        @objc get {}
0.1ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:13:14        @objc set {}
0.1ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:5:19 @objc override func viewDidLoad()
0.1ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:9:19 @objc override func didReceiveMemoryWarning()
3.8ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:18:10        @objc func createArray()
8.3ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:23:10        @objc func createDictionary()
0.0ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:3:7  @objc deinit 
0.2ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:3:40 @objc @objc override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?)
0.2ms   /Users/username/Desktop/DurationSample/DurationSample/ViewController.swift:3:40 @objc @objc required override init?(coder aDecoder: NSCoder)
```

13行目のlazy var stringsは1.3msに短縮されている。またcreateArrayメソッドも3.8msに短縮された。


このように、ビルド全体・ファイル毎・メソッド毎の時間を計測することでビルド時間の問題点を把握して修正しやすくなる。コンパイルに時間がかかる原因は多くは型推論のようなので配列やディクショナリにタイプヒンティングを付けることでコンパイル時間を短縮することができる。