Speed up build time of
big project on
Objective-C + Swift
Ivan Bondar
Lead iOS developer in Avito
Build time: 194.803s
75% of build time - Compile Swift files phase
Swift:
- *.swift files: 626
- LOC: 27264
Project structure
Objective-C:
- *.m files: 729
- LOC: 45947
- 217 imports in bridging-header
What to do
• Tune build settings
• Reduce .swift files count
• Reduce extensions count
• Optimize slow compiling functions
• Fix warnings
• Apply ccache compiler cache utility
Tune build settings
Build Active Architecture Only
Enable Objective-C Modules
Debug data format - DWARF (no dsym file)
Enable Whole module optimization
Tune build settings
Debug data format - DWARF (no dsym file)
Build time: 191.623s (194.803s before)
Tune build settings
Whole module optimization
Main target build time: 76.614 s
Not suitable for debugging
Can’t compile unit test target - segfault or weird errors with
Swift-ObjC bridging
Reduce .swift files count
Pre-build action - merge all Swift code to one FAT
Swift file
Not suitable for big projects:
- can’t compile with segmentation fault 11
- eliminates «private» modifier
- can’t use breakpoints in the original source
Reduce .swift files count
Merge different classes/protocols in big .swift files
Not suitable for VIPER in general
Decided to apply only in certain cases, e.g.
put Input and Output protocol declarations in one file
Reduce extensions count
0
10
20
30
40
100 1000 2000 3000 5000 10000
methods extensions
* by Dmitry Bespalov
https://tech.zalando.com/blog/speeding-up-xcode-builds/
Reduce extensions count
class FilterViewController: UIViewController {
}
// MARK: UITableViewDelegate
extension FilterViewController: UITableViewDelegate {
}
// MARK: UITableViewDataSource
extension FilterViewController: UITableViewDataSource {
}
class FilterViewController: UIViewController,
UITableViewDelegate,
UITableViewDataSource {
// MARK: UITableViewDelegate
// MARK: UITableViewDataSource
}
Changes in code style applied
Before: After:
Extensions count reduced by 400
Build time: 94.3s (191.623s before)
Optimize slow compiling functions
Profile compile time per function, filter by time > 1 ms.
xcodebuild ARCHS=arm64 ONLY_ACTIVE_ARCH=NO -workspace App.xcworkspace -scheme App
clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep [1-9].
[0-9]ms | sort -nr > culprits.txt
Optimize slow compiling functions
What to fix:
• Functions with longest compile time
• Functions with big number of occurrences
2871.3ms /Users/iyubondar/Projects/avito-ios/Models/Domain/Advertisement/Advertisement/Base/AdvertisementImage.swift:2:5 init(url100x75:
String?, url140x105: String?, url240x180: String?, url432x324: String?, url640x480: String?, url1280x960: String?)
932.7ms /Users/iyubondar/Projects/avito-ios/Core/Extensions/UIKitExtensions/UICollectionView/UICollectionView+ChangeAnimations.swift:57:17
final class func changeSet<T : Hashable>(oldArray oldArray: [T], newArray: [T]) -> CollectionViewChangeSet
253.0ms /Users/iyubondar/Projects/avito-ios/Core/Extensions/UIKitExtensions/UICollectionView/UICollectionView+ChangeAnimations.swift:90:17
final class func changeSet<T>(oldArray oldArray: [T], newArray: [T], identityHashFunction: (T) -> Int, identityCheckFunction: (T, T) -> Bool,
equalityCheckFunction: (T, T) -> Bool = default) -> CollectionViewChangeSet
136.0ms /Users/iyubondar/Projects/avito-ios/Presentation/Views/Advertisement/AdvertisementView/AdvertisementPresenter.swift:9:26 @objc
public override func setModel(model: AnyObject!)
91.4ms/Users/iyubondar/Projects/avito-ios/Presentation/Views/Controls/PullToRefresh/ScrollViewRefresher.swift:251:18 private func
handleRefreshingProgressChanged(progress: RefreshingProgress)
84.0ms/Users/iyubondar/Projects/avito-ios/VIPER/SelectCategoryParameters/Validation/SelectFromToValidator.swift:52:25 private final class func
findValidRowIndex(valuesToSelect: [SelectCategoryParameterViewModel.Data], inCompareToValues compareValues:
[SelectCategoryParameterViewModel.Data]?, selectedRowIndex: Int, iterationOrder: IterationOrder) -> Int
82.5ms/Users/iyubondar/Projects/avito-ios/Presentation/Views/Profile/ProfileViewPresenter.swift:30:10 @objc func notificationsSubtitle() -> String
81.8ms<invalid loc> init?(rawValue: String)
currentDate: NSDate, calendar: NSCalendar, todayDateFormatter: NSDateFormatter, yesterdayDateFormatter: NSDateFormatter, weekdayDateFormatter:
NSDateFormatter, dayDateFormatter: NSDateFormatter, yearDayDateFormatter: NSDateFormatter, fullDateFormatter: NSDateFormatter) -> String
8659 occurrences found.
Use lazy only when it's necessary
1204 occurrences found
Compile time: 1.0 … 11.8 ms
Optimize slow compiling functions
private lazy var footerLabel: UILabel = {
let footerLabel = UILabel()
footerLabel.textColor = SpecColors.mainText
footerLabel.font = SpecFonts.regular(14)
footerLabel.autoresizingMask = .FlexibleWidth
footerLabel.lineBreakMode = .ByWordWrapping
footerLabel.numberOfLines = 0
footerLabel.textAlignment = .Center
footerLabel.shadowColor = UIColor.whiteColor()
footerLabel.shadowOffset = CGSize(width: 0, height: -1)
return footerLabel
}()
Avoid long expressions
Compile time: 2871.3ms
Optimize slow compiling functions
self.thumbnailUrl = url240x180 ?? url140x105 ?? url100x75 ?? nil
self.fullImageUrl = url640x480 ?? url432x324 ?? url1280x960 ?? url240x180 ??
url140x105 ?? url100x75 ?? nil
self.thumbnailUrl = url240x180 ?? url140x105 ?? url100x75
self.fullImageUrl = url640x480 ?? url432x324 ?? url1280x960 ?? url240x180 ??
url140x105 ?? url100x75
Compile time: 916 ms
Avoid long expressions
Optimize slow compiling functions
var fullImageUrl: String? {
if let url640x480 = url640x480 {
return url640x480
}
if let url432x324 = url432x324 {
return url432x324
}
…
if let url100x75 = url100x75 {
return url100x75
}
return nil
}
Compile time: <1 ms
Use map() and flatMap() with care
1177 occurrences found
Compile time: 2.5 … 21.8 ms
Optimize slow compiling functions
private lazy var tabControllers: [UIViewController] = {
var controllers = [UIViewController?](count: Tab.tabsCount, repeatedValue: nil)
controllers[Tab.Search.rawValue] = self.categoriesNavigationController()
controllers[Tab.Favorites.rawValue] = self.favoritesNavigationController()
controllers[Tab.Publish.rawValue] = self.publishNavigationController()
controllers[Tab.Messenger.rawValue] = self.channelsRootViewController()
controllers[Tab.Profile.rawValue] = self.profileNavigationController()
return controllers.flatMap { $0 }
}()
Use map() and flatMap() with care
Optimize slow compiling functions
Original example: http://irace.me/swift-profiling
return [CustomType()] + array.map(CustomType.init) + [CustomType()]
Compile time: 3158.2 ms and many occurrences
Give «type hints» to the compiler when necessary
* example by IMPATHIC
http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times
Optimize slow compiling functions
func hangCompiler() { ["A": [ ["B": [ 1, 2, 3, 4, 5 ]], ["C": [ ]], ["D": [ ["A":
[ 1 ]] ]] ]] }
Build time: 54.249 s
func hangCompiler() { ["A": [ ["B": [ 1, 2, 3, 4, 5 ]] as [String: [Int]], ["C": [ ]]
as [String: [Int]], ["D": [ ["A": [ 1 ]] as [String: [Int]] ]] ]] }
Build time: 1.293 s
Optimize slow compiling functions
Build time: 92.5s (94.3s before)
No great effect in our case
Before:
• 8659 functions > 1ms
• max time 2871.3мс
• median time 1.8ms
After:
• 2227 functions > 1ms
• max time 124.3мс
• median time 3.4ms
Fix warnings
Swift Compiler Warnings: 64 total
Warning type Count
Build time
after fix
Parameters of ... have different optionality 3 92.5s
User-defined 2 92.5s
Cannot find protocol definition 3 90.481s
Overriding instance method parameter with implicitly unwraped optional
type 27 warnings
27 90.195s
Deprecation 18 93.487s
<Some code> will never been executed 1 93.654s
Immutable value ... was never used 1 93.152s
Pointer is missing nullability specifier 9 80.667s
Build time: 80.667s (92.5s before)
Fix warnings
But, with 1 warning <Some code> will never been executed, build
time is 84.919s
Fix all Swift Compiler Warnings to speed up build time!
Apply ccache compiler cache utility
Build time: 68.403s (80.667s before)
ccache limitations:
- no support for Clang modules
- no support for precompiled headers
- no Swift support
ccache applied to project
What was done
Before: 194.803 s After: 68.403 s
There is no silver bullet :(
• Tune build settings - 3s
• Reduce .swift files count - 0s
• Reduce extensions count - 97s
• Optimize slow compiling functions - 2s
• Fix warnings - 12s
• Apply ccache compiler cache utility -12s
Injection for Xcode plugin:
+ dynamically inserts new Swift / Objective-C code into a
running app
+ support «tunable parameters»
Source: https://github.com/johnno1962/injectionforxcode
Injection for Xcode plugin:
Source: https://github.com/johnno1962/injectionforxcode
Swift limitations. It’s not possible to:
- Make changes to Structs.
- Change functions or classes that are marked as final.
- Change global functions or variables that are not
constrained into a class.
Split app into frameworks with no cyclic dependencies between
classes of different frameworks *
Plans
* idea and image: http://bits.citrusbyte.com/improving-swift-compile-time/
Further legacy code refactoring, remove Swift -
Objective C dependencies.
Plans
Links
• http://bits.citrusbyte.com/improving-swift-compile-time/
• http://stackoverflow.com/questions/25537614/why-is-swift-compile-time-so-slow
General:
• http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times
• http://irace.me/swift-profiling
Debug slow compile time:
• https://tech.zalando.com/blog/speeding-up-xcode-builds/
• https://gist.github.com/lucholaf/e37f4d26e406250a156a
Speed up builds:
• https://developer.apple.com/videos/play/wwdc2015/409/
• http://useyourloaf.com/blog/swift-whole-module-optimization/
• http://useyourloaf.com/blog/modules-and-precompiled-headers/
• https://labs.spotify.com/2013/11/04/shaving-off-time-from-the-ios-edit-build-test-cycle/
• http://tomj.io/2015/08/29/speed-up-your-swift-test-builds-by-70-percent.html
• https://pewpewthespells.com/blog/managing_xcode.html#dep-imp
Build settings:
• https://pspdfkit.com/blog/2015/ccache-for-fun-and-profit/
• https://ccache.samba.org/manual.html#_configuration
ccache utility:
Questions?

"Ускорение сборки большого проекта на Objective-C + Swift" Иван Бондарь (Avito)

  • 1.
    Speed up buildtime of big project on Objective-C + Swift Ivan Bondar Lead iOS developer in Avito
  • 2.
    Build time: 194.803s 75%of build time - Compile Swift files phase Swift: - *.swift files: 626 - LOC: 27264 Project structure Objective-C: - *.m files: 729 - LOC: 45947 - 217 imports in bridging-header
  • 3.
    What to do •Tune build settings • Reduce .swift files count • Reduce extensions count • Optimize slow compiling functions • Fix warnings • Apply ccache compiler cache utility
  • 4.
    Tune build settings BuildActive Architecture Only Enable Objective-C Modules Debug data format - DWARF (no dsym file) Enable Whole module optimization
  • 5.
    Tune build settings Debugdata format - DWARF (no dsym file) Build time: 191.623s (194.803s before)
  • 6.
    Tune build settings Wholemodule optimization Main target build time: 76.614 s Not suitable for debugging Can’t compile unit test target - segfault or weird errors with Swift-ObjC bridging
  • 7.
    Reduce .swift filescount Pre-build action - merge all Swift code to one FAT Swift file Not suitable for big projects: - can’t compile with segmentation fault 11 - eliminates «private» modifier - can’t use breakpoints in the original source
  • 8.
    Reduce .swift filescount Merge different classes/protocols in big .swift files Not suitable for VIPER in general Decided to apply only in certain cases, e.g. put Input and Output protocol declarations in one file
  • 9.
    Reduce extensions count 0 10 20 30 40 1001000 2000 3000 5000 10000 methods extensions * by Dmitry Bespalov https://tech.zalando.com/blog/speeding-up-xcode-builds/
  • 10.
    Reduce extensions count classFilterViewController: UIViewController { } // MARK: UITableViewDelegate extension FilterViewController: UITableViewDelegate { } // MARK: UITableViewDataSource extension FilterViewController: UITableViewDataSource { } class FilterViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { // MARK: UITableViewDelegate // MARK: UITableViewDataSource } Changes in code style applied Before: After: Extensions count reduced by 400 Build time: 94.3s (191.623s before)
  • 11.
    Optimize slow compilingfunctions Profile compile time per function, filter by time > 1 ms. xcodebuild ARCHS=arm64 ONLY_ACTIVE_ARCH=NO -workspace App.xcworkspace -scheme App clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep [1-9]. [0-9]ms | sort -nr > culprits.txt
  • 12.
    Optimize slow compilingfunctions What to fix: • Functions with longest compile time • Functions with big number of occurrences 2871.3ms /Users/iyubondar/Projects/avito-ios/Models/Domain/Advertisement/Advertisement/Base/AdvertisementImage.swift:2:5 init(url100x75: String?, url140x105: String?, url240x180: String?, url432x324: String?, url640x480: String?, url1280x960: String?) 932.7ms /Users/iyubondar/Projects/avito-ios/Core/Extensions/UIKitExtensions/UICollectionView/UICollectionView+ChangeAnimations.swift:57:17 final class func changeSet<T : Hashable>(oldArray oldArray: [T], newArray: [T]) -> CollectionViewChangeSet 253.0ms /Users/iyubondar/Projects/avito-ios/Core/Extensions/UIKitExtensions/UICollectionView/UICollectionView+ChangeAnimations.swift:90:17 final class func changeSet<T>(oldArray oldArray: [T], newArray: [T], identityHashFunction: (T) -> Int, identityCheckFunction: (T, T) -> Bool, equalityCheckFunction: (T, T) -> Bool = default) -> CollectionViewChangeSet 136.0ms /Users/iyubondar/Projects/avito-ios/Presentation/Views/Advertisement/AdvertisementView/AdvertisementPresenter.swift:9:26 @objc public override func setModel(model: AnyObject!) 91.4ms/Users/iyubondar/Projects/avito-ios/Presentation/Views/Controls/PullToRefresh/ScrollViewRefresher.swift:251:18 private func handleRefreshingProgressChanged(progress: RefreshingProgress) 84.0ms/Users/iyubondar/Projects/avito-ios/VIPER/SelectCategoryParameters/Validation/SelectFromToValidator.swift:52:25 private final class func findValidRowIndex(valuesToSelect: [SelectCategoryParameterViewModel.Data], inCompareToValues compareValues: [SelectCategoryParameterViewModel.Data]?, selectedRowIndex: Int, iterationOrder: IterationOrder) -> Int 82.5ms/Users/iyubondar/Projects/avito-ios/Presentation/Views/Profile/ProfileViewPresenter.swift:30:10 @objc func notificationsSubtitle() -> String 81.8ms<invalid loc> init?(rawValue: String) currentDate: NSDate, calendar: NSCalendar, todayDateFormatter: NSDateFormatter, yesterdayDateFormatter: NSDateFormatter, weekdayDateFormatter: NSDateFormatter, dayDateFormatter: NSDateFormatter, yearDayDateFormatter: NSDateFormatter, fullDateFormatter: NSDateFormatter) -> String 8659 occurrences found.
  • 13.
    Use lazy onlywhen it's necessary 1204 occurrences found Compile time: 1.0 … 11.8 ms Optimize slow compiling functions private lazy var footerLabel: UILabel = { let footerLabel = UILabel() footerLabel.textColor = SpecColors.mainText footerLabel.font = SpecFonts.regular(14) footerLabel.autoresizingMask = .FlexibleWidth footerLabel.lineBreakMode = .ByWordWrapping footerLabel.numberOfLines = 0 footerLabel.textAlignment = .Center footerLabel.shadowColor = UIColor.whiteColor() footerLabel.shadowOffset = CGSize(width: 0, height: -1) return footerLabel }()
  • 14.
    Avoid long expressions Compiletime: 2871.3ms Optimize slow compiling functions self.thumbnailUrl = url240x180 ?? url140x105 ?? url100x75 ?? nil self.fullImageUrl = url640x480 ?? url432x324 ?? url1280x960 ?? url240x180 ?? url140x105 ?? url100x75 ?? nil self.thumbnailUrl = url240x180 ?? url140x105 ?? url100x75 self.fullImageUrl = url640x480 ?? url432x324 ?? url1280x960 ?? url240x180 ?? url140x105 ?? url100x75 Compile time: 916 ms
  • 15.
    Avoid long expressions Optimizeslow compiling functions var fullImageUrl: String? { if let url640x480 = url640x480 { return url640x480 } if let url432x324 = url432x324 { return url432x324 } … if let url100x75 = url100x75 { return url100x75 } return nil } Compile time: <1 ms
  • 16.
    Use map() andflatMap() with care 1177 occurrences found Compile time: 2.5 … 21.8 ms Optimize slow compiling functions private lazy var tabControllers: [UIViewController] = { var controllers = [UIViewController?](count: Tab.tabsCount, repeatedValue: nil) controllers[Tab.Search.rawValue] = self.categoriesNavigationController() controllers[Tab.Favorites.rawValue] = self.favoritesNavigationController() controllers[Tab.Publish.rawValue] = self.publishNavigationController() controllers[Tab.Messenger.rawValue] = self.channelsRootViewController() controllers[Tab.Profile.rawValue] = self.profileNavigationController() return controllers.flatMap { $0 } }()
  • 17.
    Use map() andflatMap() with care Optimize slow compiling functions Original example: http://irace.me/swift-profiling return [CustomType()] + array.map(CustomType.init) + [CustomType()] Compile time: 3158.2 ms and many occurrences
  • 18.
    Give «type hints»to the compiler when necessary * example by IMPATHIC http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times Optimize slow compiling functions func hangCompiler() { ["A": [ ["B": [ 1, 2, 3, 4, 5 ]], ["C": [ ]], ["D": [ ["A": [ 1 ]] ]] ]] } Build time: 54.249 s func hangCompiler() { ["A": [ ["B": [ 1, 2, 3, 4, 5 ]] as [String: [Int]], ["C": [ ]] as [String: [Int]], ["D": [ ["A": [ 1 ]] as [String: [Int]] ]] ]] } Build time: 1.293 s
  • 19.
    Optimize slow compilingfunctions Build time: 92.5s (94.3s before) No great effect in our case Before: • 8659 functions > 1ms • max time 2871.3мс • median time 1.8ms After: • 2227 functions > 1ms • max time 124.3мс • median time 3.4ms
  • 20.
    Fix warnings Swift CompilerWarnings: 64 total Warning type Count Build time after fix Parameters of ... have different optionality 3 92.5s User-defined 2 92.5s Cannot find protocol definition 3 90.481s Overriding instance method parameter with implicitly unwraped optional type 27 warnings 27 90.195s Deprecation 18 93.487s <Some code> will never been executed 1 93.654s Immutable value ... was never used 1 93.152s Pointer is missing nullability specifier 9 80.667s Build time: 80.667s (92.5s before)
  • 21.
    Fix warnings But, with1 warning <Some code> will never been executed, build time is 84.919s Fix all Swift Compiler Warnings to speed up build time!
  • 22.
    Apply ccache compilercache utility Build time: 68.403s (80.667s before) ccache limitations: - no support for Clang modules - no support for precompiled headers - no Swift support ccache applied to project
  • 23.
    What was done Before:194.803 s After: 68.403 s There is no silver bullet :( • Tune build settings - 3s • Reduce .swift files count - 0s • Reduce extensions count - 97s • Optimize slow compiling functions - 2s • Fix warnings - 12s • Apply ccache compiler cache utility -12s
  • 24.
    Injection for Xcodeplugin: + dynamically inserts new Swift / Objective-C code into a running app + support «tunable parameters» Source: https://github.com/johnno1962/injectionforxcode
  • 25.
    Injection for Xcodeplugin: Source: https://github.com/johnno1962/injectionforxcode Swift limitations. It’s not possible to: - Make changes to Structs. - Change functions or classes that are marked as final. - Change global functions or variables that are not constrained into a class.
  • 26.
    Split app intoframeworks with no cyclic dependencies between classes of different frameworks * Plans * idea and image: http://bits.citrusbyte.com/improving-swift-compile-time/
  • 27.
    Further legacy coderefactoring, remove Swift - Objective C dependencies. Plans
  • 28.
    Links • http://bits.citrusbyte.com/improving-swift-compile-time/ • http://stackoverflow.com/questions/25537614/why-is-swift-compile-time-so-slow General: •http://blog.impathic.com/post/99647568844/debugging-slow-swift-compile-times • http://irace.me/swift-profiling Debug slow compile time: • https://tech.zalando.com/blog/speeding-up-xcode-builds/ • https://gist.github.com/lucholaf/e37f4d26e406250a156a Speed up builds: • https://developer.apple.com/videos/play/wwdc2015/409/ • http://useyourloaf.com/blog/swift-whole-module-optimization/ • http://useyourloaf.com/blog/modules-and-precompiled-headers/ • https://labs.spotify.com/2013/11/04/shaving-off-time-from-the-ios-edit-build-test-cycle/ • http://tomj.io/2015/08/29/speed-up-your-swift-test-builds-by-70-percent.html • https://pewpewthespells.com/blog/managing_xcode.html#dep-imp Build settings: • https://pspdfkit.com/blog/2015/ccache-for-fun-and-profit/ • https://ccache.samba.org/manual.html#_configuration ccache utility:
  • 29.