Skip to content

Commit f72a629

Browse files
committed
runtime/pprof/internal: add package protopprof
This change adds code, originally written by Russ Cox <[email protected]> and open-sourced by Google, that converts from the "legacy" binary pprof profile format to a struct representation of the new protocol buffer pprof profile format. This code reads the entire binary format for conversion to the protobuf format. In a future change, we will update the code to incrementally read and convert segments of the binary format, so that the entire profile does not need to be stored in memory. This change also contains contributions by Daria Kolistratova <[email protected]> from the rolled-back change golang.org/cl/30556 adapting the code to be used by the package runtime/pprof. This code also appeared in the change golang.org/cl/32257, which was based on Daria Kolistratova's change, but was also rolled back. Updates #16093 Change-Id: I5c768b1134bc15408d80a3ccc7ed867db9a1c63d Reviewed-on: https://go-review.googlesource.com/32811 Run-TryBot: Michael Matloob <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 7465bfb commit f72a629

File tree

3 files changed

+322
-8
lines changed

3 files changed

+322
-8
lines changed

src/go/build/deps_test.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ var pkgDeps = map[string][]string{
9393
// L3 adds reflection and some basic utility packages
9494
// and interface definitions, but nothing that makes
9595
// system calls.
96-
"crypto": {"L2", "hash"}, // interfaces
96+
"crypto": {"L2", "hash"}, // interfaces
9797
"crypto/cipher": {"L2", "crypto/subtle"},
9898
"crypto/subtle": {},
9999
"encoding/base32": {"L2"},
@@ -171,13 +171,14 @@ var pkgDeps = map[string][]string{
171171
"log": {"L1", "os", "fmt", "time"},
172172

173173
// Packages used by testing must be low-level (L2+fmt).
174-
"regexp": {"L2", "regexp/syntax"},
175-
"regexp/syntax": {"L2"},
176-
"runtime/debug": {"L2", "fmt", "io/ioutil", "os", "time"},
177-
"runtime/pprof/internal/gzip0": {"L2"},
178-
"runtime/pprof": {"L2", "fmt", "os", "text/tabwriter"},
179-
"runtime/trace": {"L0"},
180-
"text/tabwriter": {"L2"},
174+
"regexp": {"L2", "regexp/syntax"},
175+
"regexp/syntax": {"L2"},
176+
"runtime/debug": {"L2", "fmt", "io/ioutil", "os", "time"},
177+
"runtime/pprof/internal/gzip0": {"L2"},
178+
"runtime/pprof/internal/protopprof": {"L2", "fmt", "internal/pprof/profile", "os", "time"},
179+
"runtime/pprof": {"L2", "fmt", "os", "text/tabwriter"},
180+
"runtime/trace": {"L0"},
181+
"text/tabwriter": {"L2"},
181182

182183
"testing": {"L2", "context", "flag", "fmt", "internal/race", "os", "runtime/debug", "runtime/pprof", "runtime/trace", "time"},
183184
"testing/iotest": {"L2", "log"},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package protopprof converts the runtime's raw profile logs
6+
// to Profile structs containing a representation of the pprof
7+
// protocol buffer profile format.
8+
package protopprof
9+
10+
import (
11+
"fmt"
12+
"os"
13+
"runtime"
14+
"time"
15+
"unsafe"
16+
17+
"internal/pprof/profile"
18+
)
19+
20+
// TranslateCPUProfile parses binary CPU profiling stack trace data
21+
// generated by runtime.CPUProfile() into a profile struct.
22+
func TranslateCPUProfile(b []byte, startTime time.Time) (*profile.Profile, error) {
23+
const wordSize = unsafe.Sizeof(uintptr(0))
24+
const minRawProfile = 5 * wordSize // Need a minimum of 5 words.
25+
if uintptr(len(b)) < minRawProfile {
26+
return nil, fmt.Errorf("truncated profile")
27+
}
28+
n := int(uintptr(len(b)) / wordSize)
29+
data := ((*[1 << 28]uintptr)(unsafe.Pointer(&b[0])))[:n:n]
30+
period := data[3]
31+
data = data[5:] // skip header
32+
33+
// profile initialization taken from pprof tool
34+
p := &profile.Profile{
35+
Period: int64(period) * 1000,
36+
PeriodType: &profile.ValueType{Type: "cpu", Unit: "nanoseconds"},
37+
SampleType: []*profile.ValueType{
38+
{Type: "samples", Unit: "count"},
39+
{Type: "cpu", Unit: "nanoseconds"},
40+
},
41+
TimeNanos: int64(startTime.UnixNano()),
42+
DurationNanos: time.Since(startTime).Nanoseconds(),
43+
}
44+
// Parse CPU samples from the profile.
45+
locs := make(map[uint64]*profile.Location)
46+
for len(b) > 0 {
47+
if len(data) < 2 || uintptr(len(data)) < 2+data[1] {
48+
return nil, fmt.Errorf("truncated profile")
49+
}
50+
count := data[0]
51+
nstk := data[1]
52+
fmt.Printf("count:%v nstk: %v\n", count, nstk)
53+
if uintptr(len(data)) < 2+nstk {
54+
return nil, fmt.Errorf("truncated profile")
55+
}
56+
stk := data[2 : 2+nstk]
57+
data = data[2+nstk:]
58+
59+
if count == 0 && nstk == 1 && stk[0] == 0 {
60+
// end of data marker
61+
break
62+
}
63+
64+
sloc := make([]*profile.Location, len(stk))
65+
for i, addr := range stk {
66+
addr := uint64(addr)
67+
// Addresses from stack traces point to the next instruction after
68+
// each call. Adjust by -1 to land somewhere on the actual call
69+
// (except for the leaf, which is not a call).
70+
if i > 0 {
71+
addr--
72+
}
73+
loc := locs[addr]
74+
if loc == nil {
75+
loc = &profile.Location{
76+
ID: uint64(len(p.Location) + 1),
77+
Address: addr,
78+
}
79+
locs[addr] = loc
80+
p.Location = append(p.Location, loc)
81+
}
82+
sloc[i] = loc
83+
}
84+
p.Sample = append(p.Sample, &profile.Sample{
85+
Value: []int64{int64(count), int64(count) * int64(p.Period)},
86+
Location: sloc,
87+
})
88+
}
89+
90+
if runtime.GOOS == "linux" {
91+
if err := addMappings(p); err != nil {
92+
return nil, err
93+
}
94+
}
95+
return p, nil
96+
}
97+
98+
func addMappings(p *profile.Profile) error {
99+
// Parse memory map from /proc/self/maps
100+
f, err := os.Open("/proc/self/maps")
101+
if err != nil {
102+
return err
103+
}
104+
defer f.Close()
105+
return p.ParseMemoryMap(f)
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package protopprof
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"internal/pprof/profile"
11+
"io"
12+
"io/ioutil"
13+
"reflect"
14+
"runtime"
15+
"runtime/pprof"
16+
"testing"
17+
"time"
18+
"unsafe"
19+
)
20+
21+
// Profile collects a CPU utilization profile and
22+
// writes it to w as a compressed profile.proto. It's used by
23+
// TestProfileParse.
24+
func Profile(w io.Writer, seconds int) error {
25+
var buf bytes.Buffer
26+
// Collect the CPU profile in legacy format in buf.
27+
startTime := time.Now()
28+
if err := pprof.StartCPUProfile(&buf); err != nil {
29+
return fmt.Errorf("Could not enable CPU profiling: %s\n", err)
30+
}
31+
time.Sleep(time.Duration(seconds) * time.Second)
32+
pprof.StopCPUProfile()
33+
34+
const untagged = false
35+
p, err := TranslateCPUProfile(buf.Bytes(), startTime)
36+
if err != nil {
37+
return err
38+
}
39+
return p.Write(w)
40+
}
41+
42+
// Helper function to initialize empty cpu profile with sampling period provided.
43+
func createEmptyProfileWithPeriod(t *testing.T, periodMs uint64) bytes.Buffer {
44+
// Mock the sample header produced by cpu profiler. Write a sample
45+
// period of 2000 microseconds, followed by no samples.
46+
buf := new(bytes.Buffer)
47+
// Profile header is as follows:
48+
// The first, third and fifth words are 0. The second word is 3.
49+
// The fourth word is the period.
50+
// EOD marker:
51+
// The sixth word -- count is initialized to 0 above.
52+
// The code below sets the seventh word -- nstk to 1
53+
// The eighth word -- addr is initialized to 0 above.
54+
words := []int{0, 3, 0, int(periodMs), 0, 0, 1, 0}
55+
n := int(unsafe.Sizeof(0)) * len(words)
56+
data := ((*[1 << 29]byte)(unsafe.Pointer(&words[0])))[:n:n]
57+
if _, err := buf.Write(data); err != nil {
58+
t.Fatalf("createEmptyProfileWithPeriod failed: %v", err)
59+
}
60+
return *buf
61+
}
62+
63+
// Helper function to initialize cpu profile with two sample values.
64+
func createProfileWithTwoSamples(t *testing.T, periodMs uintptr, count1 uintptr, count2 uintptr,
65+
address1 uintptr, address2 uintptr) bytes.Buffer {
66+
// Mock the sample header produced by cpu profiler. Write a sample
67+
// period of 2000 microseconds, followed by no samples.
68+
buf := new(bytes.Buffer)
69+
words := []uint64{0, 3, 0, uint64(periodMs), 0, uint64(count1), 2,
70+
uint64(address1), uint64(address1 + 2),
71+
uint64(count2), 2, uint64(address2), uint64(address2 + 2),
72+
0, uint64(1), 0}
73+
for _, n := range words {
74+
var err error
75+
switch unsafe.Sizeof(int(0)) {
76+
case 8:
77+
_, err = buf.Write((*[8]byte)(unsafe.Pointer(&n))[:8:8])
78+
case 4:
79+
_, err = buf.Write((*[4]byte)(unsafe.Pointer(&n))[:4:4])
80+
}
81+
if err != nil {
82+
t.Fatalf("createProfileWithTwoSamples failed: %v", err)
83+
}
84+
}
85+
return *buf
86+
}
87+
88+
// Tests that server creates a cpu profile handler that outputs a parsable Profile profile.
89+
func TestCPUProfileParse(t *testing.T) {
90+
var before, after runtime.MemStats
91+
runtime.ReadMemStats(&before)
92+
var buf bytes.Buffer
93+
if err := Profile(&buf, 30); err != nil {
94+
t.Fatalf("Profile failed: %v", err)
95+
}
96+
runtime.ReadMemStats(&after)
97+
_, err := profile.Parse(&buf)
98+
if err != nil {
99+
t.Fatalf("Could not parse Profile profile: %v", err)
100+
}
101+
}
102+
103+
// Tests TranslateCPUProfile parses correct sampling period in an otherwise empty cpu profile.
104+
func TestTranlateCPUProfileSamplingPeriod(t *testing.T) {
105+
// A test server with mock cpu profile data.
106+
var buf bytes.Buffer
107+
108+
startTime := time.Now()
109+
b := createEmptyProfileWithPeriod(t, 2000)
110+
p, err := TranslateCPUProfile(b.Bytes(), startTime)
111+
if err != nil {
112+
t.Fatalf("translate failed: %v", err)
113+
}
114+
if err := p.Write(&buf); err != nil {
115+
t.Fatalf("write failed: %v", err)
116+
}
117+
118+
p, err = profile.Parse(&buf)
119+
if err != nil {
120+
t.Fatalf("Could not parse Profile profile: %v", err)
121+
}
122+
123+
// Expected PeriodType and SampleType.
124+
expectedPeriodType := &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
125+
expectedSampleType := []*profile.ValueType{
126+
{Type: "samples", Unit: "count"},
127+
{Type: "cpu", Unit: "nanoseconds"},
128+
}
129+
if p.Period != 2000*1000 || !reflect.DeepEqual(p.PeriodType, expectedPeriodType) ||
130+
!reflect.DeepEqual(p.SampleType, expectedSampleType) || p.Sample != nil {
131+
t.Fatalf("Unexpected Profile fields")
132+
}
133+
}
134+
135+
func getSampleAsString(sample []*profile.Sample) string {
136+
var str string
137+
for _, x := range sample {
138+
for _, y := range x.Location {
139+
if y.Mapping != nil {
140+
str += fmt.Sprintf("Mapping:%v\n", *y.Mapping)
141+
}
142+
str += fmt.Sprintf("Location:%v\n", y)
143+
}
144+
str += fmt.Sprintf("Sample:%v\n", *x)
145+
}
146+
return str
147+
}
148+
149+
// Tests TranslateCPUProfile parses a cpu profile with sample values present.
150+
func TestTranslateCPUProfileWithSamples(t *testing.T) {
151+
if runtime.GOOS != "linux" {
152+
t.Skip("test requires a system with /proc/self/maps")
153+
}
154+
// Figure out two addresses from /proc/self/maps.
155+
mmap, err := ioutil.ReadFile("/proc/self/maps")
156+
if err != nil {
157+
t.Fatal("Cannot read /proc/self/maps")
158+
}
159+
rd := bytes.NewReader(mmap)
160+
mprof := &profile.Profile{}
161+
if err = mprof.ParseMemoryMap(rd); err != nil {
162+
t.Fatalf("Cannot parse /proc/self/maps")
163+
}
164+
if len(mprof.Mapping) < 2 {
165+
t.Fatalf("Less than two mappings")
166+
}
167+
address1 := mprof.Mapping[0].Start
168+
address2 := mprof.Mapping[1].Start
169+
// A test server with mock cpu profile data.
170+
171+
startTime := time.Now()
172+
b := createProfileWithTwoSamples(t, 2000, 20, 40, uintptr(address1), uintptr(address2))
173+
p, err := TranslateCPUProfile(b.Bytes(), startTime)
174+
175+
if err != nil {
176+
t.Fatalf("Could not parse Profile profile: %v", err)
177+
}
178+
// Expected PeriodType, SampleType and Sample.
179+
expectedPeriodType := &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
180+
expectedSampleType := []*profile.ValueType{
181+
{Type: "samples", Unit: "count"},
182+
{Type: "cpu", Unit: "nanoseconds"},
183+
}
184+
expectedSample := []*profile.Sample{
185+
{Value: []int64{20, 20 * 2000 * 1000}, Location: []*profile.Location{
186+
{ID: 1, Mapping: mprof.Mapping[0], Address: address1},
187+
{ID: 2, Mapping: mprof.Mapping[0], Address: address1 + 1},
188+
}},
189+
{Value: []int64{40, 40 * 2000 * 1000}, Location: []*profile.Location{
190+
{ID: 3, Mapping: mprof.Mapping[1], Address: address2},
191+
{ID: 4, Mapping: mprof.Mapping[1], Address: address2 + 1},
192+
}},
193+
}
194+
if p.Period != 2000*1000 {
195+
t.Fatalf("Sampling periods do not match")
196+
}
197+
if !reflect.DeepEqual(p.PeriodType, expectedPeriodType) {
198+
t.Fatalf("Period types do not match")
199+
}
200+
if !reflect.DeepEqual(p.SampleType, expectedSampleType) {
201+
t.Fatalf("Sample types do not match")
202+
}
203+
if !reflect.DeepEqual(p.Sample, expectedSample) {
204+
t.Fatalf("Samples do not match: Expected: %v, Got:%v", getSampleAsString(expectedSample),
205+
getSampleAsString(p.Sample))
206+
}
207+
}

0 commit comments

Comments
 (0)