<<< Back to Home Page <<<
Embedding Go Pprof
26 Feb 2018
As you know Golang has time-based profiling subsytem and visualization tool called pprof. The pprof is not friendly for use in a manner different than command line tool. However there is a way to embed it in an application to generate some reports in tests/benchmarks cases. I use it in my benchmark module to generate benchmarking report contaning top calls and callgrapth of the significant functions.
1
2
3
4
5
6
func main() {
if err := driver.PProf(&driver.Options{}); err != nil {
fmt.Fprintf(os.Stderr, "pprof: %v\n", err)
os.Exit(2)
}
}
It’s the main module of pprof tool. The package pprof/driver is a frontend to the pprof internals which adopts the оptions and calls to the pprof/internal/driver package.
1
2
3
func PProf(o *Options) error {
return internaldriver.PProf(o.internalOptions())
}
So, there are two resonable questions: what exactly is the pprof/driver.Option and how can I play with it? Since all functionality is in the internal package, the only way to use this funtionality, without modification the original code, to use the both pprof/driver.Option and pprof/driver.Pprof.
The pprof/driver.Options is a set of objects which can control the pprof actions
1
2
3
4
5
6
7
8
type Options struct {
Writer Writer
Flagset FlagSet
Fetch Fetcher
Sym Symbolizer
Obj ObjTool
UI UI
}
The fields Options.Sym and Options.Obj are used to customize symbolyzation. It’s not nesessary to use these fields in most cases.
The Options.UI presents an object manages user interactions
1
2
3
4
5
6
7
type UI interface {
ReadLine(prompt string) (string, error)
Print(...interface{})
PrintErr(...interface{})
IsTerminal() bool
SetAutoComplete(complete func(string) string)
}
The Options.Flagset presents an object with logic similar to the standard flag.FlagSet. Pprof can access to commandline arguments and options via this object.
1
2
3
4
5
6
7
8
9
10
11
12
13
type FlagSet interface {
Bool(name string, def bool, usage string) *bool
Int(name string, def int, usage string) *int
Float64(name string, def float64, usage string) *float64
String(name string, def string, usage string) *string
BoolVar(pointer *bool, name string, def bool, usage string)
IntVar(pointer *int, name string, def int, usage string)
Float64Var(pointer *float64, name string, def float64, usage string)
StringVar(pointer *string, name string, def string, usage string)
StringList(name string, def string, usage string) *[]*string
ExtraUsage() string
Parse(usage func()) []string
}
The Options.Fetch presents object wich fetches pprof/profile.Profile. Only this object can really know from where is the perf-profile.
1
2
3
type Fetcher interface {
Fetch(src string, duration, timeout time.Duration)(*profile.Profile, string, error)
}
The Options.Writer - as I see the idea of this filed is setup an object handling file operations directd by –output flag. (spoiler: it does not work yet)
1
2
3
type Writer interface {
Open(name string) (io.WriteCloser, error)
}
Now, let be a little magic… but before I still need a specific objects. One which implemets pprof/driver.Fetcher and other - pprof/driver.FlagSet
Local profile fetcher
benchmark/ppftool/fetcher.go:11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type fetcher struct {
b []byte
}
func (f *fetcher) Fetch(src string, duration, timeout time.Duration)(*profile.Profile, string, error) {
p, err := profile.ParseData(f.b)
return p, "", err
}
return nil, "", fmt.Errorf("unknown source %s", src)
}
func Fetcher(b []byte) driver.Fetcher {
return &fetcher{b}
}
and custom flagset
benchmark/ppftool/flagset.go:8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type FlagSet struct {
*flag.FlagSet
args []string
}
func (f *FlagSet) StringList(name string, def string, usage string)*[]*string {
return &[]*string{f.FlagSet.String(name, def, usage)}
}
func (f *FlagSet) ExtraUsage() string {
return ""
}
func (f *FlagSet) Parse(usage func()) []string {
f.FlagSet.Usage = func() {}
f.FlagSet.Parse(f.Args)
return f.FlagSet.Args()
}
func Flagset(a ...string) driver.FlagSet {
return &FlagSet{
flag.NewFlagSet("ppf", flag.ContinueOnError),
append(a,"")}
}
Now … The MAGIC!
playground/docs/samples/pprof/magic1.go:12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var bf bytes.Buffer
pprof.StartCPUProfile(&bf)
for s:= ""; len(s) < 100000; {
s = s + fmt.Sprintf("%d", len(s))
}
pprof.StopCPUProfile()
driver.PProf(&driver.Options{
Fetch: ppftool.Fetcher(bf.Bytes()),
Flagset: ppftool.Flagset("-top", "-nodecount=5"),
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>go run magic1.go
Main binary filename not available.
Type: cpu
Time: Feb 22, 2018 at 5:08pm (-03)
Duration: 501.76ms, Total samples = 530ms (105.63%)
Showing nodes accounting for 380ms, 71.70% of 530ms total
Showing top 5 nodes out of 61
flat flat% sum% cum cum%
190ms 35.85% 35.85% 190ms 35.85% runtime.memmove
100ms 18.87% 54.72% 190ms 35.85% runtime.scanobject
40ms 7.55% 62.26% 40ms 7.55% runtime.heapBits.bits (inline)
30ms 5.66% 67.92% 30ms 5.66% runtime.memclrNoHeapPointers
20ms 3.77% 71.70% 20ms 3.77% runtime.gcmarknewobject
It works! … but is not useful.
Since Options.Writer is not really used by the pprof/internal/driver I can’t redirect output gracefully.
pprof/internal/driver/driver.go:139
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
...
// Output to specified file.
o.UI.PrintErr("Generating report in ", output)
out, err := os.Create(output)
if err != nil {
return err
}
if _, err := src.WriteTo(out); err != nil {
out.Close()
return err
}
return out.Close()
}
So I will use external file, util it’s not fixed. It’s dirty but it works.
playground/docs/samples/pprof/magic2.go:12
1
2
3
4
5
6
7
8
9
10
11
func main() {
... the same code here
driver.PProf(&driver.Options{
Fetch: ppftool.Fetcher(bf.Bytes()),
Flagset: ppftool.Flagset("-top", "-nodecount=5", "-unit=s",
"-output=pprof.output.txt"),
UI: ppftool.FakeUi(), // supress errors and unwanted messages
})
}
Now go run magic2.go
prints nothing, but creates file pprof.output.txt containing profiling output.
To be useful this output need to be parsed and represented in some regular structure.
I can represent profiling output as a sort of structured report. I think, it’s quite useful.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Report struct {
Unit
Rows
...
}
type Rows []*Row
type Row struct {
Function string
Flat, FlatPercent, SumPercent, Cum, CumPercent float64
}
type Unit byte
const (
Second Unit = 0
Millisecond Unit = 1
Microsecond Unit = 2
...
)
And, of couse, I need to convert text output of the pprof into structured records.
benchmark/ppftool/report.go:87
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (r *Report) Write(b []byte) {
...
tf := func(s string) (x []string) {
... // extracting fields into array x
}
skip := true
for _, l := range strings.Split(string(b), "\n") {
a := tf(l)
if skip && "flat flat% sum% cum cum%" == strings.Join(a, " ") {
skip = false
}
if !skip && len(a) > 5 {
i := &Row{}
fmt.Sscanf(a[0], "%f", &i.Flat)
fmt.Sscanf(a[1], "%f", &i.FlatPercent)
fmt.Sscanf(a[2], "%f", &i.SumPercent)
fmt.Sscanf(a[3], "%f", &i.Cum)
fmt.Sscanf(a[4], "%f", &i.CumPercent)
i.Function = a[5]
r.Rows = append(r.Rows, i)
}
}
return
}
Ok, compile all together now
playground/docs/samples/pprof/magic3.go:14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
var bf bytes.Buffer
pprof.StartCPUProfile(&bf)
for s:= ""; len(s) < 100000; {
s = s + fmt.Sprintf("%d", len(s))
}
pprof.StopCPUProfile()
tempfile := "pprof.output.txt"
unit := util.DefaultUnit
rpt := &util.Report{Unit: unit}
driver.PProf(&driver.Options{
Fetch: ppftool.Fetcher(bf.Bytes()),
Flagset: ppftool.Flagset("-top", "-nodecount=5", "-unit="+unit.String(),
"-output="+tempfile),
UI: ppftool.FakeUi(),
})
if b, err := ioutil.ReadFile(tempfile); err == nil {
rpt.WriteTop(b)
os.Remove(tempfile)
}
fmt.Printf("%10s %11s %s\n", "flat", "%flat", "function")
for _, row := range rpt.Rows {
fmt.Printf("%10.3f %10.3f%% %s\n",
row.Flat, row.FlatPercent, row.Function)
}
}
Bingo! But still is not friendly. The good idea is to make a function implementing top command with constraints.
Pprof constraints … yes, pprof can limit samples by filters like -show, -hide, -focus. So I need a struct represented filtering options.
benchmark/ppftool/options.go:10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Options struct {
Unit // Second, Millisecond, Microsecond
...
Count int // -nodecount=
CumSort bool // -cum=
TagShow []string // -tagshow=
TagHide []string // -taghide=
TagIgnore []string // -tagignore=
TagFocus []string // -tagfocus=
Ignore []string // -ignore=
Focus []string // -focus=
Show []string // -show=
Hide []string // -hide=
...
}
func (o *Options) flagset(c ...string) driver.FlagSet {
if o.Count > 0 {
c = append(c, fmt.Sprintf("-nodecount=%d", o.Count))
}
// ... processing filters here
return Flagset(c...)
}
Now the top command can be implemented easily. All I need is call pprof with set of arguments generated from Options and then convert temporal file to structured Report.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func Top(b []byte, o *Options) (*Report, error) {
tempfile := TempFileName()
rpt := &Report{Unit: o.Unit}
err := driver.PProf(&driver.Options{
Fetch: &fetcher{b},
Flagset: o.flagset("-top", "-output="+tempfile),
UI: &ui{report: rpt},
})
if err != nil {
return nil, err
}
if b, err := ioutil.ReadFile(tempfile); err != nil {
return nil, err
} else {
rpt.WriteTop(b)
}
os.Remove(tempfile)
return rpt, nil
}
And now, finally, frendrly magic appears
playground/docs/samples/pprof/magic4.go:12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
var bf bytes.Buffer
pprof.StartCPUProfile(&bf)
for s:= ""; len(s) < 100000; {
s = s + fmt.Sprintf("%d",len(s))
}
pprof.StopCPUProfile()
rpt, err := ppftool.Top(
bf.Bytes(),
&ppftool.Options{Count: 5, Hide: []string{"runtime\\."}})
if err != nil {
fmt.Println(err)
os.Exit(2)
}
fmt.Printf("%10s %11s %s\n","flat","%flat","function")
for _, row := range rpt.Rows {
fmt.Printf("%10.3f %10.3f%% %s\n",
row.Flat,row.FlatPercent,row.Function)
}
}
1
2
3
4
5
6
7
>go run magic4.go
flat %flat function
0.180 28.570% main.main
0.010 1.590% sync.(*Pool).pinSlow
0.010 1.590% sync.poolCleanup
0.000 0.000% fmt.Sprintf
0.000 0.000% fmt.newPrinter