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

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

このコミットは、Go言語の標準テストパッケージtestingにおいて、t.Parallel()を使用して並列実行されるテストの計測開始タイミングを修正するものです。具体的には、テストが実際に並列実行を開始するまでタイマーが開始されないように変更することで、テスト時間の計測精度を向上させています。

コミット

  • コミットハッシュ: 6fb9cc1f63dced4ad2022fb4eac9f722cd12c708
  • Author: Rob Pike r@golang.org
  • Date: Mon Aug 19 10:15:30 2013 +1000

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

https://github.com/golang/go/commit/6fb9cc1f63dced4ad2022fb4eac9f722cd12c708

元コミット内容

testing: don't start timing a Parallel test until it's actually starting
Fixes #5285.

R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/13045044

変更の背景

Goのテストフレームワークでは、testing.T.Parallel()を呼び出すことで、テスト関数を並列で実行させることができます。これにより、テストスイート全体の実行時間を短縮することが可能です。しかし、このコミットが修正しようとしている問題は、t.Parallel()が呼び出された際のテスト時間の計測開始タイミングに関するものでした。

以前の実装では、テストの実行が開始されるとすぐにタイマーが開始されていました。しかし、t.Parallel()が呼び出されると、そのテストはすぐに実行されるわけではありません。テストランナーは、t.Parallel()を呼び出したすべてのテストを一時停止させ、シリアルに実行されるべきテストがすべて完了した後、それらの並列テストをまとめて並行して実行します。この「待機時間」の間もタイマーが動いていたため、テストが実際に並列実行を開始するまでの時間が計測に含まれてしまい、テストの実際の実行時間よりも長く報告されるという不正確な問題がありました。

この不正確な計測は、特にテストのパフォーマンスを評価したり、ボトルネックを特定したりする際に誤解を招く可能性がありました。そのため、テストが実際に並列実行を開始する時点から時間を計測するように修正する必要がありました。

前提知識の解説

Goのテストフレームワーク (testingパッケージ)

Go言語には、標準ライブラリとしてtestingパッケージが提供されており、これを使ってユニットテストやベンチマークテストを記述します。

  • go testコマンド: Goのテストを実行するためのコマンドです。プロジェクトのルートディレクトリでgo testを実行すると、_test.goで終わるファイル内のテスト関数が自動的に発見され、実行されます。
  • testing.T: テスト関数(func TestXxx(t *testing.T))に渡される構造体で、テストの実行制御、エラー報告、ログ出力など、テストに関する様々な機能を提供します。
  • t.Parallel()メソッド: testing.T型が持つメソッドの一つで、テスト関数内でこのメソッドを呼び出すと、そのテストは他の並列テストと一緒に並行して実行されるようになります。
    • t.Parallel()が呼び出されると、テストランナーは現在のテストの実行を一時停止します。
    • すべてのシリアルテストが完了し、並列実行可能なテストが十分に集まると、テストランナーはそれらのテストを同時に実行し始めます。
    • t.Parallel()より前のコードはシリアルに実行され、t.Parallel()より後のコードは並列に実行されます。この特性から、t.Parallel()はテスト関数の冒頭で呼び出すことが推奨されます。

テスト時間の計測

go testコマンドは、各テストの実行時間を報告します。この時間は、テストのパフォーマンスを把握し、リグレッションを検出するために重要です。正確な時間計測は、開発者がテストのボトルネックを特定し、最適化を行う上で不可欠です。

技術的詳細

このコミットの技術的な核心は、testing.T構造体のstartフィールド(テスト開始時刻を記録するtime.Time型)の更新タイミングを変更することにあります。

Goのテストランナーは、tRunnerという内部関数を通じて個々のテストを実行します。以前の実装では、tRunnerがテスト関数の実行を開始する直前(test.F(t)が呼び出される前)にt.start = time.Now()を設定していました。

// 変更前 (tRunner関数内)
func tRunner(t *T, test *InternalTest) {
	t.start = time.Now() // ここでタイマーが開始される

	// ...
	test.F(t) // テスト関数が実行される
}

しかし、t.Parallel()が呼び出されると、テストはt.signal <- (*T)(nil)<-t.startParallelというチャネル操作によって一時停止します。この一時停止期間中もt.startは既に設定されているため、タイマーは動き続けていました。

このコミットでは、t.start = time.Now()の呼び出しを2箇所で変更しています。

  1. tRunner関数内: t.start = time.Now()の最初の設定を削除します。これにより、テストが開始された直後にはタイマーが開始されなくなります。
  2. t.Parallel()メソッド内: t.start = time.Now()<-t.startParallelの直後に追加します。<-t.startParallelは、テストが並列実行を開始する準備ができたことを示すシグナルを待つ部分です。したがって、この位置にタイマーの開始を移動することで、テストが実際に並列実行を開始する直前にタイマーがリセットされ、正確な計測が可能になります。
  3. tRunner関数内(再追加): test.F(t)の直前にもt.start = time.Now()を再追加します。これは、t.Parallel()が呼び出されないシリアルテストの場合に、テスト関数が実行される直前にタイマーが開始されることを保証するためです。

この変更により、t.Parallel()を呼び出すテストは、並列実行のキューで待機している間は計測対象外となり、実際に実行が開始された時点から時間が計測されるようになります。これにより、go testが報告するテスト時間が、テストの実際の実行時間をより正確に反映するようになります。

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

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -357,6 +357,9 @@ func (c *common) Skipped() bool {
 func (t *T) Parallel() {
 	t.signal <- (*T)(nil) // Release main testing loop
 	<-t.startParallel     // Wait for serial tests to finish
+	// Assuming Parallel is the first thing a test does, which is reasonable,
+	// reinitialize the test's start time because it's actually starting now.
+	t.start = time.Now()
 }
 
 // An internal type but exported because it is cross-package; part of the implementation
@@ -367,8 +370,6 @@ type InternalTest struct {
 }
 
 func tRunner(t *T, test *InternalTest) {
-	t.start = time.Now()
-
 	// When this goroutine is done, either because test.F(t)
 	// returned normally or because a test failure triggered
 	// a call to runtime.Goexit, record the duration and send
@@ -384,6 +385,7 @@ func tRunner(t *T, test *InternalTest) {
 		t.signal <- t
 	}()
 
+	t.start = time.Now()
 	test.F(t)
 }
 

コアとなるコードの解説

  1. func (t *T) Parallel()内の変更:

    func (t *T) Parallel() {
    	t.signal <- (*T)(nil) // Release main testing loop
    	<-t.startParallel     // Wait for serial tests to finish
    	// Assuming Parallel is the first thing a test does, which is reasonable,
    	// reinitialize the test's start time because it's actually starting now.
    	t.start = time.Now()
    }
    

    t.Parallel()が呼び出されると、テストはまずメインのテストループを解放し(t.signal <- (*T)(nil))、その後、並列実行が開始されるのを待ちます(<-t.startParallel)。この<-t.startParallelの行は、テストが実際に並列実行の準備ができたことを意味します。したがって、この直後にt.start = time.Now()を呼び出すことで、テストが待機状態から解放され、実行を開始する正確なタイミングでタイマーがリセットされるようになります。コメントにもあるように、「Parallelがテストが最初に行うことであると仮定すると、これは合理的であり、テストの開始時刻を再初期化する。なぜなら、実際に今から開始されるからだ。」という意図が明確に示されています。

  2. func tRunner(t *T, test *InternalTest)内の変更:

    -	t.start = time.Now()
    // ...
    +	t.start = time.Now()
    	test.F(t)
    

    tRunner関数は、すべてのテスト(シリアルテストと並列テストの両方)を実行する内部的なラッパー関数です。

    • 元のコードでは、tRunnerの冒頭でt.start = time.Now()が呼び出されていました。これは、t.Parallel()が呼び出されるテストの場合、並列実行の待機時間も計測に含まれてしまう原因となっていました。この行が削除されたことで、テストがtRunnerに入った時点ではタイマーが開始されなくなります。
    • 新しいコードでは、test.F(t)(実際のテスト関数)が呼び出される直前にt.start = time.Now()が再配置されています。この変更は、t.Parallel()を呼び出さないシリアルテストの場合に重要です。シリアルテストではt.Parallel()内のt.startの再初期化は行われないため、tRunner内でテスト関数が実行される直前にタイマーが開始されることで、正確な計測が保証されます。

これらの変更により、t.Parallel()を使用するテストは、並列実行の準備ができた時点から時間が計測され、シリアルテストはテスト関数が実行される直前から時間が計測されるようになり、テスト時間の報告がより正確になりました。

関連リンク

参考にした情報源リンク

  • GitHub上のコミットページ: https://github.com/golang/go/commit/6fb9cc1f63dced4ad2022fb4eac9f722cd12c708
  • testing.T.Parallel()に関する一般的な情報源(Goのテストにおける並列実行の仕組みとタイミング問題の一般的な解説として参照):
    • golang.orgのドキュメント(t.Parallel()の動作に関する説明)
    • Goのテストに関するコミュニティの議論(t.Parallel()使用時の注意点など)
    • (注:Web検索では、このコミットが修正した特定のIssue #5285に関する詳細な情報は見つかりませんでしたが、t.Parallel()の一般的な動作とそれに伴うタイミングの課題に関する情報は、変更の背景と前提知識の解説に役立ちました。)