Docker を用いた AOSP Android10 のビルド時間の測定結果

## はじめに

[AOSP Android10](https://source.android.com/setup/start/android-10-release) 全体のビルド時間を測定したので、結果を共有します。
ビルド環境には Docker `ubuntu:18.04` を用いました。
ビルド時間短縮のポイントは、これまで AOSP のワークスペースに組み込まれてきた C/C++ の
コンパイラーキャッシュ `ccache` が [Android10 からはサポートされなくなった事](https://android.googlesource.com/platform/build/+/7556703f0dfab14c91c03dab1a3c25f6386268d4) を受けて、

> Our binary was rather old, and for a variety of reasons we haven't kept
it updated.

ビルド環境に `ccache` をインストールし、有効化する必要がありました。

> but if you still want to use ccache, continue setting USE_CCACHE
and also set CCACHE_EXEC to the path of your ccache executable.

## 実行結果

**クリーンビルド**

| Azure VM | ccache | ビルド時間（分）|
|---|---|---|
| D8v3<br>(vCPU=8/RAM=32GiB)| ccache なし | 245m |
| | **ccache あり** | **81m** |
| D16v3<br>(vCPU=16/RAM=64GiB)| ccache なし | 134m |
| | **ccache あり** | **49m** |
| D32v3<br>(vCPU=32/RAM=128GiB)| ccache なし | 68m |
| | **ccache あり** | **28m** |
![20200401_Clean_build.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32736/b7078985-6c0f-c4ed-4bb6-faf22f333fc7.png)

**AOSP Android10 インクリメンタルビルド**

| Azure VM | ccache | ビルド時間（分）|
|---|---|---|
| D8v3<br>(vCPU=8/RAM=32GiB)| ccache なし | 212m |
| | **ccache あり** | **33m** |
| D16v3<br>(vCPU=16/RAM=64GiB)| ccache なし | 114m |
| | **ccache あり** | **19m** |
| D32v3<br>(vCPU=32/RAM=128GiB)| ccache なし | 58m |
| | **ccache あり** | **10m** |
![20200401_Incremental_build.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32736/1864c8dd-d1be-c3a9-1490-cc7a0e3813b7.png)

**実行環境**

- [Azure VM](https://azure.microsoft.com/ja-jp/pricing/details/virtual-machines/linux)
- Docker `19.03.6`
  - ベースイメージは `ubuntu:18.04`
  - ccache `3.4.1`

## 環境構築

### ホストについて

ホスト OS は Ubuntu 18.04.4 LTS とします。また、Android のビルドには必要なパッケージが多いため、ホストに直接インストールする代わりに Docker を活用します。Ubuntu に Docker をインストールする手順やインストール後の便利な設定は、こちらをご確認ください。

- [Get Docker Engine >> Install using the repository](https://docs.docker.com/install/linux/docker-ce/ubuntu/#install-using-the-repository#install-using-the-repository)
  - SET UP THE REPOSITORY
  - INSTALL DOCKER ENGINE

- [Post-installation steps for Linux >> Manage Docker as a non-root user](https://docs.docker.com/install/linux/linux-postinstall/#manage-docker-as-a-non-root-user)
- [Post-installation steps for Linux >> Configure Docker to start on boot](https://docs.docker.com/install/linux/linux-postinstall/#configure-docker-to-start-on-boot)

### Dockerfile

AOSP がサポートしている（と言っても少々古い） [build/tools/docker](https://android.googlesource.com/platform/build/+/refs/tags/android-10.0.0_r14/tools/docker) を参考にしながら、 
ベースイメージ `ubuntu:18.04` で Android10 がビルドできるように少し修正を加えました。

``` Docker:Dockerfile
FROM ubuntu:18.04
ARG userid
ARG groupid
ARG username

RUN apt-get update && apt-get install -y \
    git-core \
    gnupg \
    flex \
    bison \
    gperf \
    build-essential \
    zip \
    curl \
    zlib1g-dev \
    gcc-multilib \
    g++-multilib \
    libc6-dev-i386 \
    lib32ncurses5-dev \
    x11proto-core-dev \
    libx11-dev \
    lib32z-dev \
    ccache \
    libgl1-mesa-dev \
    libxml2-utils \
    xsltproc \
    unzip \
    python \
    python3-minimal \
    rsync \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

RUN curl -o /usr/local/bin/repo https://storage.googleapis.com/git-repo-downloads/repo \
 && chmod a+x /usr/local/bin/repo

RUN groupadd -g $groupid $username \
 && useradd -m -u $userid -g $groupid $username \
 && echo $username >/root/username \
 && echo "export USER="$username >>/home/$username/.gitconfig
COPY gitconfig /home/$username/.gitconfig
RUN chown $userid:$groupid /home/$username/.gitconfig
ENV HOME=/home/$username

ENTRYPOINT chroot --userspec=$(cat /root/username):$(cat /root/username) / /bin/bash -i
```

[元 Dockerfile](https://android.googlesource.com/platform/build/+/refs/tags/android-10.0.0_r14/tools/docker/Dockerfile) との差分をハイライトすると、次のようになります。

```diff:build/tools/docker/Dockerfileとの差分
-FROM ubuntu:14.04
+FROM ubuntu:18.04

RUN apt-get update && apt-get install -y \
-    openjdk-7-jdk
+    python3-minimal \
+    rsync \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*


- RUN curl -o jdk8.tgz https://android.googlesource.com/platform/prebuilts/jdk/jdk8/+archive/master.tar.gz \
- && tar -zxf jdk8.tgz linux-x86 \
- && mv linux-x86 /usr/lib/jvm/java-8-openjdk-amd64 \
- && rm -rf jdk8.tgz

RUN curl -o /usr/local/bin/repo https://storage.googleapis.com/git-repo-download
- && echo "e147f0392686c40cfd7d5e6f332c6ee74c4eab4d24e2694b3b0a0c037bf51dc5  /usr/local/bin/repo" | sha256sum --strict -c - \
```

修正箇所について補足します。

- `openjdk-7-jdk / jdk8.tgz` 削除
    - [Software requirements >> JDK](https://source.android.com/setup/build/requirements#jdk) によると、ビルド済 OpenJDK は既にワークスペースに組み込まれているため、古いバージョンの Android のために必要な JDK は削除しました
- `python3-minimal` 追加
  - `python3.x` がないと *AssertionError: Could not find python binary: python3* とエラーになるため追加しました
- `rsync` 追加
  - `rsync` がないと *FAILED: out/target/product/generic/ramdisk-debug.img* とエラーになるため追加しました（ベースイメージ `ubuntu:18.04` に含まれていませんでした）
- `repo` のチェックサムの確認の省略
  - 元コードでは `repo` の正真性をハッシュ値で確認していましたが `e147f039` は古いため削除しました
  - その代わりに現時点の repo `2.4.1` のハッシュ値 `d2e17c4b` としても良かったのですが [git-repo](https://gerrit.googlesource.com/git-repo/+log) を読む限り、ここ最近頻繁に更新されていることを受け、今回はチェックそのものをやめておきます

ただし、十分にメンテナンスされていないように見える AOSP の [Dockerfile](https://android.googlesource.com/platform/build/+/refs/tags/android-10.0.0_r14/tools/docker/Dockerfile) を起点として作業をしているため、他にも既に不要なパッケージが含まれているかもしれません。今後 AOSP から新しい世代向けの Dockerfile が公開されたら、そちらに移行した方が無難です。

### Docker イメ―ジのビルド

[build/tools/docker/README.md](https://android.googlesource.com/platform/build/+/refs/tags/android-10.0.0_r14/tools/docker/README.md) を参考に Docker イメージをビルドします。

```bash
# Copy your host gitconfig, or create a stripped down version
$ cp ~/.gitconfig gitconfig
$ docker build --build-arg userid=$(id -u) --build-arg groupid=$(id -g) --build-arg username=$(id -un) -t android-build-bionic .
```

ビルドされたイメージを確認します。

```bash
$ docker image list
REPOSITORY             TAG                 IMAGE ID            CREATED              SIZE
android-build-bionic   latest              ff2cb9986cd7        About a minute ago   612MB
```

## ソースコード取得

今回は [Android10 Release revision 14](https://android.googlesource.com/platform/manifest/+/refs/tags/android-10.0.0_r14) を題材としてビルド時間の測定することにします。

- [Downloading the Source >> Installing Repo](https://source.android.com/setup/build/downloading?hl=en#installing-repo)
- [Downloading the Source >> Initializing a Repo client](https://source.android.com/setup/build/downloading?hl=en#initializing-a-repo-client)

``` bash:
# /mnt/work/aosp をワークスペースとします
$ sudo chown -R $USER:$USER /mnt
$ mkdir -p /mnt/work/aosp
$ cd /mnt/work/aosp
$ repo init -u https://android.googlesource.com/platform/manifest -b android-10.0.0_r14 
```

- [Downloading the Source >> Downloading the Android source tree](https://source.android.com/setup/build/downloading?hl=en#getting-the-files)

``` bash:
$ time repo sync -j8

repo sync has finished successfully.

real	51m0.159s
user	133m47.445s
sys 	13m13.264s
```

構築されたワークスぺースを確認します。

``` bash:
$ repo list | wc -l 
745

$ du -sh . 
90G	. 
```

このように、`745` もの Git リポジトリからワークスペースが構成され、ビルド前にも関わらず `90GB` もディスクを使用していることが確認できました。相変わらず巨大なプロジェクトです。

## ビルド

さて、いよいよビルドします。
[build/tools/docker/README.md](https://android.googlesource.com/platform/build/+/refs/tags/android-10.0.0_r14/tools/docker/README.md) を参考に、少し手直ししながら Docker コンテナを起動します。

```bash:docker-run.sh
# Docker 用のキャッシュディレクトリを用意します
$ mkdir -p /mnt/.cache /mnt/.ccache

$ docker run -it --rm \
    -v /mnt/work/aosp:/src \
    -v /mnt/.cache:/mnt/.cache \
    -v /mnt/.ccache:/mnt/.ccache \
    -e XDG_CACHE_HOME=/mnt/.cache \ # Docker 用キャッシュディレクトリを参照します
    -e CCACHE_DIR=/mnt/.ccache \ # Docker 用キャッシュディレクトリを参照します
    -e CCACHE_COMPRESS=1 \ # キャッシュサイズの圧縮のため
    -e CCACHE_EXEC=/usr/bin/ccache \ # 後述
    -e USE_CCACHE=1 \ # 後述
    android-build-bionic
```

[こちら](https://android.googlesource.com/platform/build/+/7556703f0dfab14c91c03dab1a3c25f6386268d4%5E%21/#F0) のコミットログによると、`ccache` を有効化するためには
 `-e USE_CCACHE=1` と `-e CCACHE_EXEC=/usr/bin/ccache` の設定が必要でした。

> So if you still want to use ccache, continue setting USE_CCACHE, but also set
> the CCACHE_EXEC environment variable to the path to your ccache executable.

これでビルド用 Docker コンテナが起動できたはずです。
続いて `ccache` の有無やビルド方式の違いについて比較していきます。
なお、以降のコマンドはすべて Azure D8v3 をホストとした Docker コンテナ上で実行します。

### クリーンビルド

中間生成物やビルド成果物を削除して、イチからビルドする方式となります。
リリースビルドの場合は、こちらになると思います。
コンフィギュレーションやビルドターゲットについては以下の公式手順を参考にしています。

- [Building Android >> Set up environment](https://source.android.com/setup/build/building?hl=en#initialize)
- [Building Android >> Building the code](https://source.android.com/setup/build/building?hl=en#build-the-code)

#### クリーンビルド（ccache なし）

```bash:Docker
$ cd src
$ rm -rf out

$ source build/envsetup.sh
$ lunch aosp_arm-userdebug
$ unset USE_CCACHE # 実験的に ccache を無効化するため
$ time m

#### build completed successfully (04:05:56 (hh:mm:ss)) ####
real	245m55.990s
user	1790m50.629s
sys 	97m4.830s
```

#### クリーンビルド（ccache あり）

キャッシュが保存されていない環境では、以下のビルド手順を **2 回** 実行して計測する必要があります。（ 1 回目のビルドはキャッシュを蓄えるため ）

```bash:Docker
$ cd src
$ rm -rf out
$ ccache -z # キャッシュ統計情報のクリア

$ source build/envsetup.sh
$ lunch aosp_arm-userdebug
$ time m

#### build completed successfully (01:21:53 (hh:mm:ss)) ####
real	81m52.620s
user	557m28.541s
sys 	39m36.687s
```

`ccache` が効果的に作用したことで **ビルド時間が約 1/3** に短縮されました。:tada:
この状態で、キャッシュのヒット数やヒット率などの統計情報を見てみます。

```bash:ccache_統計情報の表示
$ ccache -s

cache directory                     /mnt/.ccache
primary config                      /mnt/.ccache/ccache.conf
secondary config      (readonly)    /etc/ccache.conf
stats zero time                     Thu Feb 27 00:55:12 2020
cache hit (direct)                 33652
cache hit (preprocessed)             325
cache miss                             0
cache hit rate                    100.00 %
called for link                       77
called for preprocessing              18
unsupported code directive             2
cleanups performed                     0
files in cache                    101488
cache size                           3.9 GB
max cache size                       5.0 GB
```

期待通り `cache hit rate 100.00 %` となっていることが確認できました。

> `cache hit (direct) 33652`

このヒット数が妥当な数値であるかについては、ビルド対象となった C/C++ ファイルの数を調べるなどして、検証する必要があると感じています。（今回は調べ切れていません）

### インクリメンタルビルド

インクリメンタルビルド（差分ビルド）は、依存関係が正しく書かれたビルドシステムを前提として、変更されたファイルとその依存ファイルがビルドされる方式です。したがって、通常クリーンビルドと比べて処理数が少なくなるため、その分高速になります。開発者がローカル環境でビルドするときは、多くの場合がこちらになると思います。

**何も更新されていない場合**

```bash:Docker
$ cd src
$ ccache -z

$ source build/envsetup.sh
$ lunch aosp_arm-userdebug
$ time m

#### build completed successfully (5 seconds) ####
real	0m5.080s
user	0m11.956s
sys 	0m3.644s
```

Android のビルドシステムは依存関係の記述が正しくメンテナンスされているため、差分がまったくない状態でインクリメンタルビルドをすると、あっという間に完了します。なお、このケースではコンパイルもされないため期待通り `cache hit rate 0.00 %` のままになっています。

#### インクリメンタルビルド（ccache なし）

さて、今度はソースコードの更新をシミュレーションするため AOSP の一部のファイルのタイムスタンプを更新します。それによって変更されたファイルとその依存ファイルの再ビルドが実行されます。

```bash:Docker
$ cd src
$ ccache -z
$ touch external/protobuf/src/google/protobuf/*

$ source build/envsetup.sh
$ lunch aosp_arm-userdebug
$ unset USE_CCACHE # 実験的に ccache を無効化するため
$ time m

#### build completed successfully (03:32:32 (hh:mm:ss)) ####
real	212m31.772s
user	1586m36.813s
sys 	69m56.492s
```

前回の [クリーンビルド（ccache なし）](#クリーンビルドccache-なし)と比べて、わずかに短縮されていたものの、かなり時間がかかってしまいました。

#### インクリメンタルビルド（ccache あり）

```bash:Docker
$ cd src
$ ccache -z
$ touch external/protobuf/src/google/protobuf/*

$ source build/envsetup.sh
$ lunch aosp_arm-userdebug
$ time m

#### build completed successfully (33:53 (mm:ss)) ####
real	33m53.000s
user	237m48.218s
sys 	10m31.486s
```

インクリメンタルビルドのおいてもやはり `ccache` は効果的で、無効時と比べて **ビルド時間が約 1/6 ~ 1/7** に短縮されました。ただし、どのファイルが更新されたかで依存関係やビルド対象となるファイル数は大きく変わるため、これはあくまで一例となります。この状態で、キャッシュのヒット数やヒット率などの統計情報を見てみます。

```bash:キャッシュ統計情報の表示
$ ccache -s

cache directory                     /mnt/.ccache
primary config                      /mnt/.ccache/ccache.conf
secondary config      (readonly)    /etc/ccache.conf
stats zero time                     Thu Feb 27 04:28:46 2020
cache hit (direct)                  1737
cache hit (preprocessed)              35
cache miss                             0
cache hit rate                    100.00 %
called for link                        8
cleanups performed                     0
files in cache                    101503
cache size                           3.9 GB
max cache size                       5.0 GB
```

今回も期待通り `cache hit rate 100.00 %` となっていることが確認出来ました。

> cache hit (direct) 1737

[クリーンビルド（ccache あり）](#クリーンビルドccache-あり)の時と比べて、再コンパイルの必要なファイルが少ないため、このようにヒット数自体が下がることは妥当だと思います。

## おわりに

今回は Docker を用いた AOSP Android10 のビルド方法とビルド時間の測定結果についてまとめました。今後、更にビルド時間を短縮化するために、

- 更に高パフォーマンスなホストを試してみる
- より新しい https://ccache.dev (例えば `3.7+`) を試してみる

などのチャレンジをしてみても面白いと思います。

**補足資料**

ベンチマークが Android 4.x なので少し古い資料になりますが、Core 数や ccache と Android のビルド時間の関係について調査しているので、こちらも参考になると思います。
[Accelerating Android Builds](https://events.static.linuxfound.org/images/stories/slides/abs2013_melski.pdf)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32736/772c407e-eb13-e5de-0bda-5026e8a18d21.png" width=45%> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32736/fe0725a0-1106-a4de-9dad-e46458569031.png" width=45%>