Skip to content

Commit 2fdce81

Browse files
hyangahdna2github
authored andcommitted
runtime/pprof: set HasFunctions of mapping entries
The pprof tool utilizes attributes of mapping entries such as HasFunctions to determine whether the profile includes necessary symbol information. If none of the attributes is set, pprof tool tries to read the corresponding binary to use for local symbolization. If the binary doesn't exist, it prints out error messages. Go runtime generated profiles without any of the attributes set so the pprof tool always printed out the error messages. The error messages became more obvious with the new terminal support that uses red color for error messages. Go runtime can symbolize all Go symbols and generate self-contained profile for pure Go program. Thus, there is no reason for the pprof tool to look for the copy of the binary. So, this CL sets one of the attributes (HasFunctions) true if all PCs in samples look fully symbolized. For non-pure Go program, however, it's possible that symbolization of non-Go PCs is incomplete. In this case, we need to leave the attributes all false so pprof can attempt to symbolize using the local copy of the binary if available. It's hard to determine whether a mapping includes non-Go code. Instead, this CL checks PCs from collected samples. If unsuccessful symbolization is observed, it skips setting the HasFunctions attribute. Fixes golang#25743 Change-Id: I5108be45bbc37ab486d145fa03e7ce37d88fad50 Reviewed-on: https://go-review.googlesource.com/118275 Run-TryBot: Hyang-Ah Hana Kim <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 8bf4c22 commit 2fdce81

File tree

3 files changed

+226
-13
lines changed

3 files changed

+226
-13
lines changed

src/runtime/pprof/proto.go

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"io"
1212
"io/ioutil"
1313
"runtime"
14-
"sort"
1514
"strconv"
1615
"time"
1716
"unsafe"
@@ -48,10 +47,26 @@ type profileBuilder struct {
4847
}
4948

5049
type memMap struct {
51-
start uintptr
52-
end uintptr
50+
// initialized as reading mapping
51+
start uintptr
52+
end uintptr
53+
offset uint64
54+
file, buildID string
55+
56+
funcs symbolizeFlag
5357
}
5458

59+
// symbolizeFlag keeps track of symbolization result.
60+
// 0 : no symbol lookup was performed
61+
// 1<<0 (lookupTried) : symbol lookup was performed
62+
// 1<<1 (lookupFailed): symbol lookup was performed but failed
63+
type symbolizeFlag uint8
64+
65+
const (
66+
lookupTried symbolizeFlag = 1 << iota
67+
lookupFailed symbolizeFlag = 1 << iota
68+
)
69+
5570
const (
5671
// message Profile
5772
tagProfile_SampleType = 1 // repeated ValueType
@@ -171,16 +186,23 @@ func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) {
171186
}
172187

173188
// pbMapping encodes a Mapping message to b.pb.
174-
func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file, buildID string) {
189+
func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file, buildID string, hasFuncs bool) {
175190
start := b.pb.startMessage()
176191
b.pb.uint64Opt(tagMapping_ID, id)
177192
b.pb.uint64Opt(tagMapping_Start, base)
178193
b.pb.uint64Opt(tagMapping_Limit, limit)
179194
b.pb.uint64Opt(tagMapping_Offset, offset)
180195
b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file))
181196
b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID))
182-
// TODO: Set any of HasInlineFrames, HasFunctions, HasFilenames, HasLineNumbers?
183-
// It seems like they should all be true, but they've never been set.
197+
// TODO: we set HasFunctions if all symbols from samples were symbolized (hasFuncs).
198+
// Decide what to do about HasInlineFrames and HasLineNumbers.
199+
// Also, another approach to handle the mapping entry with
200+
// incomplete symbolization results is to dupliace the mapping
201+
// entry (but with different Has* fields values) and use
202+
// different entries for symbolized locations and unsymbolized locations.
203+
if hasFuncs {
204+
b.pb.bool(tagMapping_HasFunctions, true)
205+
}
184206
b.pb.endMessage(tag, start)
185207
}
186208

@@ -205,6 +227,11 @@ func (b *profileBuilder) locForPC(addr uintptr) uint64 {
205227
return 0
206228
}
207229

230+
symbolizeResult := lookupTried
231+
if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 {
232+
symbolizeResult |= lookupFailed
233+
}
234+
208235
if frame.PC == 0 {
209236
// If we failed to resolve the frame, at least make up
210237
// a reasonable call PC. This mostly happens in tests.
@@ -239,12 +266,14 @@ func (b *profileBuilder) locForPC(addr uintptr) uint64 {
239266
}
240267
frame, more = frames.Next()
241268
}
242-
if len(b.mem) > 0 {
243-
i := sort.Search(len(b.mem), func(i int) bool {
244-
return b.mem[i].end > addr
245-
})
246-
if i < len(b.mem) && b.mem[i].start <= addr && addr < b.mem[i].end {
269+
for i := range b.mem {
270+
if b.mem[i].start <= addr && addr < b.mem[i].end {
247271
b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1))
272+
273+
m := b.mem[i]
274+
m.funcs |= symbolizeResult
275+
b.mem[i] = m
276+
break
248277
}
249278
}
250279
b.pb.endMessage(tagProfile_Location, start)
@@ -392,6 +421,11 @@ func (b *profileBuilder) build() {
392421
b.pbSample(values, locs, labels)
393422
}
394423

424+
for i, m := range b.mem {
425+
hasFunctions := m.funcs == lookupTried // lookupTried but not lookupFailed
426+
b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(m.start), uint64(m.end), m.offset, m.file, m.buildID, hasFunctions)
427+
}
428+
395429
// TODO: Anything for tagProfile_DropFrames?
396430
// TODO: Anything for tagProfile_KeepFrames?
397431

@@ -506,6 +540,11 @@ func parseProcSelfMaps(data []byte, addMapping func(lo, hi, offset uint64, file,
506540
}
507541

508542
func (b *profileBuilder) addMapping(lo, hi, offset uint64, file, buildID string) {
509-
b.mem = append(b.mem, memMap{uintptr(lo), uintptr(hi)})
510-
b.pbMapping(tagProfile_Mapping, uint64(len(b.mem)), lo, hi, offset, file, buildID)
543+
b.mem = append(b.mem, memMap{
544+
start: uintptr(lo),
545+
end: uintptr(hi),
546+
offset: offset,
547+
file: file,
548+
buildID: buildID,
549+
})
511550
}

src/runtime/pprof/proto_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
"bytes"
99
"encoding/json"
1010
"fmt"
11+
"internal/testenv"
1112
"io/ioutil"
13+
"os"
14+
"os/exec"
1215
"reflect"
1316
"runtime"
1417
"runtime/pprof/internal/profile"
@@ -225,3 +228,69 @@ func TestProcSelfMaps(t *testing.T) {
225228
}
226229
}
227230
}
231+
232+
// TestMapping checkes the mapping section of CPU profiles
233+
// has the HasFunctions field set correctly. If all PCs included
234+
// in the samples are successfully symbolized, the corresponding
235+
// mapping entry (in this test case, only one entry) should have
236+
// its HasFunctions field set true.
237+
// The test generates a CPU profile that includes PCs from C side
238+
// that the runtime can't symbolize. See ./testdata/mappingtest.
239+
func TestMapping(t *testing.T) {
240+
testenv.MustHaveGoRun(t)
241+
242+
prog := "./testdata/mappingtest"
243+
244+
// GoOnly includes only Go symbols that runtime will symbolize.
245+
// Go+C includes C symbols that runtime will not symbolize.
246+
for _, traceback := range []string{"GoOnly", "Go+C"} {
247+
t.Run("traceback"+traceback, func(t *testing.T) {
248+
cmd := exec.Command("go", "run", prog)
249+
if traceback != "GoOnly" {
250+
cmd.Env = append(os.Environ(), "SETCGOTRACEBACK=1")
251+
}
252+
cmd.Stderr = new(bytes.Buffer)
253+
254+
out, err := cmd.Output()
255+
if err != nil {
256+
t.Fatalf("failed to run the test program %q: %v\n%v", prog, err, cmd.Stderr)
257+
}
258+
259+
prof, err := profile.Parse(bytes.NewReader(out))
260+
if err != nil {
261+
t.Fatalf("failed to parse the generated profile data: %v", err)
262+
}
263+
264+
allResolved := !hasUnresolvedSymbol(prof)
265+
if allResolved && traceback != "GoOnly" {
266+
t.Log("No non-Go samples were sampled")
267+
}
268+
269+
for _, m := range prof.Mapping {
270+
if !strings.Contains(m.File, "/exe/main") {
271+
continue
272+
}
273+
if allResolved && !m.HasFunctions {
274+
t.Errorf("HasFunctions=%t when all sampled PCs were symbolized\n%s", m.HasFunctions, prof)
275+
}
276+
if !allResolved && m.HasFunctions {
277+
t.Errorf("HasFunctions=%t when some sampled PCs were not symbolized\n%s", m.HasFunctions, prof)
278+
}
279+
}
280+
})
281+
}
282+
}
283+
284+
func hasUnresolvedSymbol(prof *profile.Profile) bool {
285+
for _, loc := range prof.Location {
286+
if len(loc.Line) == 0 {
287+
return true
288+
}
289+
l := loc.Line[0]
290+
f := l.Function
291+
if l.Line == 0 || f == nil || f.Name == "" || f.Filename == "" {
292+
return true
293+
}
294+
}
295+
return false
296+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2018 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+
// This program outputs a CPU profile that includes
6+
// both Go and Cgo stacks. This is used by the mapping info
7+
// tests in runtime/pprof.
8+
//
9+
// If SETCGOTRACEBACK=1 is set, the CPU profile will includes
10+
// PCs from C side but they will not be symbolized.
11+
package main
12+
13+
/*
14+
#include <stdint.h>
15+
#include <stdlib.h>
16+
17+
int cpuHogCSalt1 = 0;
18+
int cpuHogCSalt2 = 0;
19+
20+
void CPUHogCFunction() {
21+
int foo = cpuHogCSalt1;
22+
int i;
23+
for (i = 0; i < 100000; i++) {
24+
if (foo > 0) {
25+
foo *= foo;
26+
} else {
27+
foo *= foo + 1;
28+
}
29+
cpuHogCSalt2 = foo;
30+
}
31+
}
32+
33+
struct CgoTracebackArg {
34+
uintptr_t context;
35+
uintptr_t sigContext;
36+
uintptr_t *buf;
37+
uintptr_t max;
38+
};
39+
40+
void CollectCgoTraceback(void* parg) {
41+
struct CgoTracebackArg* arg = (struct CgoTracebackArg*)(parg);
42+
arg->buf[0] = (uintptr_t)(CPUHogCFunction);
43+
arg->buf[1] = 0;
44+
};
45+
*/
46+
import "C"
47+
48+
import (
49+
"log"
50+
"os"
51+
"runtime"
52+
"runtime/pprof"
53+
"time"
54+
"unsafe"
55+
)
56+
57+
func init() {
58+
if v := os.Getenv("SETCGOTRACEBACK"); v == "1" {
59+
// Collect some PCs from C-side, but don't symbolize.
60+
runtime.SetCgoTraceback(0, unsafe.Pointer(C.CollectCgoTraceback), nil, nil)
61+
}
62+
}
63+
64+
func main() {
65+
go cpuHogGoFunction()
66+
go cpuHogCFunction()
67+
runtime.Gosched()
68+
69+
if err := pprof.StartCPUProfile(os.Stdout); err != nil {
70+
log.Fatal("can't start CPU profile: ", err)
71+
}
72+
time.Sleep(1 * time.Second)
73+
pprof.StopCPUProfile()
74+
75+
if err := os.Stdout.Close(); err != nil {
76+
log.Fatal("can't write CPU profile: ", err)
77+
}
78+
}
79+
80+
var salt1 int
81+
var salt2 int
82+
83+
func cpuHogGoFunction() {
84+
// Generates CPU profile samples including a Go call path.
85+
for {
86+
foo := salt1
87+
for i := 0; i < 1e5; i++ {
88+
if foo > 0 {
89+
foo *= foo
90+
} else {
91+
foo *= foo + 1
92+
}
93+
salt2 = foo
94+
}
95+
runtime.Gosched()
96+
}
97+
}
98+
99+
func cpuHogCFunction() {
100+
// Generates CPU profile samples including a Cgo call path.
101+
for {
102+
C.CPUHogCFunction()
103+
runtime.Gosched()
104+
}
105+
}

0 commit comments

Comments
 (0)