読者です 読者をやめる 読者になる 読者になる

OpenSTFでAndroidのCIを2倍早くする

はじめまして!技術部モバイル基盤グループの加藤(@k0matatsu)です。
業務の一部でCIお兄さんとしてJenkins氏のメンテナンスなどを行っています。

今日はf:id:komatatsu:20160815174157j:plainf:id:komatatsu:20160815174215j:plainにする話をしたいと思います。
CI待ち時間1/2で PR/レビューのサイクルの速さ2倍(当社比)です。

※ ビルド所要時間のボトルネックは環境やジョブ内容によって異なるため効果には個人差があります。

当社のAndroid CI環境

さて、開発効率を2倍にする前に、まずは当社のCI環境がどうなっているか説明が必要ですね。
当社のAndroid向けCI環境は幾つかの試行錯誤を経て、現在はAmazon Web Service(AWS)を使って構成されています。
下図のように、Amazon EC2(EC2)インスタンス上に構成管理ツール:itamaeを使って作成されたJenkinsのmaster/slave構成を擁し、その中でAndroid向けSlaveが2台稼働しています。

f:id:komatatsu:20160815173846j:plain

Jenkins Slave上では、Instrumentation Testの実行にはAndroid Emulator PluginとARMエミュレーターを使いテストを実行しています。
EC2上ではハードウェア仮想化が出来ないためIntel x86 Emulation Acceleratorが利用できず、 またAndroid Emulator Pluginによるエミュレーターの起動にも時間が掛かっており、エミュレーターの起動とInstrumentation Testのエミュレーター上での実行時間がボトルネックになっていました。

現構成での解決策と問題点

そのため、今ではテストをSmall/Medium/Large/Enormousの4サイズに分け、そのサイズごとに実行タイミングを変えることで開発サイクルとCIの流れを最適化しました。
テストのサイズ分けについて、詳しくは先日のAndroid/iOSアプリのテストの区分戦略をご参照ください。
その中から、今回はCIに関係の深いSmall/Medium/Largeのみ言及します。

サイズ 実行されるタイミング 内容
Small プルリクエスト毎 JVM Test
Medium プルリクエストのmasterへのマージ時 Instrumentation Test
Large 任意 Espresso Test

テストサイズにより実施するテストを分けたことによってプルリクエスト時にはエミュレーターを起動しなくて良くなったため、テストにかかる時間が大幅に短縮されテスト以外の処理と合わせても5min/pr程度に収まる様になりました。

ですが、マージの度に実行するInstrumentation Testには今まで通りの時間がかかります。 このため、マージが頻繁に発生する場合はキューが詰まるという問題が発生し、Slaveを追加してジョブの並列度をあげることで全体のスループットをあげる方法(別名:札束で殴る)で解決せざるを得ない状況でした。
しかしこの方法は根本的なエミュレーターの準備と利用に時間がかかるという課題を解決できていないので1つのslaveによる実行時間が短くなることはなく、1回のビルドにかかる時間以上に開発効率が上がらないという限界があります。
この問題を根本から解決するにはARMエミュレーターを捨てるほかありませんでした。

OpenSTFの登場、そしてビルド所要時間1/2へ...

そんな折に燦然と姿を現したのはOpenSTFでした。 OpenSTFはブラウザからAndroidの実機を遠隔操作することが出来るオープンソースのツールです。 f:id:komatatsu:20160815173900j:plain

端末操作だけでなく、ログの取得やブラウザ経由のアプリのインストールなど様々な機能を持っており主に検証用途で利用されています。 当社でも試験的に導入しており、誰も使ってない検証用端末を繋いで簡単な動作確認などはOpenSTF上で出来るように環境構築していました。
OpenSTFの空き端末を使ってテストを実行出来るようになればこの問題を解決できるんじゃないですか?私はそう思いました。

OpenSTFでテストを走らせる

OpenSTFでテストを実行するためには、まず端末を利用状態にして確保する必要があります。 その後、確保した端末の接続用のIPとPortを取得し、adb connectする事でインターネット越しにテストを実行することが可能になります。 f:id:komatatsu:20160815173918j:plain

最新のversion2.0.1ではAPI機能が開放されており、APIを使って端末を利用状態にしたり、adb connectに必要な情報が取得出来るようになっています。

さっそく簡単なスクリプトを書いて挑みます。

DEVICE_SERIAL=`ruby stf.rb device | sed -e 's/"//g'`
DEVICE_INFO=`ruby stf.rb connect ${DEVICE_SERIAL} | sed -e 's/"//g'`
IP_PORT=(`echo $DEVICE_INFO | tr -s ':' ' '`)
export ADBHOST=${IP_PORT[0]}
export ANDROID_ADB_SERVER_PORT=${IP_PORT[1]}
adb tcpip ${IP_PORT[1]}
adb connect ${DEVICE_INFO}
adb devices

例に出てくるstf.rbはOpenSTFのAPIを叩くための簡単なRubyスクリプトです。APIドキュメントの一部をラップしています。
stf.rb deviceはAPIを叩いて端末を利用状態にし、その識別文字を取得する処理です。
同じく、stf.rb connectは上記の処理で取得した識別文字を使って接続に必要なIPとPortを取得します。

ネットワーク越しに端末に接続するadb connectを使うには、環境変数ADBHOSTANDROID_ADB_SERVER_PORTにそれぞれIPとPortを設定して置く必要があります。
その上で上記のようにadb tcpip {Port}でadb-serverのPortを設定し、adb connect {IP:Port}とすることで接続することができます。

また、初めて接続する際にはOpenSTF側にADBキーの設定が必要になります。 OpenSTFを開いた状態で上記のスクリプトを実行すると下図の様にADBキーの保存を促す画面が出ますので忘れずに保存しておきましょう。 f:id:komatatsu:20160815173602j:plain

ここまでで準備は完了です。
あとは思うがまま、Jenkins Slaveから./gradlew connectedAndroidTestしましょう!

まとめ

このように、OpenSTF連携を行うことでエミュレーターにより実行時間がかかる問題が解消し当社CIのビルド所要時間は半分になりました。
今回はエミュレーターと起動時間と、エミュレーター上でのInstrumentation Testの実行時間が主なボトルネックとなっていた為、エミュレーターを捨てOpenSTF上の実機を使うことで課題を解決しました。
同様にエミュレーターがボトルネックになっているケースでは有効な対策だと思います。

CIのメンテナはJenkinsおじさんなどと揶揄され、つらそうというイメージを持つ方も少なくないかもしれません。
ですが僕はCI待ち時間が半分になったのを確認した瞬間に最高の達成感を味わう事が出来ましたし、日々開発を頑張っている他のメンバーの待ち時間を減らすことが出来たのは誇りでもあります。
このように開発者の役にたっている事が実感できる基盤業務に興味がある仲間を募集しています。
めざせ、5min切り!
ありがとうございました。

謝辞

OpenSTFの開発は以下のお三方とコントリビューターの方々によって支えられています。
sorccuさん
guntaさん
vbanthiaさん
コントリビューターの皆さん
ありがとうございました。

/* */ @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;*/ /*}*/