[インデックス 11494] ファイルの概要
このコミットは、Go言語のテストスイート内にあるtest/map.go
ファイル内の特定のテストの堅牢性を向上させることを目的としています。具体的には、テストが実行される仮想マシン(VM)環境における時間分解能の低さに起因するテストの不安定性を解消するため、テストの実行時間を意図的に長くするように調整されています。
コミット
commit 0eb647e71c4b67db53181b1aff7755adafda0838
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Jan 30 20:17:34 2012 -0800
test: attempt at making a test more robust
A current theory is that this test is too fast for the
time resolution on the VMs where our builders run.
R=rsc
CC=golang-dev
https://golang.org/cl/5581056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0eb647e71c4b67db53181b1aff7755adafda0838
元コミット内容
test: attempt at making a test more robust
A current theory is that this test is too fast for the
time resolution on the VMs where our builders run.
R=rsc
CC=golang-dev
https://golang.org/cl/5581056
変更の背景
このコミットの背景には、Go言語のテストインフラストラクチャにおける特定のテストの不安定性がありました。コミットメッセージによると、「このテストは、我々のビルダが動作するVMの時間分解能に対して速すぎる」という仮説が立てられています。
ソフトウェア開発において、テストはコードの品質と信頼性を保証するために不可欠です。Go言語のような大規模なプロジェクトでは、継続的インテグレーション(CI)システムが利用され、コードの変更がコミットされるたびに自動的にテストが実行されます。これらのテストは、様々な環境(物理マシン、仮想マシン、異なるOSなど)で実行されることが一般的です。
問題となったのは、test/map.go
内の特定のテストが、一部の仮想マシン環境で不安定な結果を出すことでした。これは、テストが非常に短時間で完了するため、その実行時間を正確に測定するために必要なシステムの時間分解能が不足していることが原因であると推測されました。時間分解能が低い環境では、非常に短い時間間隔を正確に区別することが難しく、テストの実行時間が「0」と記録されたり、期待される線形性(例えば、処理量が2倍になれば時間も2倍になる)が観測されなかったりする可能性があります。これにより、テストが本来成功すべき状況で失敗する「flaky test(不安定なテスト)」となり、開発者の生産性を低下させ、CIパイプラインの信頼性を損なうことになります。
この問題を解決するため、テストの実行時間を意図的に長くすることで、時間分解能の低い環境でも測定が容易になり、テストがより安定して動作するようにするアプローチが取られました。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
- テストの堅牢性 (Test Robustness): テストが、実行環境のわずかな変動や非決定的な要素(例: スケジューリング、I/O遅延、時間分解能)によって結果が変わらないことを指します。堅牢なテストは、コードの変更によってのみ失敗し、環境要因では失敗しません。
- 時間分解能 (Time Resolution): コンピュータシステムが時間を測定できる最小の単位です。例えば、ミリ秒単位で時間を測定できるシステムは、マイクロ秒単位で測定できるシステムよりも時間分解能が低いと言えます。OSやハードウェア、仮想化技術によって時間分解能は異なります。
- 仮想マシン (Virtual Machine, VM): 物理的なコンピュータシステム上に構築された、ソフトウェアによってエミュレートされたコンピュータシステムです。VMは、ホストOSのリソース(CPU時間、メモリ、I/Oなど)を共有するため、物理マシンと比較して時間測定の精度が低下したり、非決定的な遅延が発生したりすることがあります。
- Go言語の
time
パッケージとtime.Since
: Go言語の標準ライブラリには、時間と日付を扱うためのtime
パッケージが含まれています。time.Since(t)
関数は、指定された時刻t
から現在までの経過時間をtime.Duration
型で返します。この関数は、ベンチマークやパフォーマンス測定によく使用されます。 - Go言語のビルダ (Go Builders): Goプロジェクトでは、様々なプラットフォームやアーキテクチャ向けにコードをビルドし、テストを実行するための自動化されたシステム(ビルダ)が運用されています。これらのビルダは、多くの場合、クラウド上の仮想マシンで動作します。
- Flaky Test (不安定なテスト): 同じコードに対して、成功したり失敗したりするテストのことです。これは、テストコード自体に非決定的な要素が含まれているか、テストが実行される環境に依存する問題がある場合に発生します。Flaky Testは、開発者がコードのバグと環境の問題を区別することを困難にし、CI/CDパイプラインの信頼性を損ないます。
技術的詳細
このコミットの技術的な詳細は、test/map.go
内のtestnan()
関数におけるn
という変数の値の変更に集約されます。
元のコードでは、n
の値は30000
に設定されており、これはMacBook Air上で約0.02秒の実行時間に対応するとコメントされていました。このn
は、t(n)
およびt(2*n)
という形で、ある操作の繰り返し回数を制御するために使用されています。t(n)
はn
回操作を実行し、その経過時間を返します。t(2*n)
は2*n
回操作を実行し、その経過時間を返します。
テストの目的は、操作回数と実行時間の間に線形関係があることを確認することです。つまり、操作回数が2倍になれば、実行時間も約2倍になるはずです(t2
が3*t1
を超えないこと)。しかし、テストが実行されるVM環境の時間分解能が低いため、n=30000
という比較的短い実行時間では、t1
やt2
の測定値が不正確になり、期待される線形性が観測されず、テストが不安定になっていました。例えば、t1
が非常に小さく測定されたり、t2
が期待値から大きく外れたりする可能性がありました。
この問題を解決するために、コミットではn
の値を30000
から60000
に倍増させています。これにより、テストの実行時間はMacBook Air上で約0.04秒に増加するとコメントされています。実行時間を倍にすることで、時間分解能の低いVM環境でも、time.Since
による測定がより正確になり、t1
とt2
の間の線形関係がより安定して観測されるようになります。
この変更は、テストのロジック自体を変更するものではなく、テストが依存する外部要因(時間測定の精度)に対する耐性を高めるための調整です。テストの実行時間を長くすることは、テストスイート全体の実行時間をわずかに増加させる可能性がありますが、不安定なテストによる開発プロセスの停滞を防ぐというメリットの方が大きいと判断されたと考えられます。
コアとなるコードの変更箇所
--- a/test/map.go
+++ b/test/map.go
@@ -667,7 +667,7 @@ func testnan() {
return time.Since(t0)
}
- n := 30000 // 0.02 seconds on a MacBook Air
+ n := 60000 // 0.04 seconds on a MacBook Air
t1 := t(n)
t2 := t(2 * n)
if t2 > 3*t1 { // should be 2x (linear); allow up to 3x
コアとなるコードの解説
変更されたのは、test/map.go
ファイル内のtestnan()
関数の一部です。
func testnan() {
// ... (前略) ...
// t は、与えられた回数だけ操作を実行し、その経過時間を返すヘルパー関数
t := func(n int) time.Duration {
t0 := time.Now()
for i := 0; i < n; i++ {
_ = map[float64]int{NaN: 0}[NaN] // NaNをキーとするマップ操作
}
return time.Since(t0)
}
// n の値を変更
// 元: n := 30000 // 0.02 seconds on a MacBook Air
n := 60000 // 0.04 seconds on a MacBook Air
t1 := t(n) // n回操作を実行した時間
t2 := t(2 * n) // 2*n回操作を実行した時間
// t2 が t1 の3倍を超えないことを確認(線形性をチェック)
// 理想的には2倍になるはずだが、許容範囲を3倍までとしている
if t2 > 3*t1 { // should be 2x (linear); allow up to 3x
// ... (後略) ...
}
}
このコードスニペットでは、n
という変数がループの繰り返し回数を決定しています。t
という匿名関数は、n
回特定のマップ操作(map[float64]int{NaN: 0}[NaN]
)を実行し、その処理にかかった時間をtime.Duration
として返します。
変更前はn
が30000
でしたが、変更後は60000
に倍増されました。この変更により、t(n)
およびt(2*n)
の実行時間がそれぞれ約2倍に伸びます。
テストの目的は、t2
(2*n
回の操作時間)がt1
(n
回の操作時間)の約2倍(最大3倍)であることを検証することです。これは、操作回数と実行時間が線形関係にあることを確認するためのものです。しかし、n
が小さすぎると、time.Since
で測定される時間が非常に短くなり、VM環境の時間分解能の制約により、正確な測定が困難になります。結果として、t1
やt2
の値が期待通りにならず、t2 > 3*t1
という条件が不適切に真となり、テストが失敗する可能性がありました。
n
を60000
に増やすことで、各測定の絶対時間が長くなり、時間分解能の低い環境でも測定誤差の影響が相対的に小さくなります。これにより、t1
とt2
の測定値がより安定し、テストが意図した線形性をより確実に検証できるようになります。
関連リンク
- Go CL 5581056: https://golang.org/cl/5581056
参考にした情報源リンク
- Go言語の
time
パッケージに関する公式ドキュメント: https://pkg.go.dev/time - Go言語のテストに関する公式ドキュメント: https://go.dev/doc/code#testing
- Flaky Testに関する一般的な情報(例: Martin Fowler's blog on Flaky Tests): https://martinfowler.com/articles/flakyTests.html (一般的な概念理解のため)
- 仮想化環境における時間測定の課題に関する情報 (一般的な概念理解のため、特定のリンクはなし)