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

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

このコミットは、Goランタイムのガベージコレクション(GC)システムテストである TestGcSys を、メインのテストプロセスから分離された独立したプロセスで実行するように変更するものです。これにより、テストの安定性が向上し、ヒープサイズが大きくなることによるテストの失敗が回避されます。

コミット

commit 46890f60cee89ffef7a9b5f2b8d5e263650f61f7
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sat Mar 2 08:36:06 2013 +0200

    runtime: move TestGcSys into a separate process
    Fixes #4904.
    The problem was that when the test runs the heap had grown to ~100MB,
    so GC allows it to grow to 200MB, and so the test fails.
    Moving the test to a separate process makes it much more isolated and stable.
    
    R=golang-dev, minux.ma
    CC=golang-dev
    https://golang.org/cl/7441046

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

https://github.com/golang/go/commit/46890f60cee89ffef7a9b5f2b8d5e263650f61f7

元コミット内容

runtime: move TestGcSys into a separate process
Fixes #4904.
The problem was that when the test runs the heap had grown to ~100MB,
so GC allows it to grow to 200MB, and so the test fails.
Moving the test to a separate process makes it much more isolated and stable.

変更の背景

TestGcSys は、Goランタイムのガベージコレクション(GC)がシステムメモリを適切に解放するかどうかを検証するためのテストです。このテストは、大量のメモリを割り当て、GCが実行された後にシステムメモリの使用量が一定の閾値を超えないことを確認します。

しかし、このテストが既存のテストスイート内で実行されると、テスト開始時にはすでにヒープサイズが約100MBに達しているという問題がありました。GoのGCは、ヒープサイズが一定の割合(デフォルトでは2倍)に達するまでGCを実行しないという挙動があります。このため、テストが開始された時点でヒープが100MBあると、GCはヒープが200MBに達するまで実行されず、結果として TestGcSys が期待するメモリ使用量の閾値(16MB)を超えてしまい、テストが失敗するという不安定な状況が発生していました。

この問題を解決するため、TestGcSys を独立したプロセスで実行することが決定されました。独立したプロセスで実行することで、テストはクリーンな状態のヒープから開始され、他のテストの影響を受けることなく、GCの挙動を正確に検証できるようになります。これにより、テストの分離性が高まり、安定性が向上します。

前提知識の解説

  • Goのガベージコレクション (GC): Goは自動メモリ管理(ガベージコレクション)を採用しています。プログラムが不要になったメモリを自動的に解放し、開発者が手動でメモリを管理する手間を省きます。GoのGCは、ヒープの使用量に基づいてトリガーされることが多く、ヒープが一定のサイズに達するとGCが実行されます。
  • runtime.MemStats: Goの runtime パッケージが提供する構造体で、Goプログラムのメモリ使用状況に関する詳細な統計情報を含みます。これには、ヒープの使用量、GCの実行回数、システムに割り当てられたメモリ量などが含まれます。
  • runtime.GC(): runtime パッケージの関数で、明示的にガベージコレクションを実行します。通常はGCが自動的に実行されますが、特定のテストシナリオやデバッグ目的で手動でトリガーすることがあります。
  • runtime.GOMAXPROCS(): Goプログラムが同時に実行できるOSスレッドの最大数を設定します。このテストでは runtime.GOMAXPROCS(1) を設定しており、単一のCPUコアで実行されることを保証し、GCの挙動をより予測可能にしています。
  • testing.Short(): Goの testing パッケージの関数で、テストが「ショートモード」で実行されているかどうかを返します。go test -short コマンドでショートモードを有効にできます。このモードでは、時間がかかるテストをスキップしたり、テストのイテレーション回数を減らしたりすることが一般的です。
  • テストの分離: ソフトウェアテストにおいて、各テストが互いに独立して実行されることを指します。これにより、あるテストの実行結果が別のテストに影響を与えることがなくなり、テストの信頼性と再現性が向上します。特にメモリ使用量やグローバルな状態に依存するテストでは、分離が重要になります。独立したプロセスでテストを実行することは、強力な分離メカニズムの一つです。

技術的詳細

このコミットの主要な変更点は、TestGcSys テストを gc_test.go ファイル内で直接実行するのではなく、独立したGoプログラムとしてコンパイルし、それを実行するというアプローチに切り替えたことです。

  1. テストソースの埋め込み: testGCSysSource という定数に、TestGcSys のロジックを含む完全なGoプログラムのソースコードが文字列として埋め込まれています。このソースコードは、main パッケージと main 関数を持ち、独立して実行可能な形式です。
  2. テンプレートエンジンの利用: 埋め込まれたソースコードには {{if .Short}}{{end}} といったGoのテンプレート構文に似たプレースホルダーが含まれています。これは、testing.Short() の値に基づいて itercount を調整するためのものです。executeTest 関数内で、このテンプレートが処理され、実際のテストコードが生成されます。
  3. executeTest 関数の導入: TestGcSys は、executeTest というヘルパー関数を呼び出すように変更されました。この関数は以下の処理を行います。
    • 埋め込まれた testGCSysSource を受け取り、必要に応じてテンプレートを処理します。
    • 処理されたソースコードを一時ファイルに書き込みます。
    • その一時ファイルを独立したGoプログラムとしてコンパイルし、実行します。
    • 実行結果(標準出力)をキャプチャし、呼び出し元に返します。
  4. 出力の変更: 元の TestGcSys では、メモリ使用量が閾値を超えた場合に t.Fatalf を使用してテストを失敗させていました。独立プロセスに移行したことで、テストの失敗はプロセスの終了コードや標準出力のメッセージによって判断される必要があります。そのため、t.Fatalffmt.Printf に変更され、成功時には "OK"、失敗時にはメモリ使用量を出力するように変更されました。これにより、executeTest がこれらの出力を解析してテストの成否を判断できるようになります。
  5. runtime.GOMAXPROCS(1) の移動: 元のテストでは defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) を使用していましたが、独立プロセスでは main 関数内で直接 runtime.GOMAXPROCS(1) を呼び出すように変更されました。これにより、テストが常に単一のCPUコアで実行されることが保証されます。

この変更により、TestGcSys は他のテストのヒープ状態に影響されることなく、常にクリーンな環境でGCの挙動を検証できるようになり、テストの信頼性と安定性が大幅に向上しました。

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

src/pkg/runtime/gc_test.go ファイルが変更されました。

--- a/src/pkg/runtime/gc_test.go
+++ b/src/pkg/runtime/gc_test.go
@@ -14,7 +14,24 @@ func TestGcSys(t *testing.T) {
 	if os.Getenv("GOGC") == "off" {
 		t.Fatalf("GOGC=off in environment; test cannot pass")
 	}
-	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
+	data := struct{ Short bool }{testing.Short()}
+	got := executeTest(t, testGCSysSource, &data)
+	want := "OK\\n"
+	if got != want {
+		t.Fatalf("expected %q, but got %q", want, got)
+	}
+}
+
+const testGCSysSource = `
+package main
+
+import (
+	"fmt"
+	"runtime"
+)
+
+func main() {
+	runtime.GOMAXPROCS(1)
 	memstats := new(runtime.MemStats)
 	runtime.GC()
 	runtime.ReadMemStats(memstats)
@@ -23,9 +40,9 @@ func TestGcSys(t *testing.T) {
 	runtime.MemProfileRate = 0 // disable profiler
  
 	itercount := 1000000
-	if testing.Short() {
-		itercount = 100000
-	}
+{{if .Short}}
+	itercount = 100000
+{{end}}
 	for i := 0; i < itercount; i++ {
 		workthegc()
 	}
@@ -38,15 +55,17 @@ func TestGcSys(t *testing.T) {
 	} else {
 		sys = memstats.Sys - sys
 	}
-	t.Logf("used %d extra bytes", sys)
 	if sys > 16<<20 {
-		t.Fatalf("using too much memory: %d bytes", sys)
+		fmt.Printf("using too much memory: %d bytes\\n", sys)
+		return
 	}
+	fmt.Printf("OK\\n")
+}
  
 func workthegc() []byte {\n \treturn make([]byte, 1029)\n }\n+`
  
 func TestGcDeepNesting(t *testing.T) {
 	type T [2][2][2][2][2][2][2][2][2][2]*int

コアとなるコードの解説

  1. TestGcSys 関数の変更:

    • 元の TestGcSys の本体は削除され、代わりに executeTest 関数を呼び出すようになりました。
    • data := struct{ Short bool }{testing.Short()}: testing.Short() の結果を Short フィールドに持つ匿名構造体を作成し、これを executeTest に渡します。これは、埋め込まれたテストソース内でショートモードの挙動を制御するために使用されます。
    • got := executeTest(t, testGCSysSource, &data): testGCSysSource に定義されたテストコードを独立プロセスで実行し、その標準出力を got に格納します。
    • want := "OK\\n": 期待される出力は "OK" と改行です。
    • if got != want { t.Fatalf("expected %q, but got %q", want, got) }: 独立プロセスからの出力が "OK\n" でない場合、テストを失敗させます。
  2. testGCSysSource 定数:

    • これは、独立プロセスで実行されるGoプログラムのソースコードを文字列として保持する定数です。
    • package mainfunc main() を持ち、独立した実行ファイルとして機能します。
    • runtime.GOMAXPROCS(1): main 関数内で GOMAXPROCS を1に設定し、単一のCPUコアで実行されることを保証します。
    • memstats := new(runtime.MemStats): メモリ統計情報を格納するための構造体を初期化します。
    • runtime.GC(): 明示的にGCを実行します。
    • runtime.ReadMemStats(memstats): 現在のメモリ統計情報を取得します。
    • itercount の調整:
      • {{if .Short}} itercount = 100000 {{end}}: これはGoのテンプレート構文に似ており、executeTest 関数内で data.Shorttrue の場合(つまり、go test -short で実行された場合)に itercount を100000に設定します。これにより、ショートモードでのテスト時間を短縮します。
    • メモリ使用量のチェックと出力:
      • if sys > 16<<20 { fmt.Printf("using too much memory: %d bytes\\n", sys); return }: システムメモリの使用量が16MB(16<<2016 * 2^20 バイト、つまり16MB)を超えた場合、エラーメッセージを出力してプログラムを終了します。
      • fmt.Printf("OK\\n"): メモリ使用量が閾値内であれば "OK" を出力します。
  3. workthegc() 関数:

    • return make([]byte, 1029): 1029バイトのバイトスライスを割り当てて返します。これはGCをトリガーするための「作業」をシミュレートします。この関数がループ内で繰り返し呼び出されることで、ヒープに一時的なオブジェクトが生成され、GCの対象となります。

この変更により、TestGcSys は独立した環境で実行され、他のテストの影響を受けずにGCの挙動を正確に検証できるようになりました。

関連リンク

参考にした情報源リンク

特になし。コミットメッセージとコードの差分から直接情報を抽出しました。