[インデックス 13746] ファイルの概要
このコミットは、Go言語のランタイムにおけるCPUプロファイリング機能の改善に関するものです。具体的には、CPUプロファイルデータの末尾に「ログ終了マーカー」を出力するように変更し、それに対応するテストを追加しています。これにより、プロファイルデータの解析がより堅牢になります。
コミット
commit b2458ff75c75c9fafe1b5f4e0521d4949cd3754e
Author: Alan Donovan <adonovan@google.com>
Date: Tue Sep 4 14:34:03 2012 -0400
runtime/pprof: emit end-of-log marker at end of CPU profile.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6489065
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b2458ff75c75c9fafe1b5f4e0521d4949cd3754e
元コミット内容
runtime/pprof: emit end-of-log marker at end of CPU profile.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6489065
変更の背景
Go言語のCPUプロファイラは、プログラムの実行中にCPUがどの関数に時間を費やしているかをサンプリングし、その情報をログとして出力します。このログは通常、pprof
ツールによって解析され、視覚化されます。
従来のCPUプロファイルデータは、特定の終了マーカーを持っていませんでした。このため、プロファイルデータが何らかの理由で途中で切断された場合(例えば、プログラムがクラッシュした場合や、プロファイル収集が予期せず停止した場合など)、pprof
ツールがデータの終端を正確に識別することが困難でした。データが不完全であるにもかかわらず、ツールがそれを完全なものとして解釈しようとすると、誤った解析結果やエラーを引き起こす可能性がありました。
このコミットは、プロファイルデータの末尾に明示的な「ログ終了マーカー」を導入することで、この問題を解決しようとしています。これにより、pprof
ツールはデータの終端を確実に認識し、不完全なデータに対してもより堅牢な処理を行うことができるようになります。特に、プロファイルデータがストリームとして扱われる場合や、部分的に読み込まれる場合に、このマーカーはデータの整合性を保証する上で非常に重要です。
前提知識の解説
Go言語のプロファイリング (pprof)
Go言語には、標準で強力なプロファイリングツールキット pprof
が組み込まれています。これは、CPU使用率、メモリ割り当て、ゴルーチン、ブロック同期、ミューテックス競合などのパフォーマンスデータを収集し、分析するための機能を提供します。
- CPUプロファイリング: プログラムの実行中に、一定の間隔で現在実行中の関数(スタックトレース)をサンプリングします。これにより、どの関数がCPU時間を最も消費しているかを特定できます。
- プロファイルデータの形式:
pprof
によって生成されるプロファイルデータは、通常、バイナリ形式で出力されます。このバイナリデータは、スタックトレースのリストと、それぞれのスタックトレースがサンプリングされた回数を含んでいます。 pprof
ツール: 収集されたバイナリデータは、go tool pprof
コマンドを使用して解析されます。このツールは、テキスト形式のレポート、コールグラフのSVG画像、Webベースのインタラクティブなビューなど、様々な形式でプロファイル結果を視覚化できます。
Goランタイム (runtime)
Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、スケジューリング、メモリ管理、そしてプロファイリングなどの機能を提供します。src/pkg/runtime/cpuprof.c
のようなファイルは、GoランタイムのC言語で書かれた部分であり、Goプログラムのパフォーマンスに直接影響を与える低レベルの操作を扱います。
uintptr
型
uintptr
は、Go言語における符号なし整数型で、ポインタを保持するのに十分な大きさがあります。これは、ポインタと整数の間で変換を行う際に使用されます。プロファイルデータのような低レベルのバイナリデータを扱う際には、メモリ上のアドレスやオフセットを表現するために頻繁に用いられます。
Lock
と並行処理
cpuprof.c
のようなランタイムのコードでは、複数のゴルーチン(またはスレッド)が同時にプロファイルデータにアクセスする可能性があるため、データの一貫性を保つためにロック(Lock
)が使用されます。これにより、競合状態を防ぎ、データの破損を回避します。
技術的詳細
このコミットの核心は、CPUプロファイルデータの構造に明示的な終端マーカーを導入することです。
src/pkg/runtime/cpuprof.c
の変更点
-
Profile
構造体へのeod_sent
フィールドの追加:struct Profile { // ... 既存のフィールド ... bool eod_sent; // special end-of-data record sent; => flushing };
eod_sent
は、プロファイルデータの末尾に特別な「End-Of-Data (EOD)」レコードが既に送信されたかどうかを示すブーリアンフラグです。これにより、EODマーカーが複数回送信されるのを防ぎます。 -
eod
マーカーの定義:static uintptr eod[3] = {0, 1, 0};
これは、プロファイルデータの終端を示すために使用される3つの
uintptr
値の配列です。この特定のシーケンス{0, 1, 0}
が、データの論理的な終端として認識されます。この値は、プロファイルデータの他の有効なエントリと衝突しないように慎重に選ばれていると推測されます。 -
runtime·SetCPUProfileRate
での初期化:// ... prof->eod_sent = false; // ...
プロファイリングが開始される際に、
eod_sent
フラグはfalse
に初期化されます。 -
breakflush
ロジックの変更:breakflush
セクションは、プロファイルデータがフラッシュされる(ディスクに書き込まれる)際のロジックを扱います。// ... if(!p->eod_sent) { // We may not have space to append this to the partial log buf, // so we always return a new slice for the end-of-data marker. p->eod_sent = true; ret.array = (byte*)eod; ret.len = sizeof eod; ret.cap = ret.len; return ret; } // ...
このコードブロックは、プロファイルデータがすべて処理され、ハッシュテーブルからログに記録すべきエントリがなくなった場合に実行されます。
!p->eod_sent
の条件は、EODマーカーがまだ送信されていないことを確認します。p->eod_sent = true;
でフラグを立て、二重送信を防ぎます。ret.array = (byte*)eod;
は、定義されたeod
マーカーをプロファイルデータとして返します。- コメントにあるように、部分的なログバッファにEODマーカーを追加するスペースがない可能性があるため、常に新しいスライスとしてEODマーカーを返す設計になっています。これにより、EODマーカーが確実にプロファイルデータの末尾に追加されます。
src/pkg/runtime/pprof/pprof_test.go
の変更点
テストファイルでは、この新しいEODマーカーが正しく生成され、pprof
ツールによって認識されることを検証しています。
-
プロファイルデータの長さの調整:
// 変更前: if len(val) < 10 { if l < 13 { // 変更後
プロファイルデータの最小長が10から13に増えています。これは、既存のヘッダー(5要素)と、新しいEODマーカー(3要素)が追加されたため、合計で8要素が増加したことを示唆しています。元のプロファイルデータに加えて、ヘッダーとEODマーカーの分だけ長さが増えることを期待しています。
-
ヘッダーとEODマーカーの分離と検証:
hd, val, tl := val[:5], val[5:l-3], val[l-3:] // ... if hd[0] != 0 || hd[1] != 3 || hd[2] != 0 || hd[3] != 1e6/100 || hd[4] != 0 { t.Fatalf("unexpected header %#x", hd) } // ... if tl[0] != 0 || tl[1] != 1 || tl[2] != 0 { t.Fatalf("malformed end-of-data marker %#x", tl) }
hd
はプロファイルデータの先頭5要素(ヘッダー)を抽出します。tl
はプロファイルデータの末尾3要素(EODマーカー)を抽出します。val
はヘッダーとEODマーカーを除いた実際のプロファイルデータ部分になります。- ヘッダーの検証に加え、
tl
が期待されるEODマーカー{0, 1, 0}
と一致するかどうかが厳密にチェックされます。これにより、ランタイムが正しいマーカーを出力していること、そしてpprof
ツールがそれを正しく解釈できることが保証されます。
これらの変更により、pprof
ツールはプロファイルデータの終端を明確に識別できるようになり、データの解析がより堅牢で信頼性の高いものになります。
コアとなるコードの変更箇所
src/pkg/runtime/cpuprof.c
--- a/src/pkg/runtime/cpuprof.c
+++ b/src/pkg/runtime/cpuprof.c
@@ -99,6 +99,7 @@ struct Profile {
uint32 wtoggle;
bool wholding; // holding & need to release a log half
bool flushing; // flushing hash table - profile is over
+ bool eod_sent; // special end-of-data record sent; => flushing
};
static Lock lk;
@@ -109,6 +110,8 @@ static void add(Profile*, uintptr*, int32);
static bool evict(Profile*, Entry*);
static bool flushlog(Profile*);
+static uintptr eod[3] = {0, 1, 0};
+
// LostProfileData is a no-op function used in profiles
// to mark the number of profiling stack traces that were
// discarded due to slow data writers.
@@ -163,6 +166,7 @@ runtime·SetCPUProfileRate(int32 hz)\n prof->wholding = false;\n prof->wtoggle = 0;\n prof->flushing = false;\n+\t\tprof->eod_sent = false;\n runtime·noteclear(&prof->wait);\n
runtime·setcpuprofilerate(tick, hz);\n
@@ -409,6 +413,16 @@ breakflush:\n }\n
// Made it through the table without finding anything to log.\n+\tif(!p->eod_sent) {\n+\t\t// We may not have space to append this to the partial log buf,\n+\t\t// so we always return a new slice for the end-of-data marker.\n+\t\tp->eod_sent = true;\n+\t\tret.array = (byte*)eod;\n+\t\tret.len = sizeof eod;\n+\t\tret.cap = ret.len;\n+\t\treturn ret;\n+\t}\n+\n // Finally done. Clean up and return nil.\n p->flushing = false;\n if(!runtime·cas(&p->handoff, p->handoff, 0))\n```
### `src/pkg/runtime/pprof/pprof_test.go`
```diff
--- a/src/pkg/runtime/pprof/pprof_test.go
+++ b/src/pkg/runtime/pprof/pprof_test.go
@@ -49,19 +49,25 @@ func TestCPUProfile(t *testing.T) {\n
// Convert []byte to []uintptr.\n bytes := prof.Bytes()\n+\tl := len(bytes) / int(unsafe.Sizeof(uintptr(0)))\n val := *(*[]uintptr)(unsafe.Pointer(&bytes))\n-\tval = val[:len(bytes)/int(unsafe.Sizeof(uintptr(0)))]\n+\tval = val[:l]\n
-\tif len(val) < 10 {\n+\tif l < 13 {\n \t\tt.Fatalf(\"profile too short: %#x\", val)\n \t}\n-\tif val[0] != 0 || val[1] != 3 || val[2] != 0 || val[3] != 1e6/100 || val[4] != 0 {\n-\t\tt.Fatalf(\"unexpected header %#x\", val[:5])\n+\n+\thd, val, tl := val[:5], val[5:l-3], val[l-3:]\n+\tif hd[0] != 0 || hd[1] != 3 || hd[2] != 0 || hd[3] != 1e6/100 || hd[4] != 0 {\n+\t\tt.Fatalf(\"unexpected header %#x\", hd)\n+\t}\n+\n+\tif tl[0] != 0 || tl[1] != 1 || tl[2] != 0 {\n+\t\tt.Fatalf(\"malformed end-of-data marker %#x\", tl)\n \t}\n
// Check that profile is well formed and contains ChecksumIEEE.\n found := false\n-\tval = val[5:]\n \tfor len(val) > 0 {\n \t\tif len(val) < 2 || val[0] < 1 || val[1] < 1 || uintptr(len(val)) < 2+val[1] {\n \t\t\tt.Fatalf(\"malformed profile. leftover: %#x\", val)\n```
## コアとなるコードの解説
### `src/pkg/runtime/cpuprof.c`
このファイルはGoランタイムのCPUプロファイリングの低レベルな実装を担っています。
* **`Profile`構造体**: CPUプロファイリングの状態を管理する構造体です。`eod_sent`フィールドが追加され、プロファイルデータの終端マーカーが送信済みかどうかを追跡します。これにより、マーカーの重複送信を防ぎ、プロファイルデータの整合性を保ちます。
* **`eod`配列**: `static uintptr eod[3] = {0, 1, 0};` は、プロファイルデータの終端を示すための固定のバイトシーケンスを定義しています。このシーケンスは、`pprof`ツールがプロファイルデータの終わりを正確に識別するために使用されます。
* **`runtime·SetCPUProfileRate`関数**: CPUプロファイリングのレートを設定する際に呼び出される関数です。ここで`prof->eod_sent = false;`と初期化されることで、新しいプロファイリングセッションが開始されるたびに、EODマーカーがまだ送信されていない状態から始まることが保証されます。
* **`breakflush`ラベル内のロジック**: この部分が、EODマーカーを実際にプロファイルデータストリームに挿入する役割を担います。プロファイリングが終了し、すべてのプロファイルデータがフラッシュされた後、`!p->eod_sent`の条件が真であれば、`eod`配列がプロファイルデータとして返されます。これにより、`pprof`ツールがデータの終端を認識できるようになります。コメントにあるように、このマーカーは常に新しいスライスとして返されるため、既存のバッファの容量に依存せず、確実にデータに追加されます。
### `src/pkg/runtime/pprof/pprof_test.go`
このファイルは、`runtime/pprof`パッケージのテストケースを含んでいます。
* **`TestCPUProfile`関数**: CPUプロファイリング機能の動作を検証するテスト関数です。
* **プロファイルデータの解析ロジックの更新**:
* `l := len(bytes) / int(unsafe.Sizeof(uintptr(0)))` は、バイトスライスを`uintptr`の配列として解釈した際の要素数を計算します。
* `if l < 13` の変更は、プロファイルデータにEODマーカーが追加されたため、期待される最小長が増加したことを反映しています。
* `hd, val, tl := val[:5], val[5:l-3], val[l-3:]` は、受信したプロファイルデータをヘッダー (`hd`)、実際のプロファイルデータ (`val`)、そして終端マーカー (`tl`) の3つの部分に分割します。この分割により、各部分を個別に検証できるようになります。
* `if tl[0] != 0 || tl[1] != 1 || tl[2] != 0` の行は、受信したプロファイルデータの末尾3要素が、`cpuprof.c`で定義されたEODマーカー `{0, 1, 0}` と正確に一致するかどうかを検証します。これにより、ランタイムが正しい終端マーカーを出力していること、そして`pprof`ツールがそれを正しく解釈できることが保証されます。
これらの変更は、Goのプロファイリングシステムが生成するデータの堅牢性と信頼性を向上させる上で重要な役割を果たします。特に、プロファイルデータが不完全な場合でも、`pprof`ツールがより適切に動作するための基盤を提供します。
## 関連リンク
* Go言語のプロファイリングに関する公式ドキュメント: [https://go.dev/doc/diagnostics#profiling](https://go.dev/doc/diagnostics#profiling)
* `pprof`ツールの詳細: [https://github.com/google/pprof](https://github.com/google/pprof)
* Go言語のソースコードリポジトリ: [https://github.com/golang/go](https://github.com/golang/go)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のソースコード
* `pprof`ツールのドキュメント
* `uintptr`に関するGo言語の仕様
* 一般的なプロファイリングツールの設計原則に関する情報 (Web検索)
* Go言語のランタイムに関する技術記事 (Web検索)