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

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

このコミットは、Go言語の標準ライブラリtimeパッケージ内のテストコードsleep_test.goに対する変更です。具体的には、time.Sleeptime.Afterといった時間関連の関数が、非常に低速な仮想マシン(VM)環境下でもテストが失敗しないように、許容される時間のずれ(スロップ)を拡大する修正が行われています。

コミット

commit fc7b9fc26990e3a480f816e7c34d981488340c0c
Author: Russ Cox <rsc@golang.org>
Date:   Mon Dec 12 18:33:47 2011 -0500

    time: allow sleep tests to run for 200% too long
    
    Some VMs are slow. Very slow.
    Fixes #2421.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5482049

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

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

元コミット内容

このコミットは、timeパッケージのテストにおいて、time.Sleeptime.Afterなどの時間計測が、想定よりも大幅に遅延した場合でもテストが失敗しないように、許容範囲を広げるものです。特に「一部のVMは非常に遅い」という問題に対処しています。

変更の背景

この変更の背景には、Go言語のテストスイートが様々な実行環境、特に仮想マシン(VM)上で実行される際に、時間関連のテストが不安定になるという問題がありました。time.Sleeptime.Afterといった関数は、指定された期間だけ処理を停止したり、指定された期間後にイベントを発生させたりするものです。これらの関数の正確性は、テストによって検証されますが、VM環境ではホストOSのスケジューリング、VMのオーバーヘッド、I/Oの遅延など、様々な要因によって時間の精度が低下することがあります。

コミットメッセージにある「Some VMs are slow. Very slow.」という記述は、特定のVM環境下で、これらの時間関連の操作が期待される時間よりも大幅に長くかかることがあり、その結果、テストが「時間切れ」や「期待される時間内に完了しない」といった理由で失敗していたことを示唆しています。Fixes #2421という記述がありますが、このIssueは公開されているGoリポジトリでは見つかりませんでした。これは、内部のIssueトラッカーの番号であるか、非常に古いIssueである可能性があります。いずれにせよ、このコミットは、テストの信頼性を向上させ、不安定なVM環境下でもテストがパスするようにするための実用的な調整です。

前提知識の解説

Go言語のtimeパッケージ

Go言語の標準ライブラリtimeパッケージは、時間の測定、表示、および操作のための機能を提供します。主要な機能には以下のようなものがあります。

  • time.Duration: 時間の長さを表す型。ナノ秒単位で表現され、time.Second, time.Minuteなどの定数を使って人間が読みやすい形で時間を指定できます。
  • time.Now(): 現在の時刻をtime.Time型で返します。
  • time.Sleep(d Duration): 指定された期間dだけ現在のゴルーチンをスリープさせます。
  • time.After(d Duration): 指定された期間dが経過した後に、現在の時刻を送信するチャネルを返します。これは非同期的な時間イベントの待機によく使われます。
  • time.Tick(d Duration): 指定された期間dごとに現在の時刻を送信するチャネルを返します。定期的な処理に利用されます。
  • time.Time.Sub(t Time): 2つのtime.Time間の差をtime.Durationとして計算します。

テストにおける「スロップ(Slop)」

ソフトウェアテストにおいて、「スロップ」とは、許容される誤差範囲やずれを指します。特に時間に関連するテストでは、厳密な時間精度を期待することは難しいため、ある程度の誤差を許容する必要があります。例えば、「1秒スリープする」というテストでは、厳密に1.000000000秒でなくても、1.000000000秒±ε(イプシロン)の範囲内であれば成功とみなす、といった具合です。このεがスロップにあたります。

このコミットでは、このスロップの値を拡大することで、テストがより多くの環境でパスするように調整しています。

仮想マシン(VM)の特性

仮想マシンは、物理ハードウェア上にソフトウェア的に構築されたコンピュータシステムです。VMはホストOSのリソース(CPU、メモリ、I/Oなど)を共有して動作するため、以下のような特性があります。

  • オーバーヘッド: VMのハイパーバイザーやゲストOS自体がリソースを消費するため、物理マシンに比べてパフォーマンスが低下する可能性があります。
  • スケジューリングの不確実性: ホストOSが複数のVMや他のプロセスをスケジューリングするため、特定のVMに割り当てられるCPU時間やI/O処理のタイミングが不確実になることがあります。
  • クロックのずれ: VM内のシステムクロックがホストOSのクロックと完全に同期しない場合があり、時間の測定にずれが生じることがあります。

これらの特性により、VM環境では時間関連の操作が物理マシンよりも遅延したり、不安定になったりすることがよくあります。

技術的詳細

このコミットは、src/pkg/time/sleep_test.goファイル内の2つのテスト関数TestAfterTicktestAfterQueuingにおける時間計測の許容範囲を変更しています。

TestAfterTick関数における変更

このテストは、time.Tick関数が指定された間隔で正確に動作するかを検証します。元のコードでは、Delta * Countで計算される期待される合計時間targetに対して、target * 2 / 10(つまりtargetの20%)のスロップを許容していました。

変更後、このスロップの計算が以下のように変わりました。

  • 下限: target * 9 / 10 (期待時間の90%)
  • 上限: target * 30 / 10 (期待時間の300%、つまり3倍)

これにより、テストが完了するまでの時間が期待値の90%から300%の範囲内であれば、テストは成功とみなされます。特に上限がtarget * 2 / 10からtarget * 30 / 10へと大幅に拡大されており、これは「200% too long」(200%長すぎても許容)というコミットメッセージの意図と一致します。

testAfterQueuing関数における変更

このテストは、time.After関数が複数のイベントをキューイングした場合に、それぞれのイベントが正確なタイミングで発生するかを検証します。元のコードでは、Delta / 4Deltaの25%)のスロップを許容していました。

変更後、このスロップの計算が以下のように変わりました。

  • 下限: target - Delta / 2 (期待時間からDeltaの50%を引いた値)
  • 上限: target + Delta * 10 (期待時間にDeltaの10倍を加えた値)

これにより、イベントの発生時間が期待値からDeltaの50%手前から、Deltaの10倍遅れてもテストは成功とみなされます。こちらも上限が大幅に拡大されており、特に遅延に対する許容度が高められています。

これらの変更は、テストのロジック自体を変更するものではなく、テストが成功と判断するための閾値を調整するものです。これにより、低速なVM環境下での時間的なずれが原因でテストが不必要に失敗するのを防ぎ、テストの安定性を向上させています。

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

--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -119,8 +119,7 @@ func TestAfterTick(t *testing.T) {
 	t1 := Now()
 	d := t1.Sub(t0)
 	target := Delta * Count
-	slop := target * 2 / 10
-	if d < target-slop || d > target+slop {
+	if d < target*9/10 || d > target*30/10 {
 		t.Fatalf("%d ticks of %s took %s, expected %s", Count, Delta, d, target)
 	}
 }
@@ -197,9 +196,8 @@ func testAfterQueuing(t *testing.T) error {
 		}
 		dt := r.t.Sub(t0)
 		target := Duration(slot) * Delta
-		slop := Delta / 4
-		if dt < target-slop || dt > target+slop {
-			return fmt.Errorf("After(%s) arrived at %s, expected [%s,%s]", target, dt, target-slop, target+slop)
+		if dt < target-Delta/2 || dt > target+Delta*10 {
+			return fmt.Errorf("After(%s) arrived at %s, expected [%s,%s]", target, dt, target-Delta/2, target+Delta*10)
 		}
 	}
 	return nil

コアとなるコードの解説

TestAfterTick関数内の変更

元のコード:

	slop := target * 2 / 10
	if d < target-slop || d > target+slop {

変更後:

	if d < target*9/10 || d > target*30/10 {

この変更では、slop変数を削除し、許容範囲の計算を直接if文の中に埋め込んでいます。

  • d < target*9/10: 実際の経過時間dが、期待される合計時間targetの90%未満である場合にテストが失敗します。これは、テストが早すぎる場合に失敗する条件です。
  • d > target*30/10: 実際の経過時間dが、期待される合計時間targetの300%(3倍)を超える場合にテストが失敗します。これは、テストが遅すぎる場合に失敗する条件です。

これにより、テストの実行時間が期待値の90%から300%の範囲内であれば、テストは成功とみなされます。

testAfterQueuing関数内の変更

元のコード:

		slop := Delta / 4
		if dt < target-slop || dt > target+slop {
			return fmt.Errorf("After(%s) arrived at %s, expected [%s,%s]", target, dt, target-slop, target+slop)

変更後:

		if dt < target-Delta/2 || dt > target+Delta*10 {
			return fmt.Errorf("After(%s) arrived at %s, expected [%s,%s]", target, dt, target-Delta/2, target+Delta*10)

同様に、slop変数を削除し、許容範囲の計算を直接if文の中に埋め込んでいます。

  • dt < target-Delta/2: 個々のイベントの実際の経過時間dtが、期待される時間targetからDeltaの半分(50%)を引いた値よりも小さい場合にテストが失敗します。
  • dt > target+Delta*10: 個々のイベントの実際の経過時間dtが、期待される時間targetDeltaの10倍を加えた値よりも大きい場合にテストが失敗します。

これにより、個々のイベントの発生時間が、期待値からDeltaの50%手前から、Deltaの10倍遅れてもテストは成功とみなされます。エラーメッセージも新しい許容範囲に合わせて更新されています。

これらの変更は、テストの堅牢性を高め、特に時間精度が不安定な環境(低速なVMなど)での誤ったテスト失敗を防ぐことを目的としています。

関連リンク

参考にした情報源リンク