golangci-lintで有効な全てのlinterの実行時間を計測する方法

この方法を使って30mのCIを5mにしました。

先に結論

  1. verboseオプションで実行時間Top10のstage一覧をみる
  2. コード改変して有効な全ての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は並行実行に対応しているので時間が大きく出ていますね。

before.png

ここまで分かればあとは対象の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

after.png

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, ", "))
}

誰がここまで見たいねん!と思う気もしますが、気が迷った時に覗いてみてください🙆

img.png

終わりに

色々書きましたがgolangci-lintの基本は以下だと思っています。

GOGCを調整したり並行数を変えたり細かいところを変える前に、ルールとキャッシュの見直しをお勧めします。

それでは、楽しいlintライフを!