現在、実験的にですが少しパフォーマンスを気にしたいパッケージを書いています。 ちなみに、その1つはリバースプロキシです。
「気にしたいパフォーマンス」というのは以下の2つです。
- 現在のGoの実装がNGINX(のリバースプロキシ機能)と比べてリクエストあたりの処理時間が小さいか
- 現在の実装が以前の実装(Pull Requestであればmainブランチ)と比べてリクエストあたりの処理時間が極端に大きくなっていないか
GitHubにリポジトリがあるのでCI環境としてはGitHub Actionsがあります。
そこで、上記2点をGitHub Actions上で継続的に計測したいと考えました。
まず、1つ目の「NGINXとの比較」ですが、これはただNGINXを使ったベンチマークと自分の実装を使ったベンチマークをそれぞれ実行することにしました。
NGINXを使ったベンチマークの結果(ns/op)と、同じようにGoの実装を使ったベンチマークの値(ns/op)を比較するだけです。
それぞれのベンチマークを go test -bench
で計測しoctocovのカスタムメトリクス機能を使って結果を表示します。
GitHub Actionsで提供されるRunnerのスペックはいつも同じとは限らない
2つ目の「以前の実装との比較」です。
当初はoctocovの比較機能を使い、GitHub Artifactsに保存してある以前のベンチマーク計測結果と比較するようにしたのですが、ベンチマーク結果が安定しませんでした。
原因はGitHub Actionsで提供されるRunnerのスペックがいつも同じとは限らないことでした。
このRunnerのスペックの違いを、CPU命令数とCPUキャッシュのヒット率を使って乗り越える方法を実現した方がいます。
私も手元ではうまくいったように見えたので、Cachegrindの結果をoctocovのカスタムメトリクスで取得できる仕組みまで作ったのですが、残念ながら期待した結果にはなりませんでした。おそらく、ベンチマーク対象のコードにCPU以外に大きく影響をうけるような箇所があるんだろうなと推測しています。
同じRunnerで実装前後のベンチマークを比較する
ようは、実装前後の2値の比較だけができればいいわけです。
ということで愚直に同一Job上で実装前後のベンチマークを取得し比較するようにしました。そうすれば少なくともRunnerのスペックの違いに悩まされることはなくなります。
actions/checkout と k1LoW/octocov-action の機能を駆使します。
作成したGitHub ActionsのWorkflowは次のとおりです。
# .github/workflows/benchmark.yml name: benchmark on: pull_request: jobs: benchmark: name: Benchmark runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Check out source code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version-file: go.mod - name: Set up octocov-go-test-bench run: go install github.com/k1LoW/octocov-go-test-bench/cmd/octocov-go-test-bench@latest - name: Check out source code (main) uses: actions/checkout@v4 with: ref: main path: main - name: Run benchmark (main) run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json working-directory: main - name: Run octocov (main) uses: k1LoW/octocov-action@v0 with: config: .octocov.benchmark.main.yml env: OCTOCOV_GITHUB_REF: refs/heads/main OCTOCOV_GITHUB_SHA: none OCTOCOV_CUSTOM_METRICS_BENCHMARK: main/custom_metrics_benchmark.json - name: Run benchmark run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json - name: Run octocov uses: k1LoW/octocov-action@v0 with: config: .octocov.benchmark.yml env: OCTOCOV_CUSTOM_METRICS_BENCHMARK: custom_metrics_benchmark.json
順を追って説明します。
まず、Check out source code
Set up Go
Set up octocov-go-test-bench
ステップまでは通常通りですが、Check out source code (main)
でmainブランチのソースコードを mainディレクトリにチェックアウトします。
- name: Check out source code (main) uses: actions/checkout@v4 with: ref: main path: main
次にmainブランチのソースコードでベンチマークを実行します。ベンチマーク結果を octocov-go-test-bench でパースしてoctocovのカスタムメトリクスJSONとして、main/custom_metrics_benchmark.json
に保存します。
- name: Run benchmark (main) run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json working-directory: main
mainブランチのoctocovカスタムメトリクスJSONを使ってoctocovでレポートを作成します。
- name: Run octocov (main) uses: k1LoW/octocov-action@v0 with: config: .octocov.benchmark.main.yml env: OCTOCOV_GITHUB_REF: refs/heads/main OCTOCOV_GITHUB_SHA: none OCTOCOV_CUSTOM_METRICS_BENCHMARK: main/custom_metrics_benchmark.json
mainブランチのカバレッジレポートであることを記録するために環境変数 GITHUB_REF
と GITHUB_SHA
を上書きします。GitHub Actions上では直接 GITHUB_REF
と GITHUB_SHA
を上書きすることはできないので OCTOCOV_GITHUB_REF
と OCTOCOV_GITHUB_SHA
で上書きします*1。
使用する設定ファイル .octocov.benchmark.main.yml は次のように指定します。
# .octocov.benchmark.main.yml repository: ${GITHUB_REPOSITORY}/benchmark coverage: if: false codeToTestRatio: if: false testExecutionTime: if: false report: datastores: - local://main # レポートをローカルの main/ ディレクトリ以下に保存する
コードメトリクスの取得は全てOFFにし、レポートファイルをローカルディレクトリに保存するようにしています。
最後はPull Requestの実装のベンチマークを取得してoctocovにカスタムメトリクスとして読み込ませます。
- name: Run benchmark run: go test -bench . -benchmem -benchtime 10000x | octocov-go-test-bench --tee > custom_metrics_benchmark.json - name: Run octocov uses: k1LoW/octocov-action@v0 with: config: .octocov.benchmark.yml env: OCTOCOV_CUSTOM_METRICS_BENCHMARK: custom_metrics_benchmark.json
ここで比較対象として、先ほどローカルの保存したmainブランチのoctocovレポートを指定します。
.octocov.benchmark.yml は次のようになります。
# .octocov.benchmark.yaml repository: ${GITHUB_REPOSITORY}/benchmark coverage: if: false codeToTestRatio: if: false testExecutionTime: if: false diff: datastores: - local://main # 比較対象としてローカルの main/ ディレクトリ以下に保存されているレポートを使用する comment: if: is_pull_request summary: if: true
実際のレポート結果
- Goの実装とNGINX(のリバースプロキシ機能)との比較
- 現在の実装と以前の実装(Pull Requestであればmainブランチ)との比較
なのでちょうど次のようなマトリクスになります。
以前の実装(main) | Pull Requestの実装 | |
---|---|---|
NGINX | ||
Goの実装 |
そして、今回説明したWorkflowをPull Requestで実行すると以下のようになります。
Metadataを展開するとちゃんと実行環境のCPUが揃っていることがわかります。
まとめ
今回、2つの要件を満たすためのGitHub Actions上での安定したベンチマーク環境を得る方法を紹介しました。
ただ、残念ながら継続的なベンチマーク環境とはいかず、あくまで相対比較専用となっています。
それでも、Pull Requestごとに数値で確認できるのは大きなメリットです。
個人的には、Cachegrindの手法(もしくはそれに準じる手法)が「GitHub Actions上での安定した継続的ベンチマーク環境」を得る方法だと思っているので、折を見て再挑戦したいと思っています。