Cookpad TechConf 2017 開催報告

f:id:Yoshiori:20170121154607j:plain

こんにちは! @yoshiori です。

先週の土曜日の 2017/01/21、技術系カンファレンスCookpad TechConf 2017を開催しました。
第二回となる今年は、ライブ配信・Wi-Fi 設置などの新しい取り組みやサプライズ発表など盛りだくさんでお届けしました。

この記事では色々なリンクをまとめたいと思います。

発表資料と動画

  1. Cookpad under a microscope by 成田 一生
  2. Go Global by 滝口 健太郎
  3. Building infrastructure for our global service by sorah
  4. サービス開発におけるデザインの取り組み方 by 若月 啓聡
  5. モバイルアプリのABテスト by 加藤 龍
  6. チームでプロダクト開発をするための取り組み by 丸山 亮
  7. よりよい検索体験の為の情報設計とプロトタイピング by 須藤 耕平
  8. 組織全体でGitHubを使うようになるまで by 長 俊祐
  9. 快適なサービス開発を支える技術 by 国分 崇志
  10. Real World Machine Learning by 染谷 悠一郎
  11. 行動ログでプロダクトを改善するには by 兼山 元太
  12. Cookpad awakens by 庄司 嘉織
  13. Spot Instances in Cookpad by 荒井 良太

記事やブログ等

笹田耕一 入社について

「Go Global」 の発表内容について

「組織全体でGitHubを使うようになるまで」の発表内容について

参加者の方々の感想

togetter

* 僕が見つけたのを集めただけなので漏れがあるかもしれません><(新しい記事など見つけたら@yoshiori まで連絡いただければと思います)

最後に

今年もなんとか無事に開催することが出来ました。
お楽しみいただけていたら幸いです。
引き続き技術的な挑戦をドンドンして、来年の Cookpad TechConf 2018 で発表できるようにするつもりです!来年もぜひお楽しみに!!!

Cookpad TechConf 2017 はライブ配信も行います!

f:id:Yoshiori:20170118132915j:plain

こんにちは! @yoshiori です。

こちらで告知した「Cookpad TechConf 2017」 ですが、ライブ配信することが決定しました!!!

残念ながら抽選から漏れてしまった方々、エントリーに間に合わなかった方々、また、今この記事を見て知った方々、ご安心ください!

僕自身も色々なカンファレンスに参加、もしくはライブ配信でみたりしますが、Twitter などのハッシュタグを追いかけつつ見るのが一番楽しいと思っています。ですので、会場に来られない方も是非、ライブ配信で楽しんでいただければと思います。
ちなみにハッシュタグは #CookpadTechConf になります!

そして、肝心のライブ配信 URLはこちらです!
https://techconf.cookpad.com/2017/streaming

残り一週間を切って僕もドキドキしていますがどうぞお楽しみに!!! では!

Swiftプロジェクトのビルド時間を計測・改善するxcprofilerを作った話

技術部モバイル基盤グループの@です。

我々のチームでは、iOS/Androidアプリの認証、決済、ロギングと言った基幹部分の開発のほか、各事業部のモバイルエンジニアの開発効率を上げるための業務改善を日々行っています。

その一環として、さまざまなモバイル開発上の指標を収集・監視し、問題の発見や、施策への効果計測に利用できるようにしています。 例として、iOS/AndroidのCIの実行時間や、開発期間中のissueの量の変化、コード全体のSwift対応率などがあります。

f:id:gigi-net:20161228111203p:plain

f:id:gigi-net:20161228111221p:plain

収集したデータは、オープンソースのデータビジュアライゼーションツールであるGrafana上にダッシュボードを作成し、監視しています。

この記事では、iOS版クックパッドアプリでビルド時間を計測、改善をした事例についてご紹介します。

コマンドごとの実行時間の計測

まず、CIサーバーで実行されている各Shellコマンドについて、コマンドごとの実行時間を調べ、合計時間の経過を監視するようにしました。

しかし、コマンドごとに見てみると、この計測方法では、xcodebuildに大部分の時間がかかっていることが判明し、これだけでは具体的な改善が難しい状況でした。

f:id:gigi-net:20161228111238p:plain

ビルド時間の内訳について詳細に把握する方法はないのでしょうか。

コンパイラフラグによるビルド時間の計測

この問題はXcode標準のデバッグ機能を利用することで解決します。

Xcodeプロジェクトに以下のコンパイラフラグを追加すると、それぞれのメソッドについてのビルド時間を計測してくれるようになります。

-Xfrontend -debug-time-function-bodies

f:id:gigi-net:20161228111139p:plain

しかし、結果はプレーンテキストとして出力され、閲覧、収集のしづらいデータフォーマットでした。

また、このログを整形して表示してくれるGUIツールはありましたが、CI上で実行することができず、運用が難しい問題もありました。

xcprofiler

そこで、Swiftコードのビルド時間をメソッド単位で計測して様々な形式で出力するユーティリティ「xcprofiler」を開発しました。

xcprofilerはRubyGemsから導入できます。

gem install xcprofiler

xcprofilerは最新のビルドログを自動で取得し、整形して表示してくれるCLIを提供します。

例えば、以下はCookpadのiOSアプリで実行した例です。

$ xcprofiler Cookpad -l 10
+-------------------------------------------------------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
| File                                                  | Line | Method name                                                                                                                                                   | Time(ms) |
+-------------------------------------------------------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.swift              | 9    | final didSet {}                                                                                                                                               | 5003.8   |
| xxxxxxxxx.swift                                       | 44   | init(title: String?, message: String?, content: Content?, actions: [AlertAction])                                                                             | 143.8    |
| xxxxxxxxxxxxxxxxxxxxxxxxx.swift                       | 11   | func cellHeightFromConstraints<CellType : UITableViewCell>(tableView: UITableView, createFromNib: Bool = default, cellUpdater: ((CellType) -> ())) -> CGFloat | 117.0    |
| xxxxxxxxxxxxxxxxxxxxx.swift                           | 55   | @objc func myFolderDidRemoveRecipeNotification(_ notification: Notification)                                                                                  | 115.9    |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxx.swift                    | 136  | private func assets(in rects: [CGRect]) -> [PHAsset]?                                                                                                         | 115.4    |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.swift | 4    | private static func makeInconsistentNotificationSettingAlert() -> AlertViewController                                                                         | 114.0    |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.swift           | 5    | @objc(showFromViewController:completion:) static func show(from viewController: UIViewController, completion: @escaping (Bool) -> Void)                       | 107.9    |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.swift                 | 8    | @objc override init(frame: CGRect)                                                                                                                            | 102.5    |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.swift                 | 31   | private final func configureAppearance()                                                                                                                      | 102.1    |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.swift           | 7    | private static func makeAlertController() -> AlertViewController                                                                                              | 102.1    |
+-------------------------------------------------------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+

内部では、Xcodeの出力したビルドログを取得、デコードし、ビルド時間についての出力のみを抽出しています。

カスタムレポーターを実装して、メトリクスを監視する

xcprofilerにはReporterという仕組みがあり、独自のReporterを実装することで、解析結果を様々な方法で出力することができます。

今回は、実行結果をGrafana上で閲覧するために、データソースであるInfluxDB向けのReporterを実装し、最新の実行結果をテーブルにしてみました。

#!/usr/bin/env ruby
require 'xcprofiler'
require 'influxdb'
include Xcprofiler

INFLUX_DB_TABLE_NAME = 'table_name'

class InfluxDBReporter < AbstractReporter
  def report!(executions)
    client ||= InfluxDB::Client.new(
      ENV['INFLUXDB_DB_NAME'],
      host: ENV['INFLUXDB_HOST'],
      port: ENV['INFLUXDB_PORT'],
      username: ENV['INFLUXDB_USERNAME'],
      password: ENV['INFLUXDB_PASSWORD'],
    )

    payload = executions.map { |e|
      key = "#{e.filename}:#{e.line}:#{e.column} #{e.method_name}"
      Hash[*[key, e.time]]
    }.reduce(&:merge)
    client.write_point(INFLUX_DB_TABLE_NAME, {values: payload})
  end
end

profiler = Profiler.by_product_name('Cookpad')
profiler.reporters = [
  StandardOutputReporter.new(limit: 20, order: :time),
  InfluxDBReporter.new(limit: 20),
]
profiler.report!

このようなスクリプトを実装し、CI上から実行すれば、最新のビルド時間をGrafana上で定常的に監視することができます。

f:id:gigi-net:20161228111315p:plain

Swiftのビルド時間を高速化する

では実際に、これらのデータを元にSwiftコードのビルド時間をカイゼンしてみましょう。

一番遅いメソッドは、ビルドに5003ms、約5秒もの時間がかかっています。 この値はCPU時間で、実時間ではないのでしょうが、他のメソッドと比べると特異的に遅いヶ所であることがわかります。

該当箇所のコードを実際に見てみましょう。

// 日付ラベルを表示するために、日付の区切りとなる写真のインデックスを求める
// (例) [photo(10月30日), photo(10月29日), photo(10月29日), photo(10月28日), photo(10月28日), photo(10月27日)]
// という配列の場合、日付の区切りとなるインデックスは [0, 1, 3, 5]
let indicesShowDateLabel = [0] + photos.reduce([]) { (photosGroupedByDate, photo) -> [[Photo]] in
    var result = photosGroupedByDate
    if let previousPhotoDate = photosGroupedByDate.last?.last?.cookedDate, calendar.isDate(photo.date, inSameDayAs: previousPhotoDate) {
        result[result.count - 1].append(photo)
    } else {
        result.append([photo])
    }
    return result
}
.map { $0.count }
.reduce([]) { (result, element) -> [Int] in
    return result + [(result.last ?? 0) + element]
}
.dropLast()

このビルド時間の遅さは、このように大量のメソッドチェイニングが行われていることにより、型推論が複雑になっていることが原因だと推測できます。

試しに、メソッドチェイニングをやめ、適宜テンポラリな変数に格納し、型情報を与えてみました。

// 型を明示する
let groupedPhotos: [[Photo]] = photos.reduce([]) { (photosGroupedByDate, photo) -> [[Photo]] in
    var result = photosGroupedByDate
    if let previousPhotoDate = photosGroupedByDate.last?.last?.date, calendar.isDate(photo.date, inSameDayAs: previousPhotoDate) {
        result[result.count - 1].append(photo)
    } else {
        result.append([photo])
    }
    return result
}
let counts: [Int] = groupedPhotos.map { $0.count }
indicesShowDateLabel = [0] + counts.reduce([]) { (result, element) -> [Int] in
    return result + [(result.last ?? 0) + element]
}
.dropLast()

わずかこれだけの変更でビルド時間が5003msから152msに、 つまり 97% も短縮することができました。 この値は、複数の開発者が毎日アプリをビルドし続けることを考えると大きな差と言えます。

このように現在のSwiftコンパイラによる型推論は、特定の場合においてビルド時間に多大な影響を与えてしまうことが見て取れます。 コードの簡潔さを取るか、ビルド時間を取るかはトレードオフであり、今後の課題と言えそうです。

まとめ

今回開発したxcprofilerは、どのプロジェクトでも簡単に利用でき、ワンショットで実行してみるほか、定常的な監視にも利用することができます。 ぜひご自分のプロジェクトに導入してみてください。Pull Requestもお待ちしております。

クックパッドのモバイル基盤グループでは、開発者のための問題解決が好きなモバイルエンジニアを募集しています。

iOS/Android アプリエンジニア | クックパッド株式会社 採用情報

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/