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

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

このコミットは、Goランタイムのブロックプロファイラがチャネル操作(select文とチャネルのcloseを含む)をより適切にサポートするように改善するものです。具体的には、チャネルの送受信、select文、チャネルのクローズによってゴルーチンがブロックされた際に、そのブロックイベントがプロファイラによって正確に記録され、デバッグモードでより詳細なスタックトレースが提供されるようになります。

コミット

commit c922287686d3735050dc36a2eaa93ae1824732500
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Aug 14 13:56:01 2013 +0400

    runtime: improve block profiler support for channels
    1. Handle select statements.
    2. Handle chan close.
    3. Show top frame in debug mode (chansend/chanrecv/selectgo).
    Fixes #6049.
    
    R=golang-dev, daniel.morsing, rsc
    CC=golang-dev
    https://golang.org/cl/12694050

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

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

元コミット内容

runtime: improve block profiler support for channels
1. Handle select statements.
2. Handle chan close.
3. Show top frame in debug mode (chansend/chanrecv/selectgo).
Fixes #6049.

変更の背景

Go言語のブロックプロファイラは、ゴルーチンが同期プリミティブ(ミューテックス、チャネルなど)によってブロックされた時間を計測し、そのブロックが発生したコールスタックを記録することで、アプリケーションのパフォーマンスボトルネックを特定するのに役立ちます。このコミット以前は、チャネル操作、特にselect文やチャネルのcloseによって発生するブロックイベントが、プロファイラによって十分に捕捉・報告されていませんでした。

Goの並行処理モデルにおいて、チャネルはゴルーチン間の通信と同期の主要な手段です。チャネル操作が原因でゴルーチンがブロックされることは頻繁に発生し、これがアプリケーション全体のレイテンシやスループットに大きな影響を与える可能性があります。したがって、これらのブロックイベントを正確にプロファイルし、その原因を特定できるようにすることは、Goアプリケーションのパフォーマンスチューニングにおいて非常に重要です。

このコミットは、以下の具体的な課題を解決することを目的としています。

  1. select文によるブロックのプロファイル不足: select文は複数のチャネル操作を待機するために使用されますが、いずれのケースも準備ができていない場合にゴルーチンがブロックされます。このブロックがプロファイラによって適切に記録されていませんでした。
  2. チャネルcloseによるブロックのプロファイル不足: クローズされたチャネルからの受信は即座にゼロ値を返し、送信はパニックを引き起こします。しかし、チャネルがクローズされるのを待機しているゴルーチンがブロックされるケース(例えば、バッファ付きチャネルが満杯で送信側がブロックされ、そのチャネルがクローズされるのを待つ場合など)のプロファイルが不十分でした。
  3. デバッグモードでのスタックトレースの改善: ブロックプロファイラのデバッグ出力において、ブロックが発生した正確なランタイム関数(chansendchanrecvselectgoなど)がスタックトレースの最上位に表示されないことがあり、ブロックの原因特定を困難にしていました。

これらの改善により、開発者はチャネル関連のパフォーマンス問題をより効果的に診断できるようになります。

前提知識の解説

このコミットの理解には、以下のGo言語およびランタイムの概念に関する知識が必要です。

  1. Goのチャネル (Channels):

    • ゴルーチン間で値を送受信するための通信メカニズム。
    • make(chan T)で作成され、ch <- valで送信、val := <-chで受信します。
    • バッファなしチャネルは同期通信を行い、送信側と受信側が同時に準備ができていないとブロックされます。
    • バッファ付きチャネルは非同期通信を行い、バッファが満杯または空でない限りブロックされません。
    • close(ch)でチャネルをクローズできます。クローズされたチャネルからの受信は即座にゼロ値を返し、送信はパニックを引き起こします。
  2. select:

    • 複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するための制御構造。
    • case <-ch:(受信)、case ch <- val:(送信)、default:(いずれのチャネルも準備ができていない場合の非ブロック処理)などの句を持ちます。
    • select文内のどのケースも準備ができていない場合、default句がなければゴルーチンはブロックされます。
  3. Goのゴルーチン (Goroutines):

    • Goランタイムによって管理される軽量な実行スレッド。
    • goキーワードで関数を呼び出すことで起動されます。
    • Goスケジューラによって、OSスレッド上で多重化されて実行されます。
  4. Goのプロファイリングツール:

    • Goには、CPU、メモリ、ブロック、ミューテックスなどのプロファイリングをサポートする組み込みツールがあります。
    • ブロックプロファイラ (Block Profiler): ゴルーチンが同期プリミティブ(チャネル、ミューテックスなど)によってブロックされた時間を計測し、そのブロックが発生した場所(コールスタック)を記録します。これにより、並行処理における競合やデッドロック、パフォーマンスボトルネックを特定できます。
    • runtime.SetBlockProfileRate関数を使用して、ブロックプロファイリングを有効にし、サンプリングレートを設定します。
    • runtime.Lookup("block").WriteTo(...)を使用して、プロファイルデータを取得します。
  5. runtimeパッケージ:

    • Goランタイムの低レベルな機能を提供するパッケージ。
    • チャネル操作、ゴルーチン管理、スケジューリング、メモリ管理など、Goプログラムの実行基盤を担うC言語(またはGoアセンブリ)で実装された部分(chan.cなど)と、Go言語で書かれた部分から構成されます。
  6. runtime·cputicks():

    • Goランタイム内部で使用される関数で、CPUのティック数を取得します。これは、ブロック時間の計測に使用されます。
  7. runtime·blockevent():

    • ブロックプロファイラにブロックイベントを報告するためのランタイム内部関数。ブロック時間とイベントタイプ(チャネル送受信、ミューテックスロックなど)を引数に取ります。
  8. SudoG構造体:

    • runtimeパッケージ内部で使用される構造体で、チャネル操作によってブロックされたゴルーチンに関する情報を保持します。このコミットでは、releasetimeフィールドが追加され、ブロック解除時のCPUティックを記録するために使用されます。

技術的詳細

このコミットの主要な変更点は、src/pkg/runtime/chan.csrc/pkg/runtime/pprof/pprof.gosrc/pkg/runtime/pprof/pprof_test.goの3つのファイルにわたります。

src/pkg/runtime/chan.cの変更

このファイルはGoランタイムにおけるチャネルのコアロジックを実装しています。変更の目的は、select文とチャネルのclose操作におけるブロックイベントの計測と報告を改善することです。

  1. selectgo関数の変更:

    • selectgoselect文のランタイム実装です。
    • ブロックプロファイリングが有効な場合(runtime·blockprofilerate > 0)、t0 = runtime·cputicks()select操作が開始された時刻を記録します。
    • selectの各ケース(Scase)に関連付けられたSudoG構造体のreleasetimeフィールドを-1に初期化します。これは、後でブロックが解除された時刻を記録するためのフラグとして機能します。
    • 非同期受信 (asyncrecv)、非同期送信 (asyncsend)、同期受信 (syncrecv)、同期送信 (syncsend) の各パスにおいて、ゴルーチンがready状態になる直前に、sg->releasetime = runtime·cputicks()を呼び出して、ブロックが解除された時刻を記録します。
    • select文が完了し、ゴルーチンがブロックから解放される直前(retcラベルの直前)で、cas->sg.releasetime > 0の場合にruntime·blockevent(cas->sg.releasetime - t0, 2)を呼び出します。ここで、cas->sg.releasetime - t0はブロックされた時間を示し、2はイベントタイプ(チャネル操作によるブロック)を示します。これにより、selectによるブロックイベントがプロファイラに報告されます。
  2. closechan関数の変更:

    • closechanはチャネルをクローズするランタイム実装です。
    • チャネルがクローズされた際に、そのチャネルでブロックされていた送信側ゴルーチン(c->sendq)と受信側ゴルーチン(c->recvq)が解放されます。
    • これらのゴルーチンが解放される直前に、sg->releasetime = runtime·cputicks()を呼び出して、ブロックが解除された時刻を記録します。これにより、チャネルのクローズによってブロックが解除されたイベントもプロファイラに報告されるようになります。

src/pkg/runtime/pprof/pprof.goの変更

このファイルはGoのプロファイリングデータの書き出しロジックを含んでいます。

  1. writeBlock関数の変更:
    • writeBlockはブロックプロファイルデータを書き出す関数です。
    • デバッグモード(debug > 0)の場合、printStackRecord(w, r.Stack(), true)と変更されています。以前はfalseでした。このtrueという引数は、スタックトレースの最上位フレーム(chansendchanrecvselectgoなど、ブロックが発生したランタイム関数)を強制的に表示させるためのフラグです。これにより、プロファイル出力がより詳細になり、ブロックの原因を特定しやすくなります。

src/pkg/runtime/pprof/pprof_test.goの変更

このファイルはブロックプロファイラのテストケースを含んでいます。

  1. TestBlockProfile関数の変更:
    • 以前はproduceChanContention()produceMutexContention()という2つの関数を呼び出してチャネルとミューテックスの競合をテストしていましたが、このコミットではより多くのシナリオをカバーするためにテストケースが拡張されました。
    • TestCase構造体が導入され、テスト名、実行する関数、期待される正規表現パターンが定義されています。
    • 追加されたテストケースは以下の通りです。
      • blockChanRecv: チャネル受信によるブロック
      • blockChanSend: チャネル送信によるブロック
      • blockChanClose: チャネルクローズによるブロック(受信側がブロックされるケース)
      • blockSelectRecvAsync: select文での非同期受信によるブロック
      • blockSelectSendSync: select文での同期送信によるブロック
      • blockMutex: ミューテックスロックによるブロック(既存のテストを再利用)
    • 各テストケースは、blockDelay(10ミリ秒)だけ遅延を導入することで、意図的にブロックを発生させています。
    • プロファイル出力が各テストケースの期待される正規表現パターンと一致するかどうかを検証することで、ブロックプロファイラが正しく機能していることを確認しています。特に、スタックトレースの最上位にruntime.chanrecv1runtime.chansend1runtime.selectgosync.(*Mutex).Lockといったランタイム関数が表示されることを検証しています。

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

src/pkg/runtime/chan.c

--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -851,6 +851,7 @@ selectgo(Select **selp)
 {
 	Select *sel;
 	uint32 o, i, j, k;
+	int64 t0; // 追加: select開始時刻を記録するための変数
 	Scase *cas, *dfl;
 	Hchan *c;
 	SudoG *sg;
@@ -865,6 +866,13 @@ selectgo(Select **selp)
 	if(debug)
 		runtime·printf("select: sel=%p\n", sel);
 
+	t0 = 0; // 初期化
+	if(runtime·blockprofilerate > 0) { // ブロックプロファイリングが有効な場合
+		t0 = runtime·cputicks(); // select開始時刻を記録
+		for(i=0; i<sel->ncase; i++)
+			sel->scase[i].sg.releasetime = -1; // releasetimeを初期化
+	}
+
 	// The compiler rewrites selects that statically have
 	// only 0 or 1 cases plus default into simpler constructs.
 	// The only way we can end up with such small sel->ncase
@@ -1048,6 +1056,8 @@ asyncrecv:
 	if(sg != nil) {
 		gp = sg->g;
 		selunlock(sel);
+		if(sg->releasetime) // releasetimeが設定されている場合(プロファイリング有効時)
+			sg->releasetime = runtime·cputicks(); // ブロック解除時刻を記録
 		runtime·ready(gp);
 	} else {
 		selunlock(sel);
@@ -1066,6 +1076,8 @@ asyncsend:
 	if(sg != nil) {
 		gp = sg->g;
 		selunlock(sel);
+		if(sg->releasetime)
+			sg->releasetime = runtime·cputicks();
 		runtime·ready(gp);
 	} else {
 		selunlock(sel);
@@ -1085,6 +1097,8 @@ syncrecv:
 		c->elemalg->copy(c->elemsize, cas->sg.elem, sg->elem);
 	gp = sg->g;
 	gp->param = sg;
+	if(sg->releasetime)
+		sg->releasetime = runtime·cputicks();
 	runtime·ready(gp);
 	goto retc;
 
@@ -1110,6 +1124,8 @@ syncsend:
 		c->elemalg->copy(c->elemsize, sg->elem, cas->sg.elem);
 	gp = sg->g;
 	gp->param = sg;
+	if(sg->releasetime)
+		sg->releasetime = runtime·cputicks();
 	runtime·ready(gp);
 
 retc:
@@ -1123,6 +1139,8 @@ retc:
 		as = (byte*)selp + cas->so;
 		*as = true;
 	}
+	if(cas->sg.releasetime > 0) // releasetimeが有効な場合
+		runtime·blockevent(cas->sg.releasetime - t0, 2); // ブロックイベントを報告
 	runtime·free(sel);
 	return pc;
 
@@ -1265,6 +1283,8 @@ closechan(Hchan *c, void *pc)
 			break;
 		gp = sg->g;
 		gp->param = nil;
+		if(sg->releasetime)
+			sg->releasetime = runtime·cputicks();
 		runtime·ready(gp);
 	}
 
@@ -1275,6 +1295,8 @@ closechan(Hchan *c, void *pc)
 			break;
 		gp = sg->g;
 		gp->param = nil;
+		if(sg->releasetime)
+			sg->releasetime = runtime·cputicks();
 		runtime·ready(gp);
 	}

src/pkg/runtime/pprof/pprof.go

--- a/src/pkg/runtime/pprof/pprof.go
+++ b/src/pkg/runtime/pprof/pprof.go
@@ -666,7 +666,7 @@ func writeBlock(w io.Writer, debug int) error {
 		}
 		fmt.Fprint(w, "\n")
 		if debug > 0 {
-			printStackRecord(w, r.Stack(), false) // falseからtrueに変更
+			printStackRecord(w, r.Stack(), true) // 最上位フレームを表示
 		}
 	}

src/pkg/runtime/pprof/pprof_test.go

--- a/src/pkg/runtime/pprof/pprof_test.go
+++ b/src/pkg/runtime/pprof/pprof_test.go
@@ -181,10 +181,55 @@ var badOS = map[string]bool{
 }
 
 func TestBlockProfile(t *testing.T) {
+	type TestCase struct { // 新しいテストケース構造体
+		name string
+		f    func()
+		re   string // 期待される正規表現パターン
+	}
+	tests := [...]TestCase{ // 複数のテストケースを定義
+		{"chan recv", blockChanRecv, `
+[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
+#	0x[0-9,a-f]+\truntime\.chanrecv1\+0x[0-9,a-f]+\t.*/src/pkg/runtime/chan.c:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.blockChanRecv\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+`},
+		{"chan send", blockChanSend, `
+[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
+#	0x[0-9,a-f]+\truntime\.chansend1\+0x[0-9,a-f]+\t.*/src/pkg/runtime/chan.c:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.blockChanSend\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+`},
+		{"chan close", blockChanClose, `
+[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
+#	0x[0-9,a-f]+\truntime\.chanrecv1\+0x[0-9,a-f]+\t.*/src/pkg/runtime/chan.c:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.blockChanClose\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+`},
+		{"select recv async", blockSelectRecvAsync, `
+[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
+#	0x[0-9,a-f]+\truntime\.selectgo\+0x[0-9,a-f]+\t.*/src/pkg/runtime/chan.c:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.blockSelectRecvAsync\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+#	0x[0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+`},
+		{"select send sync", blockSelectSendSync, `
+[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
+#	0x[0-9,a-f]+\truntime\.selectgo\+0x[0-9,a-f]+\t.*/src/pkg/runtime/chan.c:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.blockSelectSendSync\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+`},
+		{"mutex", blockMutex, `
+[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
+#	0x[0-9,a-f]+\tsync\\.\\(\\*Mutex\\)\\.Lock\\+0x[0-9,a-f]+\t.*/src/pkg/sync/mutex\\.go:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.blockMutex\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+#	0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+
+`},
+	}
+
 	runtime.SetBlockProfileRate(1)
 	defer runtime.SetBlockProfileRate(0)
-\tproduceChanContention()\n-\tproduceMutexContention()\n+\tfor _, test := range tests { // 各テストケースを実行
+\t\ttest.f()
+\t}
 	var w bytes.Buffer
 	Lookup("block").WriteTo(&w, 1)
 	prof := w.String()
@@ -193,40 +238,73 @@ func TestBlockProfile(t *testing.T) {
 	\tt.Fatalf("Bad profile header:\n%v", prof)
 	}
 
-\treChan := regexp.MustCompile(`\n-[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+\n-#\t0x[0-9,a-f]+\truntime/pprof_test\.produceChanContention\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+\n-#\t0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+\n-`)\n-\tif !reChan.MatchString(prof) {\n-\t\tt.Fatalf("Bad chan entry, expect:\n%v\ngot:\n%v", reChan, prof)\n-\t}\n-\n-\treMutex := regexp.MustCompile(`\n-[0-9]+ [0-9]+ @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+\n-#\t0x[0-9,a-f]+\tsync\\.\\(\\*Mutex\\)\\.Lock\+0x[0-9,a-f]+\t.*/src/pkg/sync/mutex\\.go:[0-9]+\n-#\t0x[0-9,a-f]+\truntime/pprof_test\.produceMutexContention\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+\n-#\t0x[0-9,a-f]+\truntime/pprof_test\.TestBlockProfile\+0x[0-9,a-f]+\t.*/src/pkg/runtime/pprof/pprof_test.go:[0-9]+\n-`)\n-\tif !reMutex.MatchString(prof) {\n-\t\tt.Fatalf("Bad mutex entry, expect:\n%v\ngot:\n%v", reMutex, prof)\n+\tfor _, test := range tests { // 各テストケースの正規表現を検証
+\t\tif !regexp.MustCompile(test.re).MatchString(prof) {
+\t\t\tt.Fatalf("Bad %v entry, expect:\n%v\ngot:\n%v", test.name, test.re, prof)
+\t\t}
 	}
 }
 
-func produceChanContention() {
+const blockDelay = 10 * time.Millisecond // ブロック遅延定数
+
+func blockChanRecv() { // チャネル受信によるブロックを発生させる関数
 	c := make(chan bool)
 	go func() {
-\t\ttime.Sleep(10 * time.Millisecond)\n+\t\ttime.Sleep(blockDelay)
 	\tc <- true
 	}()
 	<-c
 }
 
-func produceMutexContention() {
+func blockChanSend() { // チャネル送信によるブロックを発生させる関数
+	c := make(chan bool)
+	go func() {
+		time.Sleep(blockDelay)
+		<-c
+	}()
+	c <- true
+}
+
+func blockChanClose() { // チャネルクローズによるブロックを発生させる関数
+	c := make(chan bool)
+	go func() {
+		time.Sleep(blockDelay)
+		close(c)
+	}()
+	<-c
+}
+
+func blockSelectRecvAsync() { // selectでの非同期受信によるブロックを発生させる関数
+	c := make(chan bool, 1)
+	c2 := make(chan bool, 1)
+	go func() {
+		time.Sleep(blockDelay)
+		c <- true
+	}()
+	select {
+	case <-c:
+	case <-c2:
+	}
+}
+
+func blockSelectSendSync() { // selectでの同期送信によるブロックを発生させる関数
+	c := make(chan bool)
+	c2 := make(chan bool)
+	go func() {
+		time.Sleep(blockDelay)
+		<-c
+	}()
+	select {
+	case c <- true:
+	case c2 <- true:
+	}
+}
+
+func blockMutex() { // ミューテックスロックによるブロックを発生させる関数
 	var mu sync.Mutex
 	mu.Lock()
 	go func() {
-\t\ttime.Sleep(10 * time.Millisecond)\n+\t\ttime.Sleep(blockDelay)
 	\t\tmu.Unlock()
 	}()
 	mu.Lock()

コアとなるコードの解説

src/pkg/runtime/chan.c

  • selectgo関数内のt0releasetimeの導入:

    • int64 t0;が追加され、select文の実行が開始された時刻をruntime·cputicks()で記録します。
    • selectの各ケースに対応するSudoG構造体(ブロックされたゴルーチンに関する情報を持つ)のreleasetimeフィールドが-1に初期化されます。これは、このゴルーチンがまだブロック解除されていないことを示すマーカーです。
    • select内のチャネル操作(asyncrecv, asyncsend, syncrecv, syncsend)が成功し、ゴルーチンがruntime·ready(gp)によって実行可能状態になる直前に、sg->releasetime = runtime·cputicks();が呼び出されます。これにより、ゴルーチンがブロックから解放された正確な時刻が記録されます。
    • retcラベルの直前、つまりselect文が完了する直前に、if(cas->sg.releasetime > 0)の条件で、ブロックされた時間が有効な場合にruntime·blockevent(cas->sg.releasetime - t0, 2);が呼び出されます。ここで、cas->sg.releasetime - t0がブロックされた期間を表し、2はチャネル操作によるブロックイベントであることを示します。これにより、select文によるブロックがプロファイラに報告されるようになります。
  • closechan関数内のreleasetimeの導入:

    • closechan関数内で、チャネルがクローズされた際に、そのチャネルでブロックされていた送信側 (c->sendq) および受信側 (c->recvq) のゴルーチンが解放されます。
    • これらのゴルーチンがruntime·ready(gp)によって実行可能状態になる直前に、sg->releasetime = runtime·cputicks();が呼び出されます。これにより、チャネルのクローズによってブロックが解除されたゴルーチンの解放時刻が記録され、ブロックプロファイラがこれらのイベントを捕捉できるようになります。

これらの変更により、Goランタイムはselect文やチャネルのクローズによって発生するゴルーチンのブロックイベントを正確に計測し、プロファイラに報告するための情報を収集するようになります。

src/pkg/runtime/pprof/pprof.go

  • writeBlock関数内のprintStackRecordの引数変更:
    • printStackRecord(w, r.Stack(), true)への変更は、ブロックプロファイルのデバッグ出力において、スタックトレースの最上位フレームを強制的に表示させるためのものです。
    • 以前はfalseが渡されており、これによりランタイム内部の低レベルな関数(例: chansend, chanrecv, selectgo)が省略される可能性がありました。trueにすることで、これらの関数がスタックトレースの最上位に明示的に表示されるようになり、ブロックがどのランタイム関数で発生したかをより正確に把握できるようになります。これは、プロファイル結果の可読性と診断能力を大幅に向上させます。

src/pkg/runtime/pprof/pprof_test.go

  • TestBlockProfileのテストケース拡張:
    • TestCase構造体とtestsスライスを導入することで、チャネルの送受信、クローズ、select文の様々なシナリオにおけるブロックプロファイリングを網羅的にテストできるようになりました。
    • 各テスト関数(blockChanRecv, blockChanSend, blockChanClose, blockSelectRecvAsync, blockSelectSendSync, blockMutex)は、time.Sleep(blockDelay)を介して意図的にゴルーチンをブロックさせ、そのブロックイベントがプロファイラによって捕捉されることを確認します。
    • regexp.MustCompile(test.re).MatchString(prof)による正規表現マッチングは、プロファイル出力に期待されるスタックトレース(特に最上位のランタイム関数)が含まれていることを検証します。これにより、ランタイムの変更がブロックプロファイラの機能に正しく反映されていることが保証されます。

これらの変更は、Goの並行処理におけるパフォーマンスボトルネックの特定とデバッグを大幅に改善し、開発者がより効率的にGoアプリケーションを最適化できるようにするための重要なステップです。

関連リンク

参考にした情報源リンク

  • Goの公式リポジトリ: https://github.com/golang/go
  • Goのコードレビューシステム (Gerrit): https://go.dev/cl/ (コミットメッセージに記載されているhttps://golang.org/cl/12694050は、このコミットのGerritレビューへのリンクです。)
  • GoのIssue Tracker: https://github.com/golang/go/issues (コミットメッセージに記載されているFixes #6049は、関連するIssueへの参照ですが、今回の検索では直接的なIssueは見つかりませんでした。これは、Issueがクローズされたか、番号が変更されたか、あるいは内部的なIssueであった可能性を示唆しています。)