継続的にベンチマークを取るための GitHub Action をつくった
今年9月に GitHub Action v2 がリリースされました.GitHub Action は GitHub が提供する CI/CD サービスです.
既存のサービスと大きく違う点は,処理を汎用的に Action として切り出して再利用できることです.
例えば,GitHub からのリポジトリのクローン actions/fetch
や
Node.js のセットアップ actions/setup-node
などの基本的な実行ステップも
Action として実装されています.
今回はこの GitHub Action を利用して,前々からあると良いなと思っていたベンチマークを継続的に取るための Action をつくりました.
github-action-benchmark はベンチマークの実行の出力からベンチマーク結果を抽出し,GitHub pages のブランチに JSON で保存します. 保存された JSON は GitHub pages でグラフチャートで確認できます.
実際にベンチマークを取って表示しているデモは下記の URL で確認できます:
https://rhysd.github.io/github-action-benchmark/dev/bench/
ベンチマークツールの出力からベンチマーク結果を抽出するところでツール固有のロジックが必要で,現在は下記のツールに対応しています.
cargo bench
(Rust)go test -bench
(Go)- benchmark.js (JavaScript/TypeScript)
解決したい問題
アプリやライブラリの実行パフォーマンスを見るための手段としてベンチマークがあります. ベンチマークの結果は最適化の際に効果を検証するのに使ったり,開発の中で意図せずパフォーマンスがデグレしていないことを確認したりするために使うのが一般的だと思います. ここでは後者にフォーカスを当てます.
それなりにメジャーな言語には大抵ベンチマークを取るための公式ライブラリやデファクトなライブラリがあります. それらのツールはベンチマークを実行して結果を表示したり,指定したリビジョン間での比較を行ったりはできますが,開発の中で継続的に計測結果をモニタするような仕組みは提供してくれません.
ですが,意図しないパフォーマンスのデグレを防ぐには継続的にベンチマーク結果をチェックする必要があります. それを GitHub 上のリポジトリに絞って行うための Action が github-action-benchmark です.
使い方
Examples
リポジトリ内に Rust, Go, JavaScript のそれぞれの実際に動くデモを置いてあります.これらのデモは実際に github-action-benchmark のリポジトリで CI の一部として実行されています.
- Rust:
- Go:
- JavaScript:
これらのワークフローを実行して収集されたログが github-action-benchmark の GitHub pages のページにホストされています.
github-action-benchmark は複数のベンチマーク結果を1箇所に集められるため,複数の言語で開発されているリポジトリや monorepo な構成のリポジトリでも問題なく使えます.
余談ですが,全てのプロジェクトでフィボナッチ数を求めるベンチマークを回していて,各言語のフィボナッチ数計算に対する実行パフォーマンスが何となく分かりますね.この超簡単な例では Rust が一番速く,Go は Rust に比べて1.6倍ほど遅く,Node.js は Go に比べて2倍ほど遅いです.
ワークフローの書き方
上記の Examples のワークフロー設定(YAML)を真似すれば問題ないと思いますが,一応ワークフローの書き方を説明します.どの言語でもほぼ変わらないので,ここでは Go プロジェクトを仮定します.
まずはベンチマークの出力を取得するステップを追加します.github-action-benchmark はベンチマーク出力を含むテキストファイルを入力とするので,tee
コマンドなどを使ってベンチマーク結果をログに出しつつ保存します.
- name: Run benchmark run: go test -bench 'Benchmark' | tee output.txt
ここで得た output.txt
を使って github-action-benchmark を実行します.
- name: Store benchmark result to gh-pages uses: rhysd/github-action-benchmark@v1 with: name: My Project Go Benchmark tool: 'go' output-file-path: output.txt
ここで,uses: rhysd/github-action-benchmark@v1
はリポジトリ https://github.com/rhysd/github-action-benchmark の v1
をチェックアウトして使いますという宣言です.github-action-benchmark では v1
ブランチに v1.x.y の最新のバージョンをデプロイしているので,それがチェックアウトされます.もしバージョンを固定したければ v1.0.2
のようなタグも提供しています.
with:
のセクションはこのアクションへの入力です.
name
はベンチマークの名前です.複数のベンチマークを取る場合はこの値がリポジトリ内で一意になるようにしてください. 指定しない場合のデフォルト値は"Benhcmark"
です.tool
はどのツールを使ってベンチマーク出力を取得したかを示すものです."cargo"
,"go"
,"benchmarkjs"
の中のいずれかの値を指定する必要があります.output-file-path
はベンチマークの出力が保存されているファイルへのパスを指定します.リポジトリからの相対パスで記述できます.
これ以外にも,入力や出力をカスタマイズするためにいくつかの入力が定義されています.詳しくは下記の README の表を参照してください.
https://github.com/rhysd/github-action-benchmark#action-inputs
github-action-benchmark を実行すると,gh-pages
ブランチの dev/bench/
以下に結果が JavaScript ファイル data.js
として出力され,(もし既に無ければ)同じディレクトリにグラフチャートを見るための index.html
が生成され,それらを含むコミットが作られます.
ブランチや生成場所のディレクトリパスは上記の with:
に書く入力で変えられます.
最後にブランチをリモートに push します.github-action-benchmark がコミットの生成まで行い push をしないのは,Action は(GitHub API トークンを入力で受け取るなどしない限り)リモートに push する権限を持っていないからです. ここでは安全側に倒して github-action-benchmark 内では push しない設計にしました.
追記(2019/11/11 20:28): どうやら Action は現在 private リポジトリのみで GitHub pages をデプロイすることが可能なようです.いただいた情報によるとこれは GitHub Action 側の問題らしいので,いずれ解消されて GitHub pages への push は github-action-benchmark 側でできるようになるかもしれません(thanks to @pris314 さん)
GitHub Action ではワークフローの実行で GitHub API トークンを自動で発行してくれるので,push するのは簡単です.
- name: Push benchmark result run: git push 'https://you:${{ secrets.GITHUB_TOKEN }}@github.com/you/repo-name.git' gh-pages:gh-pages
トークンは secrets に保存されているのでそれを展開して git push
します.もちろん実行ログでは secrets の展開部分は隠されます.
ちなみに複数のベンチマークを複数ワークフローで並列で実行している場合は,他のワークフローによって GitHub pages のブランチが更新されていて直接 push できない場合があるので,push 前に git pull --rebase
などを挟んでください.
また,github-push-action を使う手もあります(push は自前でやるのがおすすめですが).
- name: Push changes uses: ad-m/github-push-action@master with: branch: gh-pages github_token: ${{ secrets.GITHUB_TOKEN }}
gh-pages
ブランチがリモートにまだ無い場合は作成しておいてください.
$ git checkout -b --orphan gh-pages $ git commit -m 'first commit' --allow-empty $ git push origin gh-pages
これでワークフローが走ったときにベンチマーク結果が gh-pages
内に保存され,https://you.github.io/repo-name/dev/bench
で確認できるようになります.
ページを開くとベンチマークのテストケースごとにグラフが表示されます.縦軸がベンチマークの値,横軸がコミットで,右に行くほど新しいコミットを示しています.これを確認することで,ベンチマーク結果の変化が確認できます.
グラフのデータポイント上にマウスカーソルを置くと
- コミットハッシュ
- コミットメッセージ
- コミットの作成日時とコミッター
- ベンチマークの値
が表示されます.
さらにここでクリックすると GitHub 上でそのコミットのページが開くので,そこでコードの変更内容を確認できます.
また,最下部にベンチマークデータをダウンロードできるボタンを置いてあるので,それをクリックするとベンチマーク結果を JSON としてダウンロードできます.
もし GitHub pages のブランチにデフォルトで生成される index.html
が気に入らなければ,好きに変更したり自前で作ったものに差し替えて OK です.github-action-benchmark は既に index.html
がある場合は関与しませんし,データは全て data.js
側にあって window.BENCHMARK_DATA
に格納されています.
使用の注意点
上記で説明したとおり,github-action-benchmark はリポジトリ上のブランチを変更し,それをワークフロー側でリモートに push するユースケースを想定しています.
なので,pull request ではこの Action が実行されないように注意してください.万一悪意あるユーザが pull request を作成し,gh-pages に悪意ある変更を加える処理を入れて pull request を投げると,それが反映されてしまいます.
ワークフローの指定で master
のみで実行するように制限するか,
on: push: branches: - master
ジョブの実行ステップに記述できる if:
セクションで pull request で gh-pages
ブランチを push するステップを実行しないようにガードしてください.if:
の書き方は公式ドキュメントを読むと分かります:
GitHub Action 実行環境の注意
そもそも GitHub Action の実行環境がベンチマークに適しているのか?という疑問が残っていると思います.
ずっと同じ内容の単純なベンチマークを実行しているデモページでは大体 10~20% のブレがどのベンチマークでも出ています. そのブレを許容できない場合は使えませんし,許容できるなら使えます.そこはプロジェクト次第だと思います.また,あまり無いと思いますが,ネットワークなどのリソースが絡むともっとブレ幅が大きくなる可能性があります.
安定した環境を自前で用意し,self-hosted runner として使うのも手だと思います.
今後
- 以前のベンチマーク結果と比較して結果が極端に悪くなっている(e.g. 2倍以上遅くなっているなど)時にコミットへのコメントで通知する機能
- ベンチマークツールの出力フォーマットに JSON を追加し,ユーザがベンチマーク結果を JSON で用意することで,どんなベンチマークツールでも対応できるようにする
- ベンチマーク結果を GitHub pages にデプロイする代わりに Elasticsearch や Mackerel のようなサービスにアップロードする機能
- GitHub pages のページとして出力する以外に生の JSON データを生成するオプションを追加する.ベンチマーク結果だけ JSON で欲しくて,後の処理は自前でやりたいときに便利
などがアイデアとしてありますが,今のところ機能的に自分のユースケースでは満足しているため,あまり追加の予定はありません. もし何かアイデアがあれば,issue や pull request で提案していただくのは歓迎です.
感想
GitHub Action のための Action を初めて作成しました.kiro-editor のためにつくったので,早速活用していきたいと思います.
ちなみに JavaScript Action として,TypeScript で作ってみました.GitHub Action 自体まだリリースされたところで情報がほとんどありませんが,実行は速くバグにも逢わなかったので,手探りでもあまり困ったことはありませんでした.微妙にハマったポイントもありましたが,それは別エントリで書こうかなと思います.