Slide 1

Slide 1 text

Working at scale How to save time with CI and build time reduction Yusei Nishiyama @yuseinishiyama

Slide 2

Slide 2 text

• iOS, Android, Web • 67 countries • 21 languages • 60M monthly users (Japan) • 30M+ monthly users 
 (Outside Japan) https://info.cookpad.com/en https://cookpad.com/en

Slide 3

Slide 3 text

Distributed Team

Slide 4

Slide 4 text

Working at Cookpad is … • Working in a large team • Working with large code base

Slide 5

Slide 5 text

Working at Cookpad is … • Working in a large team • Working with a large code base

Slide 6

Slide 6 text

• Someone broke the build • We can’t release today because {name} is on vacation • Code review never finishes

Slide 7

Slide 7 text

Automation with CI • Testing • Nitpicking • Beta distribution • Release management

Slide 8

Slide 8 text

CI Server

Slide 9

Slide 9 text

CI Server

Slide 10

Slide 10 text

CI Server

Slide 11

Slide 11 text

CI Server

Slide 12

Slide 12 text

Fastlane in Cookpad • 7 Fastfiles • 28 lanes • 740 lines • 5 plugins

Slide 13

Slide 13 text

Sync dSYMs desc "Refresh dSYMs" lane :refresh_dsyms do version = get_version_number build_number = get_build_number # Download dSYM files from iTC into a temporary directory download_dsyms(version: version, build_number: build_number) # Upload them to Crashlytics upload_symbols_to_crashlytics # Delete the temporary dSYM files clean_build_artifacts end

Slide 14

Slide 14 text

Release the App Spaceship::Tunes.login app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) edit_version = Spaceship::Application.find(app_identifier).edit_version version_to_release = edit_version.version UI.user_error! "No pending verion to release" unless edit_version.app_status == "Pending Developer Release" edit_version.release! UI.success "Version #{version_to_release} is released " slack( channel: "#release", attachment_properties: { pretext: ":appleinc: *Cookpad iOS #{version_to_release} - Released*” }) refresh_dsyms

Slide 15

Slide 15 text

Triggers beta distribution

Slide 16

Slide 16 text

Check Labels desc "Check if pr is marked as beta distribution" private_lane :beta_distribution do labels.any? { |label| label == 'beta distribution' } end desc "Get labels of pull request" private_lane :labels do pr_number = ENV['PR_NUMBER'] repo = ENV['GITHUB_PULL_REQUEST_REPO'] result = github_api( http_method: 'GET', path: "/repos/#{repo}/issues/#{pr_number}/labels" ) result[:json].map { |label| label['name'] } end

Slide 17

Slide 17 text

Notify a New Build desc "Notify new beta distribution" private_lane :notify_build do messages = "A new version is available" pr_number = ENV['PR_NUMBER'] repo = ENV['GITHUB_PULL_REQUEST_REPO'] github_api( http_method: 'POST', path: "/repos/#{repo}/issues/#{pr_number}/comments", body: { "body": message } ) end

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

CI Server

Slide 20

Slide 20 text

Dangerfile github.dismiss_out_of_range_messages({ error: false, warning: true, message: true, markdown: true }) xcode_summary.inline_mode = true xcode_summary.ignored_files = ['Pods/**'] xcode_summary.ignored_results { |result| result.message.start_with? 'ld' } log = File.join('logs', 'test.json') xcode_summary.report log if File.exist?(log) swiftlint.binary_path = './Pods/SwiftLint/swiftlint' swiftlint.lint_files inline_mode: true

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

CI Server

Slide 23

Slide 23 text

InfluxDB • Only purpose-built platform for any time-based data • Written in Go • Built-in HTTP API • SQL-like query language • Answer queries in real-time • Rich client libraries

Slide 24

Slide 24 text

Post Code Coverage lane :post_coverage do rate = export_coverage if rate influxdb(table_name: table_name, values: {coverage: rate}) slack(message: "Code coverage: #{rate.to_f.round(4) * 100}%", channel: “#ios- notifications") end end private_lane :export_coverage do fastlane_require 'nokogiri' output_directory = 'coverage_report' reports = Dir.glob(File.join(ROOT_PATH, output_directory, '*.xml')) slather(use_bundle_exec: true, output_directory: output_directory) if reports.empty? nil else xml = Nokogiri::XML(File.open(reports.first)) coverage = xml.xpath('//coverage').first coverage.attr("line-rate").to_f end end

Slide 25

Slide 25 text

CI Server

Slide 26

Slide 26 text

Grafana • Rich metrics dashboard and editor • Fast rendering • Support InfluxDB as a data source • Query editor

Slide 27

Slide 27 text

Visualize Code Coverage

Slide 28

Slide 28 text

Other metrics

Slide 29

Slide 29 text

Working at Cookpad is … • Working in a large team • Working with a large code base

Slide 30

Slide 30 text

How many times 
 do you build every day?

Slide 31

Slide 31 text

Reduce Build Time • Measurement • Improvement

Slide 32

Slide 32 text

What I will “not” talk about • Annotate all types explicitly • Write as much as code in a single file • Get your Mac Pro

Slide 33

Slide 33 text

Reduce Build Time • Measurement • Improvement

Slide 34

Slide 34 text

“Premature optimization is the root of all evil.”
 
 — Donald Knuth —

Slide 35

Slide 35 text

Display build duration $ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

Slide 36

Slide 36 text

Build Process • Dependency Manager (CocoaPods, Carthage) • Compile source files • Run tests

Slide 37

Slide 37 text

fastlane/report.xml …

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Build Process • Dependency Manager (CocoaPods, Carthage) • Compile source files • Run tests

Slide 42

Slide 42 text

Build Process • Dependency Manager (CocoaPods, Carthage) • Compile source files • Function A • Function B • … • Runs tests

Slide 43

Slide 43 text

Warn long function bodies

Slide 44

Slide 44 text

Time function bodies $ pbpaste | grep '.[0-9]ms' | sort -nr | head -20

Slide 45

Slide 45 text

xcactivitylog • You can access build logs under DerivedData • xcactivitylog is just gzip. You can check the content as a plain text just by unzipping it

Slide 46

Slide 46 text

giginet/danger-xcprofiler (in Dangerfile)
 
 xcprofiler.thresholds = { warn: 50, fail: 500 } xcprofiler.report ‘ProductName'

Slide 47

Slide 47 text

Reduce Build Time • Measurement • Improvement

Slide 48

Slide 48 text

Test with Real Data $ cloc
 
 github.com/AlDanial/cloc v 1.72 T=3.40 s (153.3 files/s, 16646.6 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Swift 497 8944 3122 43831 JSON 18 0 0 522 Objective C 3 37 5 166 C/C++ Header 4 19 6 27 ------------------------------------------------------------------------------- SUM: 522 9000 3133 44546 -------------------------------------------------------------------------------

Slide 49

Slide 49 text

Test with Real Data • 40,000 lines of Swift code • CocoaPods • 38 direct dependencies and 60 libraries • Linter in build phases • CI archives the app and distributes it for internal testing

Slide 50

Slide 50 text

(Local clean build: 330s)

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

Whole Module Optimization

Slide 53

Slide 53 text

WMO without Optimization

Slide 54

Slide 54 text

(Local clean build: 160s)

Slide 55

Slide 55 text

Time Function Bodies

Slide 56

Slide 56 text

F

Slide 57

Slide 57 text

Carthage + github "ReactiveX/RxSwift" ~> 3.4 + github "RxSwiftCommunity/RxRealm" + github "realm/realm-cocoa" ~> 2.4 + github "danielgindi/Charts" ~> 3.0.2 - pod 'RxSwift', '3.3.1' - pod 'RxCocoa', '3.3.1' - pod 'RxRealm', '0.5.2' - pod 'RealmSwift', '2.4.4' Cartfile Podfile

Slide 58

Slide 58 text

https://github.com/apple/swift-evolution#development-major-version--swift-50

Slide 59

Slide 59 text

(Local clean build: 68s)

Slide 60

Slide 60 text

Xcode 9 New Build System

Slide 61

Slide 61 text

• Xcode 9 • Swift 3.2 • New build system • No significant improvement in build time

Slide 62

Slide 62 text

Do you really need to build your entire app?

Slide 63

Slide 63 text

Code Modularization Feature A Design Feature B Feature C Network Persistence Core

Slide 64

Slide 64 text

Benefits • Faster incremental build/indexing • Strict access control • Clear separation

Slide 65

Slide 65 text

Lessons Learned • Breaking existing implicit dependencies is hard • Starting from the single repository • Once modules become stable, consider • managing them in separated repositories • using pre-build versions

Slide 66

Slide 66 text

Pitfall

Slide 67

Slide 67 text

dylib Loading Time Total pre-main time: 4.2 seconds (100.0%) dylib loading time: 4.0 seconds (94.4%) rebase/binding time: 93.32 milliseconds (2.1%) ObjC setup time: 44.64 milliseconds (1.0%) initializer time: 98.30 milliseconds (2.3%)

Slide 68

Slide 68 text

Pitfall

Slide 69

Slide 69 text

Xcode 9 supports Swift Static Library https://download.developer.apple.com/Developer_Tools/Xcode_9_beta_4/ Xcode_9_beta_4_Release_Notes.pdf

Slide 70

Slide 70 text

Recap • Automate daily workflows with CI and Fastlane • Reduce workload of code review with Danger • Visualize code metrics you want to improve • Revising project settings may improve build time dramatically • Code modularization improves build/index time and helps you get well structured code/teams

Slide 71

Slide 71 text

The presentation is available on
 https://speakerdeck.com/yuseinishiyama/working-at-scale

Slide 72

Slide 72 text

Thank you! Yusei Nishiyama @yuseinishiyama