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

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

このコミットは、Go言語のプロファイリングツールであるpprofパッケージにProfile型を導入し、プロファイリングAPIの統一と拡張性向上を図るものです。これにより、クライアントコードが独自のプロファイルを管理できるようになり、既存の組み込みプロファイルもこの新しい型でモデル化されることで、APIの表面積が削減され、より一貫性のあるインターフェースが提供されます。

コミット

commit ebae73bb24c7d4473055353ece1a82b0370f069f
Author: Russ Cox <rsc@golang.org>
Date:   Wed Feb 22 21:46:12 2012 -0500

    pprof: add Profile type
    
    Makes it possible for client code to maintain its own profiles,
    and also reduces the API surface by giving us a type that
    models built-in profiles.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5684056

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

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

元コミット内容

このコミットは、Go言語の標準ライブラリの一部であるpprofパッケージに、プロファイルデータを抽象化するための新しい型Profileを追加します。これにより、ユーザーが独自のプロファイルを作成・管理できるようになるとともに、既存のヒープ、ゴルーチン、スレッド作成などの組み込みプロファイルもこの新しいProfile型で統一的に扱えるようになります。結果として、pprofのAPIがよりシンプルで一貫性のあるものになります。

変更の背景

Go言語の初期のpprofパッケージでは、ヒーププロファイルやスレッド作成プロファイルなどがそれぞれ独立した関数として提供されており、APIが分散していました。また、ユーザーがアプリケーション固有のカスタムプロファイルを作成・管理するための標準的なメカニズムがありませんでした。

このコミットの背景には、以下の課題意識があったと考えられます。

  1. APIの一貫性の欠如: 各プロファイルが個別の関数で提供されていたため、プロファイルの種類ごとに異なるAPIを覚える必要がありました。
  2. 拡張性の不足: ユーザーが独自のイベント(例: ファイルディスクリプタのリーク、データベース接続のリークなど)をプロファイルしたい場合に、pprofの既存の枠組みに組み込むことが困難でした。
  3. net/http/pprofの機能強化: /debug/pprof/エンドポイントで利用可能なプロファイルを一覧表示する機能や、より汎用的なプロファイルハンドリングの必要性がありました。

これらの課題を解決するために、プロファイルデータを抽象化するProfile型を導入し、APIを統一することで、pprofパッケージの使いやすさと拡張性を向上させることを目指しました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびプロファイリングに関する基本的な知識が必要です。

  • Go言語のパッケージシステム: Goのコードはパッケージに分割されており、import文で他のパッケージの機能を利用します。src/pkg/net/http/pprofsrc/pkg/runtime/pprofは、それぞれHTTPサーバー経由でプロファイルを提供するパッケージと、ランタイムレベルのプロファイルデータを提供するパッケージです。
  • pprofツール: Go言語に付属するプロファイリングツールで、CPU使用率、メモリ割り当て、ゴルーチン、ブロックなどのプロファイルデータを収集・可視化するために使用されます。通常、go tool pprof <URL>のようにHTTPエンドポイントからプロファイルデータを取得します。
  • プロファイルの種類:
    • CPUプロファイル: プログラムがCPU時間をどこで消費しているかを示します。
    • ヒーププロファイル: プログラムがメモリをどこで割り当てているか、どのオブジェクトがメモリを消費しているかを示します。
    • ゴルーチンプロファイル: 現在実行中のすべてのゴルーチンのスタックトレースを示します。デッドロックやゴルーチンリークの検出に役立ちます。
    • スレッド作成プロファイル: 新しいOSスレッドがどこで作成されたかを示します。
  • スタックトレース: プログラムの実行中に、現在呼び出されている関数のリストです。プロファイリングでは、特定のイベント(メモリ割り当てなど)が発生した時点でのスタックトレースを記録し、そのイベントがどのコードパスで発生したかを特定します。
  • io.Writerインターフェース: Go言語の標準ライブラリで定義されているインターフェースで、データを書き込むための抽象化を提供します。pprofのプロファイルデータは通常、このインターフェースを介してファイルやHTTPレスポンスライターに書き込まれます。
  • sync.Mutex: Go言語の標準ライブラリで提供されるミューテックス(相互排他ロック)で、複数のゴルーチンからの共有データへのアクセスを同期するために使用されます。プロファイルデータへの同時アクセスを防ぐために利用されます。
  • html/templateパッケージ: Go言語の標準ライブラリで提供されるHTMLテンプレートエンジンです。このコミットでは、/debug/pprof/のインデックスページを動的に生成するために使用されています。
  • runtimeパッケージ: Go言語のランタイムシステムと対話するための機能を提供します。プロファイリングデータ(ヒープ、ゴルーチン、スレッドなど)の収集には、このパッケージの関数が使用されます。

技術的詳細

このコミットの主要な技術的変更点は、runtime/pprofパッケージにProfile型を導入し、既存のプロファイルメカニズムをこの新しい型に統合したことです。

runtime/pprofパッケージの変更

  1. Profile構造体の定義:

    type Profile struct {
        name  string
        mu    sync.Mutex
        m     map[interface{}][]uintptr // カスタムプロファイル用: 値とスタックトレースのマップ
        count func() int                // 組み込みプロファイル用: プロファイル内の要素数を返す関数
        write func(io.Writer, int) error // 組み込みプロファイル用: プロファイルデータを書き込む関数
    }
    

    この構造体は、プロファイルの名前、ミューテックス、そしてカスタムプロファイル用のスタックトレースマップ、組み込みプロファイル用のカウント関数と書き込み関数を保持します。

  2. 組み込みプロファイルのProfile型への移行: goroutineProfilethreadcreateProfileheapProfileといった組み込みプロファイルが、それぞれProfile型のインスタンスとして定義されました。これらのインスタンスは、それぞれのプロファイルに特化したcount関数とwrite関数を持ちます。

  3. プロファイル管理APIの追加:

    • NewProfile(name string) *Profile: 新しいカスタムプロファイルを作成します。既に同じ名前のプロファイルが存在する場合はパニックします。
    • Lookup(name string) *Profile: 指定された名前のプロファイルを検索して返します。
    • Profiles() []*Profile: 現在登録されているすべてのプロファイルのリストを返します。
  4. Profile型のメソッド:

    • Name() string: プロファイルの名前を返します。
    • Count() int: プロファイル内の要素数を返します。カスタムプロファイルの場合はマップの要素数、組み込みプロファイルの場合は対応するcount関数を呼び出します。
    • Add(value interface{}, skip int): カスタムプロファイルに現在の実行スタックを追加します。valueはスタックトレースに関連付けられる任意のキーで、skipはスタックトレースの開始位置を調整します。組み込みプロファイルに対して呼び出された場合はパニックします。
    • Remove(value interface{}): カスタムプロファイルから指定されたvalueに関連付けられたスタックトレースを削除します。
    • WriteTo(w io.Writer, debug int) error: プロファイルのスナップショットをio.Writerに書き込みます。debugパラメータは出力の詳細度を制御します。
  5. 汎用的なプロファイル書き込みロジック: printCountProfile関数が導入され、countProfileインターフェース(スタックトレースのリストを抽象化)を実装する任意のプロファイルに対して、カウントとスタックトレースを整形して出力できるようになりました。これにより、ヒーププロファイルやスレッド作成プロファイルなどの出力ロジックが共通化されました。

net/http/pprofパッケージの変更

  1. Indexハンドラの導入: /debug/pprof/へのリクエストを処理するIndexハンドラが追加されました。このハンドラは、runtime/pprof.Profiles()を呼び出して利用可能なすべてのプロファイルのリストを取得し、html/templateを使用してそれらをHTMLページとして表示します。これにより、ユーザーはブラウザで/debug/pprof/にアクセスするだけで、利用可能なプロファイルの一覧とリンクを確認できるようになりました。

  2. 汎用的なHandler関数の追加: Handler(name string) http.Handler関数が追加されました。これは、指定された名前のプロファイルに対応するhttp.Handlerを返します。このハンドラは、runtime/pprof.Lookup()でプロファイルを取得し、そのWriteToメソッドを呼び出してHTTPレスポンスとしてプロファイルデータを出力します。

  3. 既存ハンドラの統合: 以前は個別のhttp.Handle呼び出しで登録されていたheapthreadプロファイルのハンドラが削除され、代わりにIndexハンドラと汎用的なHandler関数を通じて提供されるようになりました。これにより、net/http/pprofのコードが簡素化され、新しいプロファイルが追加された場合でも、http.Handleの呼び出しを追加する必要がなくなりました。

text/tabwriter/tabwriter_test.goの変更

このファイルへの変更は非常に軽微で、テストパッケージの名前をtabwriterからtabwriter_testに変更し、. "text/tabwriter"をインポートすることで、テストコードがtabwriterパッケージの公開されたシンボルにアクセスできるようにしたものです。これは、Goのテストパッケージの慣例に合わせた変更であり、本コミットの主要な機能変更とは直接関係ありませんが、関連するリファクタリングの一部として行われました。

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

src/pkg/net/http/pprof/pprof.go

--- a/src/pkg/net/http/pprof/pprof.go
+++ b/src/pkg/net/http/pprof/pprof.go
@@ -22,9 +22,9 @@
 //
 //	go tool pprof http://localhost:6060/debug/pprof/profile
 //
-// Or to look at the thread creation profile:
+// Or to view all available profiles:
 //
-//	go tool pprof http://localhost:6060/debug/pprof/thread
+//	go tool pprof http://localhost:6060/debug/pprof/
 //
 // For a study of the facility in action, visit
 //
@@ -36,7 +36,9 @@ import (
 	"bufio"
 	"bytes"
 	"fmt"
+	"html/template"
 	"io"
+	"log"
 	"net/http"
 	"os"
 	"runtime"
@@ -47,11 +49,10 @@ import (
 )
 
 func init() {
+	http.Handle("/debug/pprof/", http.HandlerFunc(Index))
 	http.Handle("/debug/pprof/cmdline", http.HandlerFunc(Cmdline))
 	http.Handle("/debug/pprof/profile", http.HandlerFunc(Profile))
-	http.Handle("/debug/pprof/heap", http.HandlerFunc(Heap))
 	http.Handle("/debug/pprof/symbol", http.HandlerFunc(Symbol))
-	http.Handle("/debug/pprof/thread", http.HandlerFunc(Thread))
 }
 
 // Cmdline responds with the running program's
@@ -62,20 +63,6 @@ func Cmdline(w http.ResponseWriter, r *http.Request) {
 	fmt.Fprintf(w, strings.Join(os.Args, "\x00"))
 }
 
-// Heap responds with the pprof-formatted heap profile.
-// The package initialization registers it as /debug/pprof/heap.
-func Heap(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-	pprof.WriteHeapProfile(w)
-}
-
-// Thread responds with the pprof-formatted thread creation profile.
-// The package initialization registers it as /debug/pprof/thread.
-func Thread(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-	pprof.WriteThreadProfile(w)
-}
-
 // Profile responds with the pprof-formatted cpu profile.
 // The package initialization registers it as /debug/pprof/profile.
 func Profile(w http.ResponseWriter, r *http.Request) {
@@ -147,3 +134,61 @@ func Symbol(w http.ResponseWriter, r *http.Request) {
 
 	w.Write(buf.Bytes())
 }
+
+// Handler returns an HTTP handler that serves the named profile.
+func Handler(name string) http.Handler {
+	return handler(name)
+}
+
+type handler string
+
+func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	debug, _ := strconv.Atoi(r.FormValue("debug"))
+	p := pprof.Lookup(string(name))
+	if p == nil {
+		w.WriteHeader(404)
+		fmt.Fprintf(w, "Unknown profile: %s\n", name)
+		return
+	}
+	p.WriteTo(w, debug)
+	return
+}
+
+// Index responds with the pprof-formatted profile named by the request.
+// For example, "/debug/pprof/heap" serves the "heap" profile.
+// Index responds to a request for "/debug/pprof/" with an HTML page
+// listing the available profiles.
+func Index(w http.ResponseWriter, r *http.Request) {
+	if strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
+		name := r.URL.Path[len("/debug/pprof/"):]
+		if name != "" {
+			handler(name).ServeHTTP(w, r)
+			return
+		}
+	}
+
+	profiles := pprof.Profiles()
+	if err := indexTmpl.Execute(w, profiles); err != nil {
+		log.Print(err)
+	}
+}
+
+var indexTmpl = template.Must(template.New("index").Parse(`<html>
+<head>
+<title>/debug/pprof/</title>
+</head>
+/debug/pprof/<br>
+<br>
+<body>
+profiles:<br>
+<table>
+{{range .}}
+<tr><td align=right>{{.Count}}<td><a href="/debug/pprof/{{.Name}}?debug=1">{{.Name}}</a>
+{{end}}
+</table>
+<br>
+<a href="/debug/pprof/goroutine?debug=2">full goroutine stack dump</a><br>
+</body>
+</html>
+`))

src/pkg/runtime/pprof/pprof.go

--- a/src/pkg/runtime/pprof/pprof.go
+++ b/src/pkg/runtime/pprof/pprof.go
@@ -10,19 +10,354 @@ package pprof
 
 import (
 	"bufio"
+	"bytes"
 	"fmt"
 	"io"
 	"runtime"
+	"sort"
+	"strings"
 	"sync"
+	"text/tabwriter"
 )
 
 // BUG(rsc): CPU profiling is broken on OS X, due to an Apple kernel bug.
 // For details, see http://code.google.com/p/go/source/detail?r=35b716c94225.
 
-// WriteHeapProfile writes a pprof-formatted heap profile to w.
-// If a write to w returns an error, WriteHeapProfile returns that error.
-// Otherwise, WriteHeapProfile returns nil.
+// A Profile is a collection of stack traces showing the call sequences
+// that led to instances of a particular event, such as allocation.
+// Packages can create and maintain their own profiles; the most common
+// use is for tracking resources that must be explicitly closed, such as files
+// or network connections.
+//
+// A Profile's methods can be called from multiple goroutines simultaneously.
+//
+// Each Profile has a unique name.  A few profiles are predefined:
+//
+//	goroutine    - stack traces of all current goroutines
+//	heap         - a sampling of all heap allocations
+//	threadcreate - stack traces that led to the creation of new OS threads
+//
+// These predefine profiles maintain themselves and panic on an explicit
+// Add or Remove method call.
+//
+// The CPU profile is not available as a Profile.  It has a special API,
+// the StartCPUProfile and StopCPUProfile functions, because it streams
+// output to a writer during profiling.
+//
+type Profile struct {
+	name  string
+	mu    sync.Mutex
+	m     map[interface{}][]uintptr
+	count func() int
+	write func(io.Writer, int) error
+}
+
+// profiles records all registered profiles.
+var profiles struct {
+	mu sync.Mutex
+	m  map[string]*Profile
+}
+
+var goroutineProfile = &Profile{
+	name:  "goroutine",
+	count: countGoroutine,
+	write: writeGoroutine,
+}
+
+var threadcreateProfile = &Profile{
+	name:  "threadcreate",
+	count: countThreadCreate,
+	write: writeThreadCreate,
+}
+
+var heapProfile = &Profile{
+	name:  "heap",
+	count: countHeap,
+	write: writeHeap,
+}
+
+func lockProfiles() {
+	profiles.mu.Lock()
+	if profiles.m == nil {
+		// Initial built-in profiles.
+		profiles.m = map[string]*Profile{
+			"goroutine":    goroutineProfile,
+			"threadcreate": threadcreateProfile,
+			"heap":         heapProfile,
+		}
+	}
+}
+
+func unlockProfiles() {
+	profiles.mu.Unlock()
+}
+
+// NewProfile creates a new profile with the given name.
+// If a profile with that name already exists, NewProfile panics.
+// The convention is to use a 'import/path.' prefix to create
+// separate name spaces for each package.
+func NewProfile(name string) *Profile {
+	lockProfiles()
+	defer unlockProfiles()
+	if name == "" {
+		panic("pprof: NewProfile with empty name")
+	}
+	if profiles.m[name] != nil {
+		panic("pprof: NewProfile name already in use: " + name)
+	}
+	p := &Profile{
+		name: name,
+		m:    map[interface{}][]uintptr{},
+	}
+	profiles.m[name] = p
+	return p
+}
+
+// Lookup returns the profile with the given name, or nil if no such profile exists.
+func Lookup(name string) *Profile {
+	lockProfiles()
+	defer unlockProfiles()
+	return profiles.m[name]
+}
+
+// Profiles returns a slice of all the known profiles, sorted by name.
+func Profiles() []*Profile {
+	lockProfiles()
+	defer unlockProfiles()
+
+	var all []*Profile
+	for _, p := range profiles.m {
+		all = append(all, p)
+	}
+
+	sort.Sort(byName(all))
+	return all
+}
+
+type byName []*Profile
+
+func (x byName) Len() int           { return len(x) }
+func (x byName) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
+func (x byName) Less(i, j int) bool { return x[i].name < x[j].name }
+
+// Name returns this profile's name, which can be passed to Lookup to reobtain the profile.
+func (p *Profile) Name() string {
+	return p.name
+}
+
+// Count returns the number of execution stacks currently in the profile.
+func (p *Profile) Count() int {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	if p.count != nil {
+		return p.count()
+	}
+	return len(p.m)
+}
+
+// Add adds the current execution stack to the profile, associated with value.
+// Add stores value in an internal map, so value must be suitable for use as 
+// a map key and will not be garbage collected until the corresponding
+// call to Remove.  Add panics if the profile already contains a stack for value.
+//
+// The skip parameter has the same meaning as runtime.Caller's skip
+// and controls where the stack trace begins.  Passing skip=0 begins the
+// trace in the function calling Add.  For example, given this
+// execution stack:
+//
+//	Add
+//	called from rpc.NewClient
+//	called from mypkg.Run
+//	called from main.main
+//
+// Passing skip=0 begins the stack trace at the call to Add inside rpc.NewClient.
+// Passing skip=1 begins the stack trace at the call to NewClient inside mypkg.Run.
+//
+func (p *Profile) Add(value interface{}, skip int) {
+	if p.name == "" {
+		panic("pprof: use of uninitialized Profile")
+	}
+	if p.write != nil {
+		panic("pprof: Add called on built-in Profile " + p.name)
+	}
+
+	stk := make([]uintptr, 32)
+	n := runtime.Callers(skip+1, stk[:])
+
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	if p.m[value] != nil {
+		panic("pprof: Profile.Add of duplicate value")
+	}
+	p.m[value] = stk[:n]
+}
+
+// Remove removes the execution stack associated with value from the profile.
+// It is a no-op if the value is not in the profile.
+func (p *Profile) Remove(value interface{}) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	delete(p.m, value)
+}
+
+// WriteTo writes a pprof-formatted snapshot of the profile to w.
+// If a write to w returns an error, WriteTo returns that error.
+// Otherwise, WriteTo returns nil.
+//
+// The debug parameter enables additional output.
+// Passing debug=0 prints only the hexadecimal addresses that pprof needs.
+// Passing debug=1 adds comments translating addresses to function names
+// and line numbers, so that a programmer can read the profile without tools.
+//
+// The predefined profiles may assign meaning to other debug values;
+// for example, when printing the "goroutine" profile, debug=2 means to
+// print the goroutine stacks in the same form that a Go program uses
+// when dying due to an unrecovered panic.
+func (p *Profile) WriteTo(w io.Writer, debug int) error {
+	if p.name == "" {
+		panic("pprof: use of zero Profile")
+	}
+	if p.write != nil {
+		return p.write(w, debug)
+	}
+
+	// Obtain consistent snapshot under lock; then process without lock.
+	var all [][]uintptr
+	p.mu.Lock()
+	for _, stk := range p.m {
+		all = append(all, stk)
+	}
+	p.mu.Unlock()
+
+	// Map order is non-deterministic; make output deterministic.
+	sort.Sort(stackProfile(all))
+
+	return printCountProfile(w, debug, p.name, stackProfile(all))
+}
+
+type stackProfile [][]uintptr
+
+func (x stackProfile) Len() int              { return len(x) }
+func (x stackProfile) Stack(i int) []uintptr { return x[i] }
+func (x stackProfile) Swap(i, j int)         { x[i], x[j] = x[j], x[i] }
+func (x stackProfile) Less(i, j int) bool {
+	t, u := x[i], x[j]
+	for k := 0; k < len(t) && k < len(u); k++ {
+		if t[k] != u[k] {
+			return t[k] < u[k]
+		}
+	}
+	return len(t) < len(u)
+}
+
+// A countProfile is a set of stack traces to be printed as counts
+// grouped by stack trace.  There are multiple implementations:
+// all that matters is that we can find out how many traces there are
+// and obtain each trace in turn.
+type countProfile interface {
+	Len() int
+	Stack(i int) []uintptr
+}
+
+// printCountProfile prints a countProfile at the specified debug level.
+func printCountProfile(w io.Writer, debug int, name string, p countProfile) error {
+	b := bufio.NewWriter(w)
+	var tw *tabwriter.Writer
+	w = b
+	if debug > 0 {
+		tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
+		w = tw
+	}
+
+	fmt.Fprintf(w, "%s profile: total %d\n", name, p.Len())
+
+	// Build count of each stack.
+	var buf bytes.Buffer
+	key := func(stk []uintptr) string {
+		buf.Reset()
+		fmt.Fprintf(&buf, "@")
+		for _, pc := range stk {
+			fmt.Fprintf(&buf, " %#x", pc)
+		}
+		return buf.String()
+	}
+	m := map[string]int{}
+	n := p.Len()
+	for i := 0; i < n; i++ {
+		m[key(p.Stack(i))]++
+	}
+
+	// Print stacks, listing count on first occurrence of a unique stack.
+	for i := 0; i < n; i++ {
+		stk := p.Stack(i)
+		s := key(stk)
+		if count := m[s]; count != 0 {
+			fmt.Fprintf(w, "%d %s\n", count, s)
+			if debug > 0 {
+				printStackRecord(w, stk, false)
+			}
+			delete(m, s)
+		}
+	}
+
+	if tw != nil {
+		tw.Flush()
+	}
+	return b.Flush()
+}
+
+// printStackRecord prints the function + source line information
+// for a single stack trace.
+func printStackRecord(w io.Writer, stk []uintptr, allFrames bool) {
+	show := allFrames
+	for _, pc := range stk {
+		f := runtime.FuncForPC(pc)
+		if f == nil {
+			show = true
+			fmt.Fprintf(w, "#\t%#x\n", pc)
+		} else {
+			file, line := f.FileLine(pc)
+			name := f.Name()
+			// Hide runtime.goexit and any runtime functions at the beginning.
+			// This is useful mainly for allocation traces.
+			if name == "runtime.goexit" || !show && strings.HasPrefix(name, "runtime.") {
+				continue
+			}
+			show = true
+			fmt.Fprintf(w, "#\t%#x\t%s+%#x\t%s:%d\n", pc, f.Name(), pc-f.Entry(), file, line)
+		}
+	}
+	if !show {
+		// We didn't print anything; do it again,
+		// and this time include runtime functions.
+		printStackRecord(w, stk, true)
+		return
+	}
+	fmt.Fprintf(w, "\n")
+}
+
+// Interface to system profiles.
+
+type byInUseBytes []runtime.MemProfileRecord
+
+func (x byInUseBytes) Len() int           { return len(x) }
+func (x byInUseBytes) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
+func (x byInUseBytes) Less(i, j int) bool { return x[i].InUseBytes() > x[j].InUseBytes() }
+
+// WriteHeapProfile is shorthand for Lookup("heap").WriteTo(w, 0).
+// It is preserved for backwards compatibility.
 func WriteHeapProfile(w io.Writer) error {
+	return writeHeap(w, 0)
+}
+
+// countHeap returns the number of records in the heap profile.
+func countHeap() int {
+	n, _ := runtime.MemProfile(nil, false)
+	return n
+}
+
+// writeHeapProfile writes the current runtime heap profile to w.
+func writeHeap(w io.Writer, debug int) error {
 	// Find out how many records there are (MemProfile(nil, false)),
 	// allocate that many records, and get the data.
 	// There's a race—more records might be added between
@@ -44,6 +379,16 @@ func WriteHeapProfile(w io.Writer) error {
 		// Profile grew; try again.
 	}
 
+	sort.Sort(byInUseBytes(p))
+
+	b := bufio.NewWriter(w)
+	var tw *tabwriter.Writer
+	w = b
+	if debug > 0 {
+		tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
+		w = tw
+	}
+
 	var total runtime.MemProfileRecord
 	for i := range p {
 		r := &p[i]
@@ -56,78 +401,120 @@ func WriteHeapProfile(w io.Writer) error {
 	// Technically the rate is MemProfileRate not 2*MemProfileRate,
 	// but early versions of the C++ heap profiler reported 2*MemProfileRate,
 	// so that's what pprof has come to expect.
-	b := bufio.NewWriter(w)
-	fmt.Fprintf(b, "heap profile: %d: %d [%d: %d] @ heap/%d\n",
+	fmt.Fprintf(w, "heap profile: %d: %d [%d: %d] @ heap/%d\n",
 	\ttotal.InUseObjects(), total.InUseBytes(),
 	\ttotal.AllocObjects, total.AllocBytes,
 	\t2*runtime.MemProfileRate)
 
 	for i := range p {
 		r := &p[i]
-		fmt.Fprintf(b, "%d: %d [%d: %d] @",
+		fmt.Fprintf(w, "%d: %d [%d: %d] @",
 		\tr.InUseObjects(), r.InUseBytes(),
 		\tr.AllocObjects, r.AllocBytes)
 		for _, pc := range r.Stack() {
-			fmt.Fprintf(b, " %#x", pc)
+			fmt.Fprintf(w, " %#x", pc)
+		}
+		fmt.Fprintf(w, "\n")
+		if debug > 0 {
+			printStackRecord(w, r.Stack(), false)
 		}
-		fmt.Fprintf(b, "\n")
 	}
 
 	// Print memstats information too.
-	// Pprof will ignore, but useful for people.
-	s := new(runtime.MemStats)
-	runtime.ReadMemStats(s)
-	fmt.Fprintf(b, "\n# runtime.MemStats\n")
-	fmt.Fprintf(b, "# Alloc = %d\n", s.Alloc)
-	fmt.Fprintf(b, "# TotalAlloc = %d\n", s.TotalAlloc)
-	fmt.Fprintf(b, "# Sys = %d\n", s.Sys)
-	fmt.Fprintf(b, "# Lookups = %d\n", s.Lookups)
-	fmt.Fprintf(b, "# Mallocs = %d\n", s.Mallocs)
-
-	fmt.Fprintf(b, "# HeapAlloc = %d\n", s.HeapAlloc)
-	fmt.Fprintf(b, "# HeapSys = %d\n", s.HeapSys)
-	fmt.Fprintf(b, "# HeapIdle = %d\n", s.HeapIdle)
-	fmt.Fprintf(b, "# HeapInuse = %d\n", s.HeapInuse)
-
-	fmt.Fprintf(b, "# Stack = %d / %d\n", s.StackInuse, s.StackSys)
-	fmt.Fprintf(b, "# MSpan = %d / %d\n", s.MSpanInuse, s.MSpanSys)
-	fmt.Fprintf(b, "# MCache = %d / %d\n", s.MCacheInuse, s.MCacheSys)
-	fmt.Fprintf(b, "# BuckHashSys = %d\n", s.BuckHashSys)
-
-	fmt.Fprintf(b, "# NextGC = %d\n", s.NextGC)
-	fmt.Fprintf(b, "# PauseNs = %d\n", s.PauseNs)
-	fmt.Fprintf(b, "# NumGC = %d\n", s.NumGC)
-	fmt.Fprintf(b, "# EnableGC = %v\n", s.EnableGC)
-	fmt.Fprintf(b, "# DebugGC = %v\n", s.DebugGC)
-
-	fmt.Fprintf(b, "# BySize = Size * (Active = Mallocs - Frees)\n")
-	fmt.Fprintf(b, "# (Excluding large blocks.)\n")
-	for _, t := range s.BySize {
-		if t.Mallocs > 0 {
-			fmt.Fprintf(b, "#   %d * (%d = %d - %d)\n", t.Size, t.Mallocs-t.Frees, t.Mallocs, t.Frees)
-		}
+	// Pprof will ignore, but useful for people
+	if debug > 0 {
+		s := new(runtime.MemStats)
+		runtime.ReadMemStats(s)
+		fmt.Fprintf(w, "\n# runtime.MemStats\n")
+		fmt.Fprintf(w, "# Alloc = %d\n", s.Alloc)
+		fmt.Fprintf(w, "# TotalAlloc = %d\n", s.TotalAlloc)
+		fmt.Fprintf(w, "# Sys = %d\n", s.Sys)
+		fmt.Fprintf(w, "# Lookups = %d\n", s.Lookups)
+		fmt.Fprintf(w, "# Mallocs = %d\n", s.Mallocs)
+
+		fmt.Fprintf(w, "# HeapAlloc = %d\n", s.HeapAlloc)
+		fmt.Fprintf(w, "# HeapSys = %d\n", s.HeapSys)
+		fmt.Fprintf(w, "# HeapIdle = %d\n", s.HeapIdle)
+		fmt.Fprintf(w, "# HeapInuse = %d\n", s.HeapInuse)
+
+		fmt.Fprintf(w, "# Stack = %d / %d\n", s.StackInuse, s.StackSys)
+		fmt.Fprintf(w, "# MSpan = %d / %d\n", s.MSpanInuse, s.MSpanSys)
+		fmt.Fprintf(w, "# MCache = %d / %d\n", s.MCacheInuse, s.MCacheSys)
+		fmt.Fprintf(w, "# BuckHashSys = %d\n", s.BuckHashSys)
+
+		fmt.Fprintf(w, "# NextGC = %d\n", s.NextGC)
+		fmt.Fprintf(w, "# PauseNs = %d\n", s.PauseNs)
+		fmt.Fprintf(w, "# NumGC = %d\n", s.NumGC)
+		fmt.Fprintf(w, "# EnableGC = %v\n", s.EnableGC)
+		fmt.Fprintf(w, "# DebugGC = %v\n", s.DebugGC)
 	}
-	return b.Flush()
+
+	if tw != nil {
+		tw.Flush()
+	}
+	return b.Flush()
 }
 
-// WriteThreadProfile writes a pprof-formatted thread creation profile to w.
-// If a write to w returns an error, WriteThreadProfile returns that error.
-// Otherwise, WriteThreadProfile returns nil.
-func WriteThreadProfile(w io.Writer) error {
-	// Find out how many records there are (ThreadProfile(nil)),
+// countThreadCreate returns the size of the current ThreadCreateProfile.
+func countThreadCreate() int {
+	n, _ := runtime.ThreadCreateProfile(nil)
+	return n
+}
+
+// writeThreadCreate writes the current runtime ThreadCreateProfile to w.
+func writeThreadCreate(w io.Writer, debug int) error {
+	return writeRuntimeProfile(w, debug, "threadcreate", runtime.ThreadCreateProfile)
+}
+
+// countGoroutine returns the number of goroutines.
+func countGoroutine() int {
+	return runtime.NumGoroutine()
+}
+
+// writeGoroutine writes the current runtime GoroutineProfile to w.
+func writeGoroutine(w io.Writer, debug int) error {
+	if debug >= 2 {
+		return writeGoroutineStacks(w)
+	}
+	return writeRuntimeProfile(w, debug, "goroutine", runtime.GoroutineProfile)
+}
+
+func writeGoroutineStacks(w io.Writer) error {
+	// We don't know how big the buffer needs to be to collect
+	// all the goroutines.  Start with 1 MB and try a few times, doubling each time.
+	// Give up and use a truncated trace if 64 MB is not enough.
+	buf := make([]byte, 1<<20)
+	for i := 0; ; i++ {
+		n := runtime.Stack(buf, true)
+		if n < len(buf) {
+			buf = buf[:n]
+			break
+		}
+		if len(buf) >= 64<<20 {
+			// Filled 64 MB - stop there.
+			break
+		}
+		buf = make([]byte, 2*len(buf))
+	}
+	_, err := w.Write(buf)
+	return err
+}
+
+func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runtime.StackRecord) (int, bool)) error {
+	// Find out how many records there are (fetch(nil)),
 	// allocate that many records, and get the data.
-	// There's a race—more records (threads) might be added between
+	// There's a race—more records might be added between
 	// the two calls—so allocate a few extra records for safety
 	// and also try again if we're very unlucky.
 	// The loop should only execute one iteration in the common case.
-	var p []runtime.ThreadProfileRecord
-	n, ok := runtime.ThreadProfile(nil)
+	var p []runtime.StackRecord
+	n, ok := fetch(nil)
 	for {
 		// Allocate room for a slightly bigger profile,
 		// in case a few more entries have been added
 		// since the call to ThreadProfile.
-		p = make([]runtime.ThreadProfileRecord, n+10)
-		n, ok = runtime.ThreadProfile(p)
+		p = make([]runtime.StackRecord, n+10)
+		n, ok = fetch(p)
 		if ok {
 			p = p[0:n]
 			break
@@ -135,19 +522,14 @@ func WriteThreadProfile(w io.Writer) error {
 		// Profile grew; try again.
 	}
 
-	b := bufio.NewWriter(w)
-	fmt.Fprintf(b, "thread creation profile: %d threads\n", n)
-	for i := range p {
-		r := &p[i]
-		fmt.Fprintf(b, "@")
-		for _, pc := range r.Stack() {
-			fmt.Fprintf(b, " %#x", pc)
-		}
-		fmt.Fprintf(b, "\n")
-	}
-	return b.Flush()
+	return printCountProfile(w, debug, name, runtimeProfile(p))
 }
 
+type runtimeProfile []runtime.StackRecord
+
+func (p runtimeProfile) Len() int              { return len(p) }
+func (p runtimeProfile) Stack(i int) []uintptr { return p[i].Stack() }
+
 var cpu struct {
 	sync.Mutex
 	profiling bool

コアとなるコードの解説

runtime/pprof/pprof.go

このファイルは、Profile型の定義とその管理ロジック、そして組み込みプロファイル(ゴルーチン、ヒープ、スレッド作成)をProfile型に適合させるための変更を含んでいます。

  • Profile構造体: プロファイルデータの抽象化の中心となる型です。

    • name: プロファイルの一意な名前。
    • mu: Profileインスタンスへの同時アクセスを保護するためのミューテックス。
    • m: カスタムプロファイルで使用されるマップ。interface{}型のキーと、それに関連付けられたスタックトレース([]uintptr)を格納します。これにより、ユーザーは任意のオブジェクトをキーとしてプロファイルデータを追跡できます。
    • count func() int: 組み込みプロファイルの場合に、プロファイル内の要素数を動的に取得するための関数ポインタ。
    • write func(io.Writer, int) error: 組み込みプロファイルの場合に、プロファイルデータを指定されたライターに書き込むための関数ポインタ。
  • profilesグローバル変数: 登録されているすべてのProfileインスタンスを管理するためのマップです。sync.Mutexで保護されており、複数のゴルーチンからの安全なアクセスを保証します。

  • 組み込みプロファイルの初期化: goroutineProfile, threadcreateProfile, heapProfileProfile型のインスタンスとして定義され、それぞれのcount関数とwrite関数が設定されています。これにより、これらの組み込みプロファイルも新しいProfile型のインターフェースを通じてアクセスできるようになります。

  • NewProfile, Lookup, Profiles関数:

    • NewProfileは、新しいカスタムプロファイルを作成し、profilesマップに登録します。
    • Lookupは、名前でプロファイルを検索します。
    • Profilesは、登録されているすべてのプロファイルのリストを返します。これはnet/http/pprofのインデックスページで利用されます。
  • Profileのメソッド (Add, Remove, WriteTo):

    • AddRemoveは、カスタムプロファイルにスタックトレースを追加・削除するためのメソッドです。これにより、ユーザーはアプリケーション固有のイベントをプロファイルできます。
    • WriteToは、プロファイルデータをio.Writerに書き込むための汎用的なメソッドです。カスタムプロファイルの場合はmマップからスタックトレースを抽出し、組み込みプロファイルの場合は設定されたwrite関数を呼び出します。
  • printCountProfile関数: 複数のスタックトレースをカウントし、整形して出力するための共通ロジックを提供します。これにより、ヒーププロファイルやゴルーチンプロファイルなどの出力形式が統一され、コードの重複が削減されました。

net/http/pprof/pprof.go

このファイルは、HTTPサーバー経由でプロファイルを提供する部分の変更を含んでいます。

  • init関数: パッケージの初期化時にHTTPハンドラを登録します。

    • 以前はheapthreadプロファイルに個別のハンドラが登録されていましたが、これらは削除され、/debug/pprof/Indexハンドラと汎用的なHandler関数に置き換えられました。
    • http.Handle("/debug/pprof/", http.HandlerFunc(Index))が追加され、/debug/pprof/へのリクエストが新しいIndexハンドラによって処理されるようになりました。
  • Handler関数とhandler:

    • Handler(name string) http.Handlerは、指定されたプロファイル名に対応するHTTPハンドラを生成します。
    • 内部的にはhandlerという文字列型を定義し、そのServeHTTPメソッドを実装することでhttp.Handlerインターフェースを満たしています。
    • このServeHTTPメソッドは、runtime/pprof.Lookup()でプロファイルを取得し、そのWriteToメソッドを呼び出してプロファイルデータをHTTPレスポンスとして書き込みます。これにより、/debug/pprof/heap/debug/pprof/goroutineのようなURLが、汎用的なメカニズムで処理されるようになりました。
  • Index関数:

    • /debug/pprof/へのリクエストを処理します。
    • もしURLパスが/debug/pprof/で始まり、その後にプロファイル名が続く場合は、そのプロファイルに対応するhandlerを呼び出します。
    • そうでなければ(つまり、/debug/pprof/自体へのアクセスの場合)、runtime/pprof.Profiles()を呼び出して利用可能なすべてのプロファイルのリストを取得します。
    • 取得したプロファイルリストをindexTmplというHTMLテンプレートに渡して実行し、プロファイルの一覧を含むHTMLページを生成してクライアントに返します。
  • indexTmpl変数: 利用可能なプロファイルのリストを表示するためのHTMLテンプレートです。これにより、ユーザーはブラウザで/debug/pprof/にアクセスするだけで、どのプロファイルが利用可能か、そしてそれぞれのプロファイルにアクセスするためのリンクを視覚的に確認できるようになりました。

これらの変更により、pprofパッケージはよりモジュール化され、拡張性が向上し、ユーザーフレンドリーなインターフェースを提供するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • pprofツールの一般的な概念に関する情報
  • Go言語のhtml/templateパッケージのドキュメント
  • Go言語のsyncパッケージのドキュメント
  • Go言語のruntimeパッケージのドキュメント
  • Go言語のioパッケージのドキュメント
  • Go言語のnet/httpパッケージのドキュメント
  • Go言語のtext/tabwriterパッケージのドキュメント
  • Go言語のテストに関する慣例