golangci-lintで有効な全てのlinterの実行時間を計測する方法
この方法を使って30mのCIを5mにしました。
先に結論
- verboseオプションで実行時間Top10のstage一覧をみる
- コード改変して有効な全てのlinterの実行時間を計測する
1. verboseオプションで実行時間Top10のstage一覧をみる
有名な方法ですね。 パフォーマンス起因の調査の場合、多くのケースでこの方法で事足りる気がします。
実行時--verbose
もしくは-v
をつけることでより詳細な実行結果が閲覧できます。
golangci-lint run --verbose
cf. https://golangci-lint.run/usage/configuration/#command-line-options
この出力結果の中に、実行時間がかかっているstageのTop10が出ます。 以下画像だとgosecが最も重いLinterですね。次点でgocritic。
CI自体の時間は30mですが、golangci-lintは並行実行に対応しているので時間が大きく出ていますね。
ここまで分かればあとは対象のlinterのうちどのルールが遅いのかを調べて改善するだけです!
今回の例だと運良くgosecのG602が遅いということが見つけられたので、これをignoreすることでCIの時間を30mから5mまで短縮することができました。(すごい!)
linters-settings:
gosec:
excludes:
- G602
cf. https://github.com/golangci/golangci-lint/issues/4039 cf. https://github.com/securego/gosec/issues/1007
gosecのパフォーマンスの話は結構前のものなのでもう治ってるかもですね。 けど、遅いlinterを特定する手順自体は参考になるんじゃないんでしょうか。
2. コード改変して有効な全てのlinterの実行時間を計測する
さて、ここからは物好きな人向けですね。
verboseオプションをつけて実行した時に実行時間Top10が出てくる仕組みはどこにあるんでしょうか?
答えはpkg/goanalysis/runners.goあたりで、固定値10を引数に実行時間の出力を予約してそうなコードが見つかります。
const stagesToPrint = 10
defer sw.PrintTopStages(stagesToPrint)
そして実際にstageを実行時間順にsortした後出力するのはpkg/timeutils/stopwatch.goです。
引数のnがstagesToPrint=10
ですね。
func (s *Stopwatch) sprintTopStages(n int) string {}
if len(s.stages) == 0 {
return noStagesText
}
stageDurations := s.stageDurationsSorted()
var stagesStrings []string
for i := 0; i < len(stageDurations) && i < n; i++ {
s := stageDurations[i]
stagesStrings = append(stagesStrings, fmt.Sprintf("%s: %s", s.name, s.d))
}
return fmt.Sprintf("top %d stages: %s", n, strings.Join(stagesStrings, ", "))
}
そうなんです、ここまでくればもうおわかりですね。
golangci-lintのコードを落としてきて以下のようにコードを変更した後、make build
をすれば全ての有効なstageの実行時間を出力させるgolangci-lintバイナリを作ることができるんです👏
func (s *Stopwatch) sprintTopStages(n int) string {
if len(s.stages) == 0 {
return noStagesText
}
stageDurations := s.stageDurationsSorted()
// ==================== 以下デバッグ
type stageDurationDetail struct {
name string
ms int
}
details := make([]stageDurationDetail, 0, len(stageDurations))
for _, v := range stageDurations {
details = append(details, stageDurationDetail{
name: v.name,
ms: int(v.d.Milliseconds()),
})
}
panic(fmt.Sprintf("%+v", details))
// ==================== デバッグおしまい
var stagesStrings []string
for i := 0; i < len(stageDurations) && i < n; i++ {
s := stageDurations[i]
stagesStrings = append(stagesStrings, fmt.Sprintf("%s: %s", s.name, s.d))
}
return fmt.Sprintf("top %d stages: %s", n, strings.Join(stagesStrings, ", "))
}
誰がここまで見たいねん!と思う気もしますが、気が迷った時に覗いてみてください🙆
終わりに
色々書きましたがgolangci-lintの基本は以下だと思っています。
- disable-allして必要なものだけenableする
- 適宜結果をキャッシュする(ローカルをdocker起動している場合はGOCACHE/GOLANGCI_LINT_CACHEの設定に注意)
GOGCを調整したり並行数を変えたり細かいところを変える前に、ルールとキャッシュの見直しをお勧めします。
それでは、楽しいlintライフを!