Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18239] ファイルの概要

このコミットは、Go言語のランタイムプロファイリングツールであるruntime/pprofパッケージ内のテスト、具体的にはTestCPUProfileMultithreadedテストの不安定性(flakiness)を修正するものです。テストが過度に敏感であり、CPUプロファイルのサンプル収集に関するアサーションが頻繁に失敗するという問題に対処しています。

コミット

commit cefe6ac9a1914864c66b8b3044c3e4755d309f80
Author: Keith Randall <khr@golang.org>
Date:   Mon Jan 13 21:18:47 2014 -0800

    runtime/pprof: fix flaky TestCPUProfileMultithreaded test
    
    It's too sensitive.
    
    Fixes bug 7095
    
    R=golang-codereviews, iant, minux.ma, rsc
    CC=golang-codereviews
    https://golang.org/cl/50470043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/cefe6ac9a1914864c66b8b3044c3e4755d309f80

元コミット内容

runtime/pprof: fix flaky TestCPUProfileMultithreaded test

It's too sensitive.

Fixes bug 7095

R=golang-codereviews, iant, minux.ma, rsc
CC=golang-codereviews
https://golang.org/cl/50470043

変更の背景

このコミットの背景には、Go言語のCPUプロファイリング機能のテストにおける信頼性の問題がありました。具体的には、TestCPUProfileMultithreadedというテストが「flaky(不安定)」であると報告されていました。これは、テストが実行されるたびに、成功したり失敗したりする可能性があり、コードの実際のバグではなく、テスト自体の設計や環境の変動に起因する問題であることを示唆しています。

元のテストでは、CPUプロファイルで収集されたサンプル数が、特定の関数に期待される最小値(total / len(have) / 3)を下回らないことを検証していました。しかし、この閾値が厳しすぎたため、プロファイリングの性質上発生しうるわずかな変動によってテストが失敗していました。CPUプロファイリングはサンプリングベースで行われるため、厳密なサンプル数を期待することは難しく、特にマルチスレッド環境ではスケジューリングのタイミングなどによって結果が変動しやすい特性があります。

この不安定性は、開発者がコードの変更が実際に問題を修正したのか、それとも単にテストがたまたま成功しただけなのかを判断するのを困難にし、開発プロセスを妨げる要因となっていました。そのため、テストの信頼性を向上させ、真のバグを検出できるようにするために、この修正が必要とされました。

関連するバグ報告であるbug 7095は、Go 1.12および1.13で発生した、プログラムカウンタ(PC)が不適切にデクリメントされる「off-by-one」エラーに関連するものでした。これは、スタックトレースが正しくない原因となり、プロファイリングの精度に影響を与える可能性がありました。このコミットは、直接的にそのバグを修正するものではなく、そのバグによって引き起こされる可能性のあるテストの不安定性に対処するためのものです。

前提知識の解説

CPUプロファイリング

CPUプロファイリングは、プログラムがCPU時間をどこで消費しているかを特定するためのパフォーマンス分析手法です。Go言語では、runtime/pprofパッケージを通じてCPUプロファイリング機能が提供されています。これは、一定の間隔(通常はミリ秒単位)でプログラムの実行を一時停止し、その時点でのコールスタック(どの関数がどの関数を呼び出しているかの連鎖)を記録する「サンプリング」という手法に基づいています。

収集されたサンプルは、各関数がCPU時間をどれだけ消費しているかを示す統計的なデータとして集計されます。これにより、プログラムの「ホットスポット」(最もCPUを消費している部分)を特定し、最適化の対象を絞り込むことができます。

サンプリングベースのプロファイリングの特性

サンプリングベースのプロファイリングは、オーバーヘッドが低いという利点がありますが、その性質上、結果には統計的な誤差が含まれます。特に、短時間で実行される関数や、非常に頻繁に呼び出されるが個々の実行時間が短い関数については、正確なサンプル数を取得するのが難しい場合があります。また、マルチスレッド環境では、OSのスケジューラがスレッドをどのように実行するかによって、サンプルの分布が変動する可能性があります。

Go言語のテストフレームワーク

Go言語には、標準ライブラリとしてtestingパッケージが提供されており、これを用いてユニットテストやベンチマークテストを記述します。テスト関数はTestで始まる名前を持ち、*testing.T型の引数を取ります。テストの失敗はt.Error()t.Fatalf()などのメソッドを呼び出すことで報告されます。

不安定なテスト (Flaky Tests)

「Flaky test」とは、同じコードに対して実行するたびに、成功したり失敗したりするテストのことです。これは、テストが外部要因(時間、ネットワークの状態、並行処理のタイミング、乱数など)に依存している場合や、アサーションが厳しすぎる場合に発生します。不安定なテストは、開発者の信頼を損ない、CI/CDパイプラインの効率を低下させるため、修正することが重要です。

技術的詳細

このコミットは、src/pkg/runtime/pprof/pprof_test.goファイル内のtestCPUProfile関数におけるCPUプロファイルのサンプル数に関するアサーションロジックを変更しています。

元のコードでは、各関数が収集したサンプル数(have[i])が、全体のサンプル数(total)と関数の種類数(len(have))に基づいて計算された最小値(min := total / uintptr(len(have)) / 3)以上であることを期待していました。このmin値は、各関数がCPU時間の少なくとも一定割合を消費していることを暗に仮定していました。

しかし、この計算式は、特にマルチスレッド環境でのプロファイリングの統計的性質を考慮すると、厳しすぎることが判明しました。プロファイリングのサンプリングは確率的なものであり、特定の関数が常に期待通りのサンプル数を取得するとは限りません。特に、テスト環境や実行時のわずかな変動が、この厳密な閾値を下回る原因となることがありました。

このコミットでは、minの計算方法を大幅に緩和しています。

変更前:

	min := total / uintptr(len(have)) / 3

変更後:

	// We'd like to check a reasonable minimum, like
	// total / len(have) / smallconstant, but this test is
	// pretty flaky (see bug 7095).  So we'll just test to
	// make sure we got at least one sample.
	min := uintptr(1)

この変更により、minの値はuintptr(1)、つまり「少なくとも1つのサンプルが収集されたこと」を意味するようになりました。これにより、テストは各関数がCPUプロファイルに全く現れないという極端なケースのみを失敗と判断するようになります。これは、テストの目的が「プロファイリングが機能し、少なくとも何らかのサンプルが収集されること」を確認することに絞られ、サンプル数の厳密な分布を検証するものではないという判断に基づいています。

この変更は、テストの不安定性を解消し、テストがより堅牢になることを目的としています。これにより、開発者はテストの失敗が実際のバグによるものであると信頼できるようになります。

コアとなるコードの変更箇所

src/pkg/runtime/pprof/pprof_test.goファイルの以下の部分が変更されました。

--- a/src/pkg/runtime/pprof/pprof_test.go
+++ b/src/pkg/runtime/pprof/pprof_test.go
@@ -142,7 +142,11 @@ func testCPUProfile(t *testing.T, need []string, f func()) {
 		t.Logf("no CPU profile samples collected")
 		ok = false
 	}
-	min := total / uintptr(len(have)) / 3
+	// We'd like to check a reasonable minimum, like
+	// total / len(have) / smallconstant, but this test is
+	// pretty flaky (see bug 7095).  So we'll just test to
+	// make sure we got at least one sample.
+	min := uintptr(1)
 	for i, name := range need {
 		if have[i] < min {
 			t.Logf("%s has %d samples out of %d, want at least %d, ideally %d", name, have[i], total, min, total/uintptr(len(have)))

コアとなるコードの解説

変更の中心は、min変数の初期化方法です。

  • 変更前: min := total / uintptr(len(have)) / 3

    • total: 収集されたCPUプロファイルの総サンプル数。
    • len(have): プロファイルされた関数の種類数。
    • 3: 定数。これにより、各関数が総サンプル数の少なくとも1/(3 * len(have))の割合を占めることを期待していました。
    • この計算は、各関数がCPU時間をある程度均等に消費することを前提としており、実際のプロファイリングの変動に対して厳しすぎました。
  • 変更後: min := uintptr(1)

    • uintptr(1): 最小値を1に設定しています。
    • この変更により、テストは「プロファイルされた各関数について、少なくとも1つのサンプルが収集されたこと」のみを検証するようになりました。
    • コメントで示されているように、これはテストがbug 7095によって不安定になっていたため、より現実的なアサーションに緩和されたものです。理想的にはより厳密なチェックを行いたいものの、テストの信頼性を優先した結果です。

この変更は、テストの堅牢性を高め、プロファイリングの統計的性質に起因する偽陽性(false positive)の失敗を減らすことを目的としています。これにより、テストが失敗した場合には、それが実際のバグを示している可能性が高まります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (pprofパッケージ): https://pkg.go.dev/runtime/pprof
  • Go言語のテストに関するドキュメント: https://go.dev/doc/code#testing
  • サンプリングプロファイリングに関する一般的な情報 (例: Wikipedia, 各種技術ブログ)
  • Go bug 7095に関するWeb検索結果 (例: https://luisgg.me/posts/go-bug-7095/)
    • このブログ記事は、bug 7095がGo 1.12および1.13で発生した、プログラムカウンタ(PC)の「off-by-one」エラーに関連するものであり、Go 1.14で修正されたことを示唆しています。これは、プロファイリングの精度に影響を与える可能性のある問題でした。