// Copyright 2016 The Go Authors. All rights reserved.
|
// Use of this source code is governed by a BSD-style
|
// license that can be found in the LICENSE file.
|
|
package pprof
|
|
import (
|
"bytes"
|
"compress/gzip"
|
"fmt"
|
"io"
|
"io/ioutil"
|
"runtime"
|
"strconv"
|
"time"
|
"unsafe"
|
)
|
|
// lostProfileEvent is the function to which lost profiling
|
// events are attributed.
|
// (The name shows up in the pprof graphs.)
|
func lostProfileEvent() { lostProfileEvent() }
|
|
// funcPC returns the PC for the func value f.
|
func funcPC(f interface{}) uintptr {
|
return *(*[2]*uintptr)(unsafe.Pointer(&f))[1]
|
}
|
|
// A profileBuilder writes a profile incrementally from a
|
// stream of profile samples delivered by the runtime.
|
type profileBuilder struct {
|
start time.Time
|
end time.Time
|
havePeriod bool
|
period int64
|
m profMap
|
|
// encoding state
|
w io.Writer
|
zw *gzip.Writer
|
pb protobuf
|
strings []string
|
stringMap map[string]int
|
locs map[uintptr]int
|
funcs map[string]int // Package path-qualified function name to Function.ID
|
mem []memMap
|
}
|
|
type memMap struct {
|
// initialized as reading mapping
|
start uintptr
|
end uintptr
|
offset uint64
|
file, buildID string
|
|
funcs symbolizeFlag
|
fake bool // map entry was faked; /proc/self/maps wasn't available
|
}
|
|
// symbolizeFlag keeps track of symbolization result.
|
// 0 : no symbol lookup was performed
|
// 1<<0 (lookupTried) : symbol lookup was performed
|
// 1<<1 (lookupFailed): symbol lookup was performed but failed
|
type symbolizeFlag uint8
|
|
const (
|
lookupTried symbolizeFlag = 1 << iota
|
lookupFailed symbolizeFlag = 1 << iota
|
)
|
|
const (
|
// message Profile
|
tagProfile_SampleType = 1 // repeated ValueType
|
tagProfile_Sample = 2 // repeated Sample
|
tagProfile_Mapping = 3 // repeated Mapping
|
tagProfile_Location = 4 // repeated Location
|
tagProfile_Function = 5 // repeated Function
|
tagProfile_StringTable = 6 // repeated string
|
tagProfile_DropFrames = 7 // int64 (string table index)
|
tagProfile_KeepFrames = 8 // int64 (string table index)
|
tagProfile_TimeNanos = 9 // int64
|
tagProfile_DurationNanos = 10 // int64
|
tagProfile_PeriodType = 11 // ValueType (really optional string???)
|
tagProfile_Period = 12 // int64
|
tagProfile_Comment = 13 // repeated int64
|
tagProfile_DefaultSampleType = 14 // int64
|
|
// message ValueType
|
tagValueType_Type = 1 // int64 (string table index)
|
tagValueType_Unit = 2 // int64 (string table index)
|
|
// message Sample
|
tagSample_Location = 1 // repeated uint64
|
tagSample_Value = 2 // repeated int64
|
tagSample_Label = 3 // repeated Label
|
|
// message Label
|
tagLabel_Key = 1 // int64 (string table index)
|
tagLabel_Str = 2 // int64 (string table index)
|
tagLabel_Num = 3 // int64
|
|
// message Mapping
|
tagMapping_ID = 1 // uint64
|
tagMapping_Start = 2 // uint64
|
tagMapping_Limit = 3 // uint64
|
tagMapping_Offset = 4 // uint64
|
tagMapping_Filename = 5 // int64 (string table index)
|
tagMapping_BuildID = 6 // int64 (string table index)
|
tagMapping_HasFunctions = 7 // bool
|
tagMapping_HasFilenames = 8 // bool
|
tagMapping_HasLineNumbers = 9 // bool
|
tagMapping_HasInlineFrames = 10 // bool
|
|
// message Location
|
tagLocation_ID = 1 // uint64
|
tagLocation_MappingID = 2 // uint64
|
tagLocation_Address = 3 // uint64
|
tagLocation_Line = 4 // repeated Line
|
|
// message Line
|
tagLine_FunctionID = 1 // uint64
|
tagLine_Line = 2 // int64
|
|
// message Function
|
tagFunction_ID = 1 // uint64
|
tagFunction_Name = 2 // int64 (string table index)
|
tagFunction_SystemName = 3 // int64 (string table index)
|
tagFunction_Filename = 4 // int64 (string table index)
|
tagFunction_StartLine = 5 // int64
|
)
|
|
// stringIndex adds s to the string table if not already present
|
// and returns the index of s in the string table.
|
func (b *profileBuilder) stringIndex(s string) int64 {
|
id, ok := b.stringMap[s]
|
if !ok {
|
id = len(b.strings)
|
b.strings = append(b.strings, s)
|
b.stringMap[s] = id
|
}
|
return int64(id)
|
}
|
|
func (b *profileBuilder) flush() {
|
const dataFlush = 4096
|
if b.pb.nest == 0 && len(b.pb.data) > dataFlush {
|
b.zw.Write(b.pb.data)
|
b.pb.data = b.pb.data[:0]
|
}
|
}
|
|
// pbValueType encodes a ValueType message to b.pb.
|
func (b *profileBuilder) pbValueType(tag int, typ, unit string) {
|
start := b.pb.startMessage()
|
b.pb.int64(tagValueType_Type, b.stringIndex(typ))
|
b.pb.int64(tagValueType_Unit, b.stringIndex(unit))
|
b.pb.endMessage(tag, start)
|
}
|
|
// pbSample encodes a Sample message to b.pb.
|
func (b *profileBuilder) pbSample(values []int64, locs []uint64, labels func()) {
|
start := b.pb.startMessage()
|
b.pb.int64s(tagSample_Value, values)
|
b.pb.uint64s(tagSample_Location, locs)
|
if labels != nil {
|
labels()
|
}
|
b.pb.endMessage(tagProfile_Sample, start)
|
b.flush()
|
}
|
|
// pbLabel encodes a Label message to b.pb.
|
func (b *profileBuilder) pbLabel(tag int, key, str string, num int64) {
|
start := b.pb.startMessage()
|
b.pb.int64Opt(tagLabel_Key, b.stringIndex(key))
|
b.pb.int64Opt(tagLabel_Str, b.stringIndex(str))
|
b.pb.int64Opt(tagLabel_Num, num)
|
b.pb.endMessage(tag, start)
|
}
|
|
// pbLine encodes a Line message to b.pb.
|
func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) {
|
start := b.pb.startMessage()
|
b.pb.uint64Opt(tagLine_FunctionID, funcID)
|
b.pb.int64Opt(tagLine_Line, line)
|
b.pb.endMessage(tag, start)
|
}
|
|
// pbMapping encodes a Mapping message to b.pb.
|
func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file, buildID string, hasFuncs bool) {
|
start := b.pb.startMessage()
|
b.pb.uint64Opt(tagMapping_ID, id)
|
b.pb.uint64Opt(tagMapping_Start, base)
|
b.pb.uint64Opt(tagMapping_Limit, limit)
|
b.pb.uint64Opt(tagMapping_Offset, offset)
|
b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file))
|
b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID))
|
// TODO: we set HasFunctions if all symbols from samples were symbolized (hasFuncs).
|
// Decide what to do about HasInlineFrames and HasLineNumbers.
|
// Also, another approach to handle the mapping entry with
|
// incomplete symbolization results is to dupliace the mapping
|
// entry (but with different Has* fields values) and use
|
// different entries for symbolized locations and unsymbolized locations.
|
if hasFuncs {
|
b.pb.bool(tagMapping_HasFunctions, true)
|
}
|
b.pb.endMessage(tag, start)
|
}
|
|
// locForPC returns the location ID for addr.
|
// addr must a return PC or 1 + the PC of an inline marker. This returns the location of the corresponding call.
|
// It may emit to b.pb, so there must be no message encoding in progress.
|
func (b *profileBuilder) locForPC(addr uintptr) uint64 {
|
id := uint64(b.locs[addr])
|
if id != 0 {
|
return id
|
}
|
|
// Expand this one address using CallersFrames so we can cache
|
// each expansion. In general, CallersFrames takes a whole
|
// stack, but in this case we know there will be no skips in
|
// the stack and we have return PCs anyway.
|
frames := runtime.CallersFrames([]uintptr{addr})
|
frame, more := frames.Next()
|
if frame.Function == "runtime.goexit" {
|
// Short-circuit if we see runtime.goexit so the loop
|
// below doesn't allocate a useless empty location.
|
return 0
|
}
|
|
symbolizeResult := lookupTried
|
if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 {
|
symbolizeResult |= lookupFailed
|
}
|
|
if frame.PC == 0 {
|
// If we failed to resolve the frame, at least make up
|
// a reasonable call PC. This mostly happens in tests.
|
frame.PC = addr - 1
|
}
|
|
// We can't write out functions while in the middle of the
|
// Location message, so record new functions we encounter and
|
// write them out after the Location.
|
type newFunc struct {
|
id uint64
|
name, file string
|
}
|
newFuncs := make([]newFunc, 0, 8)
|
|
id = uint64(len(b.locs)) + 1
|
b.locs[addr] = int(id)
|
start := b.pb.startMessage()
|
b.pb.uint64Opt(tagLocation_ID, id)
|
b.pb.uint64Opt(tagLocation_Address, uint64(frame.PC))
|
for frame.Function != "runtime.goexit" {
|
// Write out each line in frame expansion.
|
funcID := uint64(b.funcs[frame.Function])
|
if funcID == 0 {
|
funcID = uint64(len(b.funcs)) + 1
|
b.funcs[frame.Function] = int(funcID)
|
newFuncs = append(newFuncs, newFunc{funcID, frame.Function, frame.File})
|
}
|
b.pbLine(tagLocation_Line, funcID, int64(frame.Line))
|
if !more {
|
break
|
}
|
frame, more = frames.Next()
|
}
|
for i := range b.mem {
|
if b.mem[i].start <= addr && addr < b.mem[i].end || b.mem[i].fake {
|
b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1))
|
|
m := b.mem[i]
|
m.funcs |= symbolizeResult
|
b.mem[i] = m
|
break
|
}
|
}
|
b.pb.endMessage(tagProfile_Location, start)
|
|
// Write out functions we found during frame expansion.
|
for _, fn := range newFuncs {
|
start := b.pb.startMessage()
|
b.pb.uint64Opt(tagFunction_ID, fn.id)
|
b.pb.int64Opt(tagFunction_Name, b.stringIndex(fn.name))
|
b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(fn.name))
|
b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fn.file))
|
b.pb.endMessage(tagProfile_Function, start)
|
}
|
|
b.flush()
|
return id
|
}
|
|
// newProfileBuilder returns a new profileBuilder.
|
// CPU profiling data obtained from the runtime can be added
|
// by calling b.addCPUData, and then the eventual profile
|
// can be obtained by calling b.finish.
|
func newProfileBuilder(w io.Writer) *profileBuilder {
|
zw, _ := gzip.NewWriterLevel(w, gzip.BestSpeed)
|
b := &profileBuilder{
|
w: w,
|
zw: zw,
|
start: time.Now(),
|
strings: []string{""},
|
stringMap: map[string]int{"": 0},
|
locs: map[uintptr]int{},
|
funcs: map[string]int{},
|
}
|
b.readMapping()
|
return b
|
}
|
|
// addCPUData adds the CPU profiling data to the profile.
|
// The data must be a whole number of records,
|
// as delivered by the runtime.
|
func (b *profileBuilder) addCPUData(data []uint64, tags []unsafe.Pointer) error {
|
if !b.havePeriod {
|
// first record is period
|
if len(data) < 3 {
|
return fmt.Errorf("truncated profile")
|
}
|
if data[0] != 3 || data[2] == 0 {
|
return fmt.Errorf("malformed profile")
|
}
|
// data[2] is sampling rate in Hz. Convert to sampling
|
// period in nanoseconds.
|
b.period = 1e9 / int64(data[2])
|
b.havePeriod = true
|
data = data[3:]
|
}
|
|
// Parse CPU samples from the profile.
|
// Each sample is 3+n uint64s:
|
// data[0] = 3+n
|
// data[1] = time stamp (ignored)
|
// data[2] = count
|
// data[3:3+n] = stack
|
// If the count is 0 and the stack has length 1,
|
// that's an overflow record inserted by the runtime
|
// to indicate that stack[0] samples were lost.
|
// Otherwise the count is usually 1,
|
// but in a few special cases like lost non-Go samples
|
// there can be larger counts.
|
// Because many samples with the same stack arrive,
|
// we want to deduplicate immediately, which we do
|
// using the b.m profMap.
|
for len(data) > 0 {
|
if len(data) < 3 || data[0] > uint64(len(data)) {
|
return fmt.Errorf("truncated profile")
|
}
|
if data[0] < 3 || tags != nil && len(tags) < 1 {
|
return fmt.Errorf("malformed profile")
|
}
|
count := data[2]
|
stk := data[3:data[0]]
|
data = data[data[0]:]
|
var tag unsafe.Pointer
|
if tags != nil {
|
tag = tags[0]
|
tags = tags[1:]
|
}
|
|
if count == 0 && len(stk) == 1 {
|
// overflow record
|
count = uint64(stk[0])
|
stk = []uint64{
|
uint64(funcPC(lostProfileEvent)),
|
}
|
}
|
b.m.lookup(stk, tag).count += int64(count)
|
}
|
return nil
|
}
|
|
// build completes and returns the constructed profile.
|
func (b *profileBuilder) build() {
|
b.end = time.Now()
|
|
b.pb.int64Opt(tagProfile_TimeNanos, b.start.UnixNano())
|
if b.havePeriod { // must be CPU profile
|
b.pbValueType(tagProfile_SampleType, "samples", "count")
|
b.pbValueType(tagProfile_SampleType, "cpu", "nanoseconds")
|
b.pb.int64Opt(tagProfile_DurationNanos, b.end.Sub(b.start).Nanoseconds())
|
b.pbValueType(tagProfile_PeriodType, "cpu", "nanoseconds")
|
b.pb.int64Opt(tagProfile_Period, b.period)
|
}
|
|
values := []int64{0, 0}
|
var locs []uint64
|
for e := b.m.all; e != nil; e = e.nextAll {
|
values[0] = e.count
|
values[1] = e.count * b.period
|
|
var labels func()
|
if e.tag != nil {
|
labels = func() {
|
for k, v := range *(*labelMap)(e.tag) {
|
b.pbLabel(tagSample_Label, k, v, 0)
|
}
|
}
|
}
|
|
locs = locs[:0]
|
for i, addr := range e.stk {
|
// Addresses from stack traces point to the
|
// next instruction after each call, except
|
// for the leaf, which points to where the
|
// signal occurred. locForPC expects return
|
// PCs, so increment the leaf address to look
|
// like a return PC.
|
if i == 0 {
|
addr++
|
}
|
l := b.locForPC(addr)
|
if l == 0 { // runtime.goexit
|
continue
|
}
|
locs = append(locs, l)
|
}
|
b.pbSample(values, locs, labels)
|
}
|
|
for i, m := range b.mem {
|
hasFunctions := m.funcs == lookupTried // lookupTried but not lookupFailed
|
b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(m.start), uint64(m.end), m.offset, m.file, m.buildID, hasFunctions)
|
}
|
|
// TODO: Anything for tagProfile_DropFrames?
|
// TODO: Anything for tagProfile_KeepFrames?
|
|
b.pb.strings(tagProfile_StringTable, b.strings)
|
b.zw.Write(b.pb.data)
|
b.zw.Close()
|
}
|
|
// readMapping reads /proc/self/maps and writes mappings to b.pb.
|
// It saves the address ranges of the mappings in b.mem for use
|
// when emitting locations.
|
func (b *profileBuilder) readMapping() {
|
data, _ := ioutil.ReadFile("/proc/self/maps")
|
parseProcSelfMaps(data, b.addMapping)
|
if len(b.mem) == 0 { // pprof expects a map entry, so fake one.
|
b.addMappingEntry(0, 0, 0, "", "", true)
|
// TODO(hyangah): make addMapping return *memMap or
|
// take a memMap struct, and get rid of addMappingEntry
|
// that takes a bunch of positional arguments.
|
}
|
}
|
|
func parseProcSelfMaps(data []byte, addMapping func(lo, hi, offset uint64, file, buildID string)) {
|
// $ cat /proc/self/maps
|
// 00400000-0040b000 r-xp 00000000 fc:01 787766 /bin/cat
|
// 0060a000-0060b000 r--p 0000a000 fc:01 787766 /bin/cat
|
// 0060b000-0060c000 rw-p 0000b000 fc:01 787766 /bin/cat
|
// 014ab000-014cc000 rw-p 00000000 00:00 0 [heap]
|
// 7f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064 /usr/lib/locale/locale-archive
|
// 7f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226 /lib/x86_64-linux-gnu/libc-2.19.so
|
// 7f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226 /lib/x86_64-linux-gnu/libc-2.19.so
|
// 7f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226 /lib/x86_64-linux-gnu/libc-2.19.so
|
// 7f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226 /lib/x86_64-linux-gnu/libc-2.19.so
|
// 7f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
|
// 7f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217 /lib/x86_64-linux-gnu/ld-2.19.so
|
// 7f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
|
// 7f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
|
// 7f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217 /lib/x86_64-linux-gnu/ld-2.19.so
|
// 7f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217 /lib/x86_64-linux-gnu/ld-2.19.so
|
// 7f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
|
// 7ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0 [stack]
|
// 7ffc34343000-7ffc34345000 r-xp 00000000 00:00 0 [vdso]
|
// ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
|
|
var line []byte
|
// next removes and returns the next field in the line.
|
// It also removes from line any spaces following the field.
|
next := func() []byte {
|
j := bytes.IndexByte(line, ' ')
|
if j < 0 {
|
f := line
|
line = nil
|
return f
|
}
|
f := line[:j]
|
line = line[j+1:]
|
for len(line) > 0 && line[0] == ' ' {
|
line = line[1:]
|
}
|
return f
|
}
|
|
for len(data) > 0 {
|
i := bytes.IndexByte(data, '\n')
|
if i < 0 {
|
line, data = data, nil
|
} else {
|
line, data = data[:i], data[i+1:]
|
}
|
addr := next()
|
i = bytes.IndexByte(addr, '-')
|
if i < 0 {
|
continue
|
}
|
lo, err := strconv.ParseUint(string(addr[:i]), 16, 64)
|
if err != nil {
|
continue
|
}
|
hi, err := strconv.ParseUint(string(addr[i+1:]), 16, 64)
|
if err != nil {
|
continue
|
}
|
perm := next()
|
if len(perm) < 4 || perm[2] != 'x' {
|
// Only interested in executable mappings.
|
continue
|
}
|
offset, err := strconv.ParseUint(string(next()), 16, 64)
|
if err != nil {
|
continue
|
}
|
next() // dev
|
inode := next() // inode
|
if line == nil {
|
continue
|
}
|
file := string(line)
|
|
// Trim deleted file marker.
|
deletedStr := " (deleted)"
|
deletedLen := len(deletedStr)
|
if len(file) >= deletedLen && file[len(file)-deletedLen:] == deletedStr {
|
file = file[:len(file)-deletedLen]
|
}
|
|
if len(inode) == 1 && inode[0] == '0' && file == "" {
|
// Huge-page text mappings list the initial fragment of
|
// mapped but unpopulated memory as being inode 0.
|
// Don't report that part.
|
// But [vdso] and [vsyscall] are inode 0, so let non-empty file names through.
|
continue
|
}
|
|
// TODO: pprof's remapMappingIDs makes two adjustments:
|
// 1. If there is an /anon_hugepage mapping first and it is
|
// consecutive to a next mapping, drop the /anon_hugepage.
|
// 2. If start-offset = 0x400000, change start to 0x400000 and offset to 0.
|
// There's no indication why either of these is needed.
|
// Let's try not doing these and see what breaks.
|
// If we do need them, they would go here, before we
|
// enter the mappings into b.mem in the first place.
|
|
buildID, _ := elfBuildID(file)
|
addMapping(lo, hi, offset, file, buildID)
|
}
|
}
|
|
func (b *profileBuilder) addMapping(lo, hi, offset uint64, file, buildID string) {
|
b.addMappingEntry(lo, hi, offset, file, buildID, false)
|
}
|
|
func (b *profileBuilder) addMappingEntry(lo, hi, offset uint64, file, buildID string, fake bool) {
|
b.mem = append(b.mem, memMap{
|
start: uintptr(lo),
|
end: uintptr(hi),
|
offset: offset,
|
file: file,
|
buildID: buildID,
|
fake: fake,
|
})
|
}
|