Goのruntimeの内部状態をリアルタイムに視覚化してみる
Realtime Visualization of Go Runtime
goのruntimeのとても興味深く面白い挙動は、次の記事で丁寧にまたわかりやすく解説されています。
当時の私でもその面白さの片鱗を感じとることができた一方、コードリーディングをしてもその実際の動きについてはどこか空を掴むような難しさを感じたことを記憶しています。
そこで、runtimeの内部状態を取得し、その動きを視覚的に理解することができないかを試してみた、というのがこの記事の趣旨です。
runtimeの内部状態をリアルタイムに取得さえできれば、視覚化には任意のツールを使うことができると思います。
私の場合はgo製のゲームエンジン Ebitengine を使って、goのruntimeをgoでリアルタイムにビジュアライズするということをやってみました。
runtimeの内部状態を取得するアプローチ
runtimeの内部状態を詳細に取得できるような関数を探したのですが、私が調べた限りでは見つけることができませんでした。
そこで、runtime2.go
を自分で書き換えて、runtimeの内部状態を取得することができる怪しげな関数を生やすアプローチを思いつきました。
そこから-overlay
を使用して自身が生やした関数が利用できるようにした上で main.go
を起動します。
やりかた
- まず、自身が使用しているgoのversionに対応する
runtime2.go
をいずれかの場所から取得し、overlay用のディレクトリに保存します。 - 保存した
runtime2.go
に以下のような構造体と関数を追加します。goのversionによっては細かな差異があると思うので、その辺りは適宜調整をする必要があるかもしれません。なお、命名がかなり雑であることはお目溢しいただければ幸いです。
type ExposureG struct {
ID uint64
AtomicStatus uint32
}
func NewExposureG(g *g) *ExposureG {
if g == nil {
return nil
}
return &ExposureG{
ID: g.goid,
AtomicStatus: g.atomicstatus.LoadAcquire(),
}
}
type ExposureM struct {
ID int64
CurrentG *ExposureG
G0 *ExposureG
}
func NewExposureM(m *m) *ExposureM {
if m == nil {
return nil
}
return &ExposureM{
ID: m.id,
CurrentG: NewExposureG(m.curg),
G0: NewExposureG(m.g0),
}
}
type ExposureP struct {
ID int32
Status uint32 // 0:Pidle, 1:Prunning, 2:Psyscall, 3:Pstop, 4:Pdead
RunQueue []*ExposureG
M *ExposureM
TimerCount int
RawRunqLen int
}
func NewExposureP(p *p) *ExposureP {
if p == nil {
return nil
}
runq := make([]*ExposureG, 0, len(p.runq))
h := atomic.LoadAcq(&p.runqhead)
t := p.runqtail
if t != h {
runqLen := len(p.runq)
for idx := h % uint32(runqLen); int(idx) < runqLen; idx++ {
eg := NewExposureG(p.runq[idx].ptr())
if eg != nil {
runq = append(runq, eg)
}
}
}
return &ExposureP{
ID: p.id,
Status: p.status,
RunQueue: runq,
M: NewExposureM(p.m.ptr()),
RawRunqLen: len(p.runq),
TimerCount: len(p.timers.heap),
}
}
type Exposure struct {
SchedulerG []*ExposureG
SchedulerP []*ExposureP
AllP []*ExposureP
AllG []*ExposureG
}
func RuntimeExposure() *Exposure {
schedulerGs := make([]*ExposureG, 0, 256)
for g := sched.runq.head.ptr(); g != nil; g = g.schedlink.ptr() {
schedulerGs = append(schedulerGs, NewExposureG(g))
}
schedulerPs := make([]*ExposureP, 0, 256)
for p := sched.pidle.ptr(); p != nil; p = p.link.ptr() {
schedulerPs = append(schedulerPs, NewExposureP(p))
}
allPs := make([]*ExposureP, 0, 256)
for _, p := range allp {
allPs = append(allPs, NewExposureP(p))
}
allGs := make([]*ExposureG, 0, len(allgs))
for _, g := range allgs {
allGs = append(allGs, NewExposureG(g))
}
return &Exposure{
SchedulerG: schedulerGs,
SchedulerP: schedulerPs,
AllP: allPs,
AllG: allGs,
}
}
-
main.go
側で先ほど定義したRuntimeExposure
関数をおもむろに呼び出すようにします。
package main
import (
"fmt"
"runtime"
)
func main() {
result := runtime.RuntimeExposure()
for i := range result.AllP {
fmt.Printf("%#v \n", result.AllP[i])
}
}
-
overlay.json
を作成します。私の場合は以下のようになりました。
{
"Replace":{
"/opt/homebrew/Cellar/go@1.23/1.23.9/libexec/src/runtime/runtime2.go":"./overlay/runtime2.go"
}
}
-
go run -overlay overlay.json .
のように起動します。
すると、main関数実行時のruntimeの内部状態が1回ログに出て終了します。
% go run -overlay overlay.json .
&runtime.ExposureP{ID:0, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:1, Status:0x1, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(0x140000c0000), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:2, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:3, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:4, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:5, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:6, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:7, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:8, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
&runtime.ExposureP{ID:9, Status:0x0, RunQueue:[]*runtime.ExposureG{}, M:(*runtime.ExposureM)(nil), TimerCount:0, RawRunqLen:256}
これでruntimeの内部状態を取得できるようになりました。
あとはebitenginでこの構造体を直接使って表現をしたり、あるいはこの情報を何らかの通信でやり取りし、別のツールで表示したりすることができると思います。
余談
最初はgoのsource code自体を書き換えてbuildした改造runtimeを使っていました。
(こちらの記事を参考にさせていただいていました。)
おわりに
リアルタイムにruntimenの内部状態をビジュアライズできているものの、画面の更新よりもはるかに速くruntimeの内部状態は変化しているので、より連続的な状態変化を観察するには至っていないなと感じています。
とはいえ、やはり視覚化することでruntimeの挙動がぐっとイメージしやすくなったように感じます。
あとは表現次第だと思うので、これからも試行錯誤してみます。
Discussion