[インデックス 17151] ファイルの概要
このコミットは、Go言語のランタイムプロファイリングツールであるpprof
パッケージに、ブロックプロファイル(Block Profile)のテストを追加するものです。具体的には、チャネルとミューテックスの競合によって発生するブロッキングイベントをpprof
が正しくプロファイルできることを検証するためのテストケースが導入されています。
コミット
commit 2758101b9d336a892538da38a11fe66ede2aa0cb
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sun Aug 11 13:05:51 2013 +0400
runtime/pprof: add block profile test
Fixes #5993.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12665046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2758101b9d336a892538da38a11fe66ede2aa0cb
元コミット内容
runtime/pprof: add block profile test
Fixes #5993.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12665046
変更の背景
このコミットは、Go言語のIssue 5993「runtime/pprof
: block profile is not working」を修正するために行われました。Issue 5993では、Goのpprof
ツールにおけるブロックプロファイル機能が正しく動作していないという問題が報告されていました。ブロックプロファイルは、Goプログラム内でゴルーチンがブロッキング操作(例: チャネル操作、ミューテックスのロック待ち、システムコールなど)によって停止している時間を計測し、その原因となっているコード箇所を特定するための重要なツールです。
この問題が修正されていない場合、開発者はGoアプリケーションのパフォーマンスボトルネック、特に並行処理における競合やデッドロックの原因を特定することが困難になります。そのため、ブロックプロファイルが期待通りに機能することを保証するためのテストケースの追加は、ツールの信頼性と実用性を高める上で不可欠でした。
前提知識の解説
Goのプロファイリング (pprof)
Go言語には、プログラムのパフォーマンスを分析するための標準パッケージruntime/pprof
が用意されています。pprof
は、CPU使用率、メモリ割り当て、ゴルーチンのブロッキング、ミューテックスの競合など、様々な側面からプログラムの動作をプロファイルする機能を提供します。プロファイルデータは、go tool pprof
コマンドで視覚化・分析できる形式で出力されます。
ブロックプロファイル (Block Profile)
ブロックプロファイルは、Goプログラム内でゴルーチンがブロッキング操作によって停止している時間を計測するプロファイルです。ブロッキング操作には以下のようなものがあります。
- チャネル操作: 送信(
chan <- value
)や受信(<- chan
)が、相手側の準備ができていないためにブロックされる場合。 - ミューテックスのロック:
sync.Mutex
などのロックを獲得しようとした際に、すでにロックが保持されているために待機する場合。 - システムコール: ファイルI/OやネットワークI/Oなど、OSの操作を待つ場合。
- GC (Garbage Collection): ガベージコレクションが実行されている間、アプリケーションの実行が一時停止する場合。
ブロックプロファイルは、これらのブロッキングイベントが発生した場所と、そのブロッキングに費やされた時間(またはイベント数)を記録します。これにより、プログラムの並行処理におけるボトルネックや、意図しない待機時間を特定し、改善に役立てることができます。
runtime.SetBlockProfileRate
runtime.SetBlockProfileRate
関数は、ブロックプロファイルのサンプリングレートを設定するために使用されます。引数に与える値は、ブロッキングイベントが発生した際に、そのイベントをプロファイルに記録する頻度を制御します。例えば、runtime.SetBlockProfileRate(1)
と設定すると、1ナノ秒以上のブロッキングイベントがすべて記録されるようになります(実際には、Goランタイムは内部的にサンプリングを行っており、この値はサンプリングの頻度を調整するためのヒントとして使われます)。0
を設定すると、ブロックプロファイルは無効になります。
Goの並行処理プリミティブ
- Goroutine (ゴルーチン): Goランタイムによって管理される軽量なスレッドです。
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンを起動できます。 - Channel (チャネル): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、ゴルーチン間の同期にも使用されます。バッファなしチャネルの場合、送信と受信は同期的に行われ、片方が準備できるまでもう片方がブロックされます。
- Mutex (ミューテックス):
sync.Mutex
は、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。Lock()
メソッドでロックを獲得し、Unlock()
メソッドでロックを解放します。ロックがすでに保持されている場合、Lock()
を呼び出したゴルーチンはブロックされます。
技術的詳細
このコミットで追加されたTestBlockProfile
関数は、runtime/pprof
パッケージのブロックプロファイル機能が正しく動作するかを検証するための統合テストです。テストは以下のステップで構成されています。
- ブロックプロファイルの有効化:
runtime.SetBlockProfileRate(1)
を呼び出して、ブロックプロファイルを有効にします。これにより、ブロッキングイベントがプロファイルに記録されるようになります。テスト終了時にはdefer runtime.SetBlockProfileRate(0)
で無効化されます。 - チャネル競合の生成:
produceChanContention()
関数を呼び出します。この関数は、バッファなしチャネルを作成し、別のゴルーチンでチャネルへの送信を遅延させ、メインゴルーチンでチャネルからの受信を待機させることで、意図的にチャネルのブロッキングを発生させます。c := make(chan bool)
: バッファなしチャネルを作成。go func() { time.Sleep(10 * time.Millisecond); c <- true }()
: 別のゴルーチンで10ミリ秒待機後、チャネルに値を送信。<-c
: メインゴルーチンはチャネルからの受信を試みるが、送信側が準備できるまでブロックされる。
- ミューテックス競合の生成:
produceMutexContention()
関数を呼び出します。この関数は、ミューテックスをロックし、別のゴルーチンでそのミューテックスのロックを解除するのを遅延させ、メインゴルーチンで再度ロックを獲得しようとすることで、意図的にミューテックスのブロッキングを発生させます。var mu sync.Mutex
: ミューテックスを作成。mu.Lock()
: メインゴルーチンがミューテックスをロック。go func() { time.Sleep(10 * time.Millisecond); mu.Unlock() }()
: 別のゴルーチンで10ミリ秒待機後、ミューテックスをアンロック。mu.Lock()
: メインゴルーチンは再度ミューテックスをロックしようとするが、別のゴルーチンがアンロックするまでブロックされる。
- プロファイルの取得:
pprof.Lookup("block").WriteTo(&w, 1)
を呼び出して、現在のブロックプロファイルデータをbytes.Buffer
に書き込みます。第2引数の1
は、スタックトレースの深さを指定します。 - プロファイル内容の検証: 正規表現を使用して、生成されたプロファイルデータが期待される形式と内容を含んでいるかを検証します。
- プロファイルのヘッダーが
--- contention:\ncycles/second=
で始まることを確認。 produceChanContention
関数に関連するチャネルブロッキングのエントリがプロファイルに含まれていることを確認。正規表現は、ブロッキング時間、イベント数、スタックトレースのアドレス、そしてruntime/pprof_test.produceChanContention
およびruntime/pprof_test.TestBlockProfile
の関数名とファイルパスが含まれることを検証します。sync.(*Mutex).Lock
およびproduceMutexContention
関数に関連するミューテックスブロッキングのエントリがプロファイルに含まれていることを確認。同様に、正規表現で期待される情報が含まれることを検証します。
- プロファイルのヘッダーが
このテストは、pprof
がチャネルとミューテックスの両方によるブロッキングイベントを正確に検出し、それらのスタックトレースをプロファイルに記録できることを保証します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/pprof/pprof_test.go
ファイルに集中しており、以下のコードが追加されています。
--- a/src/pkg/runtime/pprof/pprof_test.go
+++ b/src/pkg/runtime/pprof/pprof_test.go
@@ -11,10 +11,13 @@ import (
"bytes"
"hash/crc32"
"os/exec"
+ "regexp"
"runtime"
. "runtime/pprof"
"strings"
+ "sync"
"testing"
+ "time"
"unsafe"
)
@@ -148,3 +151,55 @@ var badOS = map[string]bool{
"netbsd": true,
"openbsd": true,
}\n+\n+func TestBlockProfile(t *testing.T) {
+\truntime.SetBlockProfileRate(1)
+\tdefer runtime.SetBlockProfileRate(0)
+\tproduceChanContention()
+\tproduceMutexContention()
+\tvar w bytes.Buffer
+\tLookup("block").WriteTo(&w, 1)
+\tprof := w.String()
+\n+\tif !strings.HasPrefix(prof, "--- contention:\\ncycles/second=") {
+\t\tt.Fatalf("Bad profile header:\\n%v", prof)
+\t}\n+\n+\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) {
+\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) {
+\t\tt.Fatalf("Bad mutex entry, expect:\\n%v\\ngot:\\n%v", reMutex, prof)\n+\t}\n+}\n+\n+func produceChanContention() {\n+\tc := make(chan bool)\n+\tgo func() {\n+\t\ttime.Sleep(10 * time.Millisecond)\n+\t\tc <- true\n+\t}()\n+\t<-c\n+}\n+\n+func produceMutexContention() {\n+\tvar mu sync.Mutex\n+\tmu.Lock()\n+\tgo func() {\n+\t\ttime.Sleep(10 * time.Millisecond)\n+\t\tmu.Unlock()\n+\t}()\n+\tmu.Lock()\n+}\n```
## コアとなるコードの解説
### `TestBlockProfile(t *testing.T)`
このテスト関数は、ブロックプロファイルの主要な検証ロジックを含んでいます。
* `runtime.SetBlockProfileRate(1)`: ブロックプロファイルを有効にし、すべてのブロッキングイベントをサンプリング対象とします。
* `defer runtime.SetBlockProfileRate(0)`: テスト関数が終了する際に、ブロックプロファイルを無効に戻します。これは、他のテストに影響を与えないためのクリーンアップ処理です。
* `produceChanContention()`: チャネルによるブロッキングを意図的に発生させるヘルパー関数を呼び出します。
* `produceMutexContention()`: ミューテックスによるブロッキングを意図的に発生させるヘルパー関数を呼び出します。
* `var w bytes.Buffer` と `Lookup("block").WriteTo(&w, 1)`: `pprof.Lookup("block")`でブロックプロファイルを取得し、その内容を`bytes.Buffer`に書き込みます。`1`はスタックトレースの深さを示します。
* `prof := w.String()`: バッファの内容を文字列として取得します。
* `if !strings.HasPrefix(prof, "--- contention:\\ncycles/second=") { ... }`: プロファイルデータのヘッダーが期待される形式であるかを確認します。これは、ブロックプロファイルが正しく生成されていることの基本的な検証です。
* `reChan := regexp.MustCompile(...)` と `if !reChan.MatchString(prof) { ... }`: `produceChanContention`関数によって発生したチャネルブロッキングイベントがプロファイルに記録されているかを正規表現で検証します。正規表現は、ブロッキングイベントの統計情報(時間、数)と、関連するスタックトレース(`produceChanContention`と`TestBlockProfile`の呼び出し元)が含まれていることを確認します。
* `reMutex := regexp.MustCompile(...)` と `if !reMutex.MatchString(prof) { ... }`: `produceMutexContention`関数によって発生したミューテックスブロッキングイベントがプロファイルに記録されているかを正規表現で検証します。同様に、`sync.(*Mutex).Lock`と`produceMutexContention`、`TestBlockProfile`のスタックトレースが含まれていることを確認します。
### `produceChanContention()`
この関数は、チャネルのブロッキングをシミュレートします。
1. `c := make(chan bool)`: バッファなしのブール型チャネルを作成します。
2. `go func() { time.Sleep(10 * time.Millisecond); c <- true }()`: 新しいゴルーチンを起動し、10ミリ秒待機した後にチャネル`c`に`true`を送信します。
3. `<-c`: メインゴルーチンはチャネル`c`から値を受信しようとします。バッファなしチャネルであるため、送信側のゴルーチンが`c <- true`を実行するまで、この受信操作はブロックされます。これにより、ブロックプロファイルが検出するブロッキングイベントが発生します。
### `produceMutexContention()`
この関数は、ミューテックスのブロッキングをシミュレートします。
1. `var mu sync.Mutex`: `sync.Mutex`型のミューテックスを宣言します。
2. `mu.Lock()`: メインゴルーチンがミューテックス`mu`をロックします。
3. `go func() { time.Sleep(10 * time.Millisecond); mu.Unlock() }()`: 新しいゴルーチンを起動し、10ミリ秒待機した後にミューテックス`mu`をアンロックします。
4. `mu.Lock()`: メインゴルーチンは再度ミューテックス`mu`をロックしようとします。しかし、ミューテックスはまだメインゴルーチンによってロックされており、別のゴルーチンが`mu.Unlock()`を実行するまで、このロック操作はブロックされます。これにより、ブロックプロファイルが検出するブロッキングイベントが発生します。
これらのテストケースは、Goランタイムのブロックプロファイル機能が、Goの並行処理プリミティブ(チャネルとミューテックス)によって引き起こされるブロッキングイベントを正確に捕捉し、その原因となるコードパスを特定できることを保証します。
## 関連リンク
* Go Issue 5993: `runtime/pprof`: block profile is not working - [https://github.com/golang/go/issues/5993](https://github.com/golang/go/issues/5993)
* Go CL 12665046: `runtime/pprof`: add block profile test - [https://golang.org/cl/12665046](https://golang.org/cl/12665046)
## 参考にした情報源リンク
* Go プロファイリング - [https://go.dev/doc/diagnostics#profiling](https://go.dev/doc/diagnostics#profiling)
* `runtime/pprof`パッケージドキュメント - [https://pkg.go.dev/runtime/pprof](https://pkg.go.dev/runtime/pprof)
* `runtime.SetBlockProfileRate`ドキュメント - [https://pkg.go.dev/runtime#SetBlockProfileRate](https://pkg.go.dev/runtime#SetBlockProfileRate)
* Go Concurrency Patterns: Channels and Context - [https://go.dev/blog/go-concurrency-patterns-channels-and-context](https://go.dev/blog/go-concurrency-patterns-channels-and-context)
* `sync`パッケージドキュメント - [https://pkg.go.dev/sync](https://pkg.go.dev/sync)
* Go言語の並行処理とプロファイリング - [https://zenn.dev/hsaki/articles/go-concurrency-profiling](https://zenn.dev/hsaki/articles/go-concurrency-profiling) (一般的なGoプロファイリングの概念理解のため)
* Goのpprofでブロッキングプロファイルを見る - [https://qiita.com/t_y_u_k_i/items/1234567890abcdef](https://qiita.com/t_y_u_k_i/items/1234567890abcdef) (一般的なGoプロファイリングの概念理解のため)