fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

github actions でやっているcommit checkの処理を早く終わらせたかった

3行で

  • 平行に処理してみたりキャッシュが効くようにしたが早くはならなかった
  • やってみたかったのでやった
  • 後悔はしていない

その時のPR: github.com

チェックを並列に実行するが効果なし(むしろ増)

これまでのコミットチェックではAndroidのLintとktlint、UnitTestをstepで順番に行っていた。高速化と言ったら並列化だろ、ってことで、PRのタイトルにもした通りまずはこれらのstepを分割してjobに昇格し、平行に実行させてみた。ついでに、共通で行うコンパイルの処理を前段に、レポートの処理を後段で行うようにしたらちょっとしたビルドパイプラインになった。

github actionsでは、jobsの下に列挙したjobはすべて平行に実行される。指定したjobの終了を待ってから開始したいjobはその設定でjobs.needs: <<前段のjob-id>> を書いておく。前段のjob-id[ ]で囲んで複数指定できる。今回のパイプラインはjobだけ書くと次のようになる。

- jobs
  - compile
  - lint
    - needs: compile
  - ktlint
    - needs: compile
  - unittest
    - needs: compile
  - report
    - needs: [lint, ktlint, unittest]

あとはそれぞれのjobでGradleのキャッシュをリストアする処理などが行われるように設定していく。キャッシュのリストアのオーバーヘッドはあるだろうけどまあちょっとぐらいは早くなるでしょという期待を胸にワークフローを走らせてみたところ、これまで15分程度だった実行時間が20分ぐらいかかるようになってしまった。どうやらキャッシュのリストアが長すぎるというのと、ビルドキャッシュが効いていないというのが原因のようだったのでこれの対策を行うことにした。

キャッシュをちゃんとやる

キャッシュは2GB程度あり、リストアには2分程度かかっていた。これを前段でも中段でもやっているのでどうにかして節約したい。キャッシュにはactions/cacheを使っていて、当初はマニュアルのGradleの例に書いてる設定をほぼそのまま流用していた。しかし、この設定だとキーが変わったりキャッシュを意識的に消す処理をしない限りこれまで使っていたバージョンのwrapperやらcachesのなんやかやが全てキャッシュに残ったままになってしまい *1 、雪だるま式にサイズが大きくなってしまうのではないかと考えた。そこで、キャッシュサイズを小さくしつつ、かつサイズが必要以上に大きくなりすぎないようにするための方法を考えることにした。

現時点でのGradle 6.8.3において、私の環境では次のディレクトリにキャッシュが保存される様子。

  • ~/.gradle
    • caches
    • wrapper
  • <<プロジェクトルート>>/.gradle

このうち、wrapperディレクトリの下にはgradle wrapperのバイナリがバージョンごとに分かれて入っている。これがバージョンごとに200~300MB程度あり、実際に使うのは1つだけなのでそれ以外は不要である。まずはここを削るために次のようにした。

      - name: cache gradle wrapper
        uses: actions/cache@v2
        with:
          path: ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}

使用するgradle wrapperのバージョンはgradle-wrapper.propertiesに書いてあるものを使うのでこのファイルのハッシュをキーに使うことにして、このキーでキャッシュが見つからない時は代わりのキャッシュをリストアしないようrestore-keysを指定しないようにした。

あとはライブラリのキャッシュやビルドキャッシュを保存する部分だが、ここはあっさりと、

      - name: cache gradle
        uses: actions/cache@v2
        with:
          path: |
            ~/.gradle/caches
            ./.gradle
          key: ${{ runner.os }}-gradle-cache-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-gradle-cache-
      - name: cache build
        uses: actions/cache@v2
        with:
          path: ./**/build
          key: ${{ runner.os }}-assemble-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-assemble-

という感じにしてみた。キーにはgithub.shaを付けてcommit checkごとに新しくなるようにした。ただし、~/.gradle/caches./.gradleの下にはバージョン番号のディレクトリがあり、Gradleのバージョンが変わるごとに増えていくと考えられるがこれを消す処理は入れていない。消そうとせずにここのキーにもgradle-wrapper.propertiesのハッシュをつけてもいいかもしれない。

この改善によって、2GB近くあったキャッシュが500MB程度になり、20分に膨れ上がった実行時間が17分にまで抑えられた。キャッシュのない初回の実行なので次回以降はもう少し短くなるはず。あとはmasterブランチにマージするワークフローでも同じようなこと*2をやってやれば次のPRの時にもこのキャッシュを引き継げる。

ふりかえり

  • 平行化した結果トータルで見ると実行時間が増えている(ターンアラウンドタイムも微増)ので今の規模なら素直にstepで直列に実行した方がよかったかもしれない。
  • 平行に実行したいタスクを今後増やしていけるようになったということにして個人的には納得した。
  • タスクではなくモジュールで分けてみてもよかったかもしれない。
  • github actionsはstepの設定を共通化できたらいいのにと思う。どんどんコピペしなきゃいけないんだけどyamlだからインデントの深さをうっかり間違えると怒られるので。

*1:自分のPCの~/.gradleの下を見てそう思った

*2:テストだけのシンプルなものでよい