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

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

このコミットは、Go言語の標準ライブラリ time パッケージ内の time.Tick 関数のテスト (tick_test.go) において、テストの安定性を向上させるための変更です。具体的には、テストで使用されるティック間隔を10ミリ秒から100ミリ秒に延長することで、システムが高負荷状態にある場合でもテストが安定してパスするように修正されています。

コミット

commit be76898cb31a4a9da97965cfa753685d560874e7
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jan 5 11:18:20 2009 -0800

    change time.Tick test to use 100ms intervals.
    now passes even under loaded conditions on r45.
    
    R=r
    DELTA=2  (0 added, 0 deleted, 2 changed)
    OCL=22019
    CL=22022

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

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

元コミット内容

change time.Tick test to use 100ms intervals.
now passes even under loaded conditions on r45.

変更の背景

このコミットの背景には、time.Tick 関数のテストが、システムが高負荷状態("loaded conditions")にある環境、特に当時のGoのリリースバージョンである r45 で不安定になるという問題がありました。

time.Tick のような時間依存の機能は、システムのCPU使用率が高い、I/Oが集中している、あるいはスケジューリングの遅延が発生しているといった高負荷条件下では、期待通りの厳密なタイミングでイベントが発生しないことがあります。テストが非常に短い間隔(この場合は10ミリ秒)に依存していると、わずかな遅延でもテストが失敗する「flaky test」(不安定なテスト)となる可能性が高まります。

開発者は、テストが実際のコードの振る舞いを正確に反映しつつ、実行環境の変動に左右されずに安定してパスすることを望みます。このコミットは、テストの信頼性を高め、高負荷時でもテストが誤って失敗しないようにするために、ティック間隔を長くするというアプローチが取られました。

前提知識の解説

time.Tick 関数

Go言語の time パッケージは、時間に関する機能を提供します。time.Tick 関数は、指定された期間ごとにイベントを発生させるチャネルを返します。

func Tick(d Duration) <-chan Time

Tick 関数は d で指定された期間(time.Duration型)が経過するたびに、現在の時刻をチャネルに送信します。これは、定期的な処理(例: ログの出力、メトリクスの収集、定期的なデータ同期など)を実装する際によく使用されます。

時間依存コードのテストの課題

time.Tick のような時間依存のコードをテストすることは、いくつかの課題を伴います。

  1. 非決定性 (Non-Determinism): テストがシステムの実時間(リアルタイムクロック)に依存するため、テストの実行タイミング、システム負荷、あるいは夏時間のような外部要因によって結果が変動する可能性があります。これにより、同じコードでもテストがパスしたり失敗したりする「flaky test」が発生しやすくなります。
  2. テストの遅延 (Slow Tests): 実際の時間が経過するのを待つ必要があるため、テストの実行に時間がかかります。これは、開発サイクルを遅らせ、継続的インテグレーションの効率を低下させます。
  3. 状態の検証の難しさ: 特定の期間内にイベントが発生したか、あるいは発生しなかったかを厳密に検証することが難しい場合があります。

これらの課題に対処するため、一般的には「モッククロック」や「フェイククロック」を用いた依存性注入(Dependency Injection)が推奨されます。これにより、テスト中に時間をプログラムで制御し、テストを決定論的かつ高速に実行できます。しかし、このコミットが作成された初期のGoのテストフレームワークでは、このような高度なモック化のパターンがまだ確立されていなかったか、あるいはテストの単純性を優先した可能性があります。

「Loaded Conditions」の概念

「Loaded Conditions」(高負荷状態)とは、システムがCPU、メモリ、I/Oなどのリソースを大量に消費している状態を指します。このような状況下では、オペレーティングシステムはプロセスのスケジューリングに遅延を生じさせたり、リソースの競合が発生したりすることがあります。

time.Tick のようなタイマー機能は、OSのスケジューラに依存して正確なタイミングを保証します。システムが高負荷状態にあると、スケジューラがGoのランタイムやテストプロセスにCPU時間を割り当てるのが遅れ、結果として time.Tick が期待される厳密な間隔よりも遅れてイベントを発生させることがあります。

このコミットでは、テストがこのような高負荷状態でも安定してパスするように、許容される時間的な「ずれ」の範囲を広げるために、ティック間隔自体を長くするというアプローチが取られました。

技術的詳細

このコミットの技術的な変更は非常にシンプルですが、その背後には時間依存のテストにおける現実的な課題への対処があります。

src/lib/time/tick_test.go 内の TestTick 関数は、time.Tick が指定された回数だけ正確にティックを生成したかどうかを検証しています。テストは、Count 回のティックが Delta * Count ナノ秒の合計時間内に収まることを期待します。そして、この合計時間に対して slop(許容誤差)を設定し、実際の経過時間が target - slop から target + slop の範囲内にあるかをチェックします。

元のコードでは、Delta10*1e6 ナノ秒(10ミリ秒)に設定されていました。これは非常に短い間隔であり、システムが高負荷状態にある場合、OSのスケジューリングの遅延やその他の要因により、実際のティック間隔がわずかに長くなり、テストが設定された slop の範囲を超えて失敗する可能性がありました。

このコミットでは、Delta の値を 10*1e6 から 100*1e6 ナノ秒(100ミリ秒)に増やしています。

  • Delta の増加: ティック間隔を10倍にすることで、各ティックの間に許容される絶対的な時間的ずれの割合が相対的に小さくなります。例えば、10ミリ秒のティックで1ミリ秒の遅延は10%のずれですが、100ミリ秒のティックで1ミリ秒の遅延は1%のずれに過ぎません。これにより、同じ絶対的な遅延が発生しても、テストが設定された slop の範囲内に収まる可能性が高まります。
  • テストの安定性向上: 結果として、システムが高負荷状態にある場合でも、time.Tick のテストがより安定してパスするようになります。これは、テストが実際のシステム環境の変動に対してより堅牢になることを意味します。
  • トレードオフ: この変更はテストの安定性を向上させますが、テストの粒度を粗くするというトレードオフがあります。つまり、より長い間隔でのみ time.Tick の正確性を検証することになり、非常に短い間隔での厳密なタイミング保証に関する問題を見逃す可能性はあります。しかし、このテストの目的が time.Tick の基本的な機能と、ある程度の時間的正確性を検証することであるならば、このトレードオフは許容範囲内と判断されたのでしょう。

また、t.Fatalf のフォーマット文字列が %d から %g に変更され、Delta, ns, target の値が float64 にキャストされています。これは、大きな数値(ナノ秒単位)をより読みやすく表示するため、あるいは将来的に Delta が浮動小数点数になる可能性を考慮した、表示上の改善と考えられます。

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

--- a/src/lib/time/tick_test.go
+++ b/src/lib/time/tick_test.go
@@ -11,7 +11,7 @@ import (
 
 export func TestTick(t *testing.T) {
 	const (
-		Delta uint64 = 10*1e6;
+		Delta uint64 = 100*1e6;
 		Count uint64 = 10;
 	);
 	c := Tick(Delta);
@@ -24,6 +24,6 @@ export func TestTick(t *testing.T) {
 	target := int64(Delta*Count);
 	slop := target*2/10;
 	if ns < target - slop || ns > target + slop {
-\t\tt.Fatalf("%d ticks of %d ns took %d ns, expected %d", Count, Delta, ns, target);\n+\t\tt.Fatalf("%d ticks of %g ns took %g ns, expected %g", Count, float64(Delta), float64(ns), float64(target));
 	}\n }\

コアとなるコードの解説

変更は src/lib/time/tick_test.go ファイル内の TestTick 関数に集中しています。

  1. Delta 定数の変更:

    -		Delta uint64 = 10*1e6;
    +		Delta uint64 = 100*1e6;
    

    ここがこのコミットの主要な変更点です。Deltatime.Tick 関数に渡されるティック間隔(ナノ秒単位)を定義しています。

    • 変更前: 10*1e610,000,000 ナノ秒、つまり 10 ミリ秒です。
    • 変更後: 100*1e6100,000,000 ナノ秒、つまり 100 ミリ秒です。 この変更により、テストは10ミリ秒間隔ではなく、100ミリ秒間隔でティックを待ち受けるようになります。これにより、システムが高負荷状態にある場合でも、個々のティックの遅延が相対的に小さくなり、テストが設定された許容誤差 (slop) の範囲内でパスしやすくなります。
  2. t.Fatalf フォーマット文字列の変更:

    -\t\tt.Fatalf("%d ticks of %d ns took %d ns, expected %d", Count, Delta, ns, target);
    +\t\tt.Fatalf("%d ticks of %g ns took %g ns, expected %g", Count, float64(Delta), float64(ns), float64(target));
    

    この変更は、テストが失敗した際に表示されるエラーメッセージのフォーマットを調整しています。

    • 変更前は、%d フォーマット指定子を使用して uint64int64 の整数値を表示していました。
    • 変更後は、%g フォーマット指定子を使用し、Delta, ns, target の値を float64 に明示的にキャストしています。%g は、数値の大きさに応じて %e(指数表記)または %f(浮動小数点表記)を自動的に選択する汎用的な浮動小数点フォーマット指定子です。これにより、非常に大きなナノ秒の値をより読みやすい形式で出力できるようになります。これは機能的な変更ではなく、デバッグ時の可読性を向上させるための改善です。

これらの変更は、time.Tick のテストが、より現実的な(高負荷時の)環境下で安定して動作することを目指しています。

関連リンク

参考にした情報源リンク