knqyf263's blog

自分のためのメモとして残しておくためのブログ。

コミット前後でベンチマークが悪化していたらテストを落とすGoのCI用ツール

本当にただこれがやりたかっただけです。今でも既存のものがあるだろうと思っているのですが、誰も教えてくれなかったのでシュッと作りました。落ち込まないので今からでも教えてくれて良いです!

概要

上のツイートにある通りなのですが、Goだとベンチマークを計測するツールがデフォルトでgo testに同梱されているので、GitHubとかにコミットされたらそのコミットと一つ前のコミットでベンチマークのスコアを比較して、悪くなっていたら教えて欲しかっただけです。シェルスクリプトで数行で出来るようなレベルですし、ちょっとオプションつけたり表示をリッチにしても200行ぐらいで済みそうだったのでGoでツールを作りました。ブログ執筆時点だと260行でした。

f:id:knqyf263:20200114005936p:plain:w300

github.com

実際、あまりにも簡易的なツール過ぎてGoのコードよりインストールスクリプトのほうが行数が長く、GitHub上でShellScriptのリポジトリとして認識されてしまいました。テストを足してかさ増ししたりもしたのですが、最終的には.gitattributesを足してちょろまかしました。

使い方

基本的にはCI上で使うことを想定しています。内部でgit reset相当のことをしているので、ローカルで実行する場合は注意して下さい。 もしコードが消えても責任は取れないです。というか自分が開発中に間違って自分のリポジトリ上で実行してコードが消え去りました。内部でgo-gitを使っているのですが、こいつのresetはuntrackedなやつも消されるみたいです。まだgit addすらしてないから大丈夫だろうと油断していて完全にやられました。シンプルにコードを失いました。そもそもgo-gitのこの挙動がアレだと思うので直したい気持ちもあります。

とりあえず実行するならベンチマークのあるプロジェクトのディレクトリに行って以下のように打つだけです。これで勝手にHEADとHEAD{@1}のベンチマークを比較します。

$ cob ./...

メモリとかも計測したい場合は-benchmemつければよいです。使い方はgo testと同じです。go testのうちベンチマークに関係ありそうで自分が欲しかったオプションだけ取り込んでいます。-bench-benchmem-benchtimeはそのままgo testに渡されます。

$ cob -benchmem ./...

結果は以下のようになります。上のテーブルは単純にHEADとHEAD{@1}の値が並んでいます。下のテーブルはそれがどのぐらい変化したかを表示しています。赤は悪化で青は改善を示します。

f:id:knqyf263:20200113043030p:plain

デフォルトではベンチマークが20%以上悪化するとプログラムがエラーを返します。つまりCI上で実行するとテストが落ちます。ただ元々の値が小さい場合は変化が大きく出てしまうので、同じベンチマークを実行しても20%を超える場合があります。そういう場合は-threshold を変えたり -benchtime 10s など大きくして値が安定するようにすると良いです。

$ cob -benchtime 10s -threshold 0.5 ./...

ベンチマークの関数ごとに閾値を決められると良いかもなとは思っているのですが、-benchの正規表現指定とパッケージ指定とか工夫でなんとか出来る気がしたのでやめました。上に書いたように簡易的なツールなので機能は殆ど無いです。

$ cob -bench BenchAppend -threshold ./foo

上のような感じですね。BenchAppendという文字列を含むfooパッケージ内のベンチマークだけ実行されます。-benchgo testに渡してるだけなので、正規表現も使えます。

あと、パッケージが違えば同じベンチマークの関数名が定義できますが、それも対応してないです。同じ名前がある場合はうまく結果が表示されないのでパッケージ名を指定して2回実行して下さい。

$ cob ./foo
$ cob ./bar

オプションもあまりないです。

NAME:
   cob - Continuous Benchmark for Go Project

USAGE:
   cob [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --only-degression  Show only benchmarks with worse score (default: false)
   --threshold value  The program fails if the benchmark gets worse than the threshold (default: 0.1)
   --bench value      Run only those benchmarks matching a regular expression. (default: ".")
   --benchmem         Print memory allocation statistics for benchmarks. (default: false)
   --benchtime value  Run enough iterations of each benchmark to take t, specified as a time.Duration (for example, -benchtime 1h30s). (default: "1s")
   --help, -h         show help (default: false)

結果をコミットIDと保存しておいて比較すれば2回実行しなくても済むかなと思ったのですが、CI環境だと起動される度に微妙に環境が違ったりしてそっちの要因でベンチマークのスコアが変わったら嫌だなと思って今のところやっていません。その辺り、何か良い知見をお持ちの方がいらっしゃれば助けてほしいです。

CI

CIで使う場合のサンプルを置いておきます。ここではGitHub Actionsだけ置いておきますが、他の例や実際の出力を見たい場合はcob-exampleを見て下さい。

name: Bench
on: [push, pull_request]
jobs:
  test:
    name: Bench
    runs-on: ubuntu-latest
    steps:

    - name: Set up Go 1.13
      uses: actions/setup-go@v1
      with:
        go-version: 1.13
      id: go

    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Install GolangCI-Lint
      run: curl -sfL https://raw.githubusercontent.com/knqyf263/cob/master/install.sh | sudo sh -s -- -b /usr/local/bin

    - name: Run Benchmark
      run: cob -benchmem ./...

まとめ

みんな黙ってるだけでやっぱりこういうツールあるんじゃないかと疑っています。でも自分の要件はこれぐらいなので、必要最小限という意味では自作して良かったかもしれません。自分の環境以外でちゃんと動くかも分かりません。もしこういうの欲しくて困っていた人はフィードバック頂ければもう少し真面目にメンテナンスします。