1
0
mirror of https://github.com/golang/go synced 2024-11-26 05:48:05 -07:00

cmd/vendor: upgrade pprof to latest

This change upgrades the vendored pprof to pick up the fix for a
serious issue that made the source view in browser mode blank
(tracked upstream as google/pprof#621).

I also had to patch pprof.go, since one of the upstream commit we
included introduced a breaking change in the file interface (the Base
method is now called ObjAddr and has a different signature).

I've manually verified that the upgrade fixes the aforementioned
issues with the source view.

Fixes #45786

Change-Id: I00659ae539a2ad603758e1f06572374d483b9ddc
Reviewed-on: https://go-review.googlesource.com/c/go/+/318049
Trust: Alberto Donizetti <alb.donizetti@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Alberto Donizetti 2021-05-07 19:10:28 +02:00
parent 4c8f48ed4f
commit 68327e1aa1
14 changed files with 416 additions and 196 deletions

View File

@ -3,7 +3,7 @@ module cmd
go 1.17
require (
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5
github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 // indirect
golang.org/x/arch v0.0.0-20210502124803-cbf565b21d1e
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e // indirect

View File

@ -1,8 +1,8 @@
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5 h1:zIaiqGYDQwa4HVx5wGRTXbx38Pqxjemn4BP98wpzpXo=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a h1:jmAp/2PZAScNd62lTD3Mcb0Ey9FvIIJtLohPhtxZJ+Q=
github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
golang.org/x/arch v0.0.0-20210502124803-cbf565b21d1e h1:pv3V0NlNSh5Q6AX/StwGLBjcLS7UN4m4Gq+V+uSecqM=

View File

@ -232,9 +232,9 @@ func (f *file) Name() string {
return f.name
}
func (f *file) Base() uint64 {
func (f *file) ObjAddr(addr uint64) (uint64, error) {
// No support for shared libraries.
return 0
return 0, nil
}
func (f *file) BuildID() string {

View File

@ -159,8 +159,8 @@ type ObjFile interface {
// Name returns the underlying file name, if available.
Name() string
// Base returns the base address to use when looking up symbols in the file.
Base() uint64
// ObjAddr returns the objdump address corresponding to a runtime address.
ObjAddr(addr uint64) (uint64, error)
// BuildID returns the GNU build ID of the file, or an empty string.
BuildID() string

View File

@ -42,7 +42,12 @@ type Binutils struct {
rep *binrep
}
var objdumpLLVMVerRE = regexp.MustCompile(`LLVM version (?:(\d*)\.(\d*)\.(\d*)|.*(trunk).*)`)
var (
objdumpLLVMVerRE = regexp.MustCompile(`LLVM version (?:(\d*)\.(\d*)\.(\d*)|.*(trunk).*)`)
// Defined for testing
elfOpen = elf.Open
)
// binrep is an immutable representation for Binutils. It is atomically
// replaced on every mutation to provide thread-safe access.
@ -421,14 +426,23 @@ func (b *binrep) openMachO(name string, start, limit, offset uint64) (plugin.Obj
}
func (b *binrep) openELF(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
ef, err := elf.Open(name)
ef, err := elfOpen(name)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %v", name, err)
}
defer ef.Close()
var stextOffset *uint64
var pageAligned = func(addr uint64) bool { return addr%4096 == 0 }
buildID := ""
if f, err := os.Open(name); err == nil {
if id, err := elfexec.GetBuildID(f); err == nil {
buildID = fmt.Sprintf("%x", id)
}
}
var (
stextOffset *uint64
pageAligned = func(addr uint64) bool { return addr%4096 == 0 }
)
if strings.Contains(name, "vmlinux") || !pageAligned(start) || !pageAligned(limit) || !pageAligned(offset) {
// Reading all Symbols is expensive, and we only rarely need it so
// we don't want to do it every time. But if _stext happens to be
@ -450,38 +464,29 @@ func (b *binrep) openELF(name string, start, limit, offset uint64) (plugin.ObjFi
}
}
var ph *elf.ProgHeader
// For user space executables, find the actual program segment that is
// associated with the given mapping. Skip this search if limit <= start.
// We cannot use just a check on the start address of the mapping to tell if
// it's a kernel / .ko module mapping, because with quipper address remapping
// enabled, the address would be in the lower half of the address space.
if stextOffset == nil && start < limit && limit < (uint64(1)<<63) {
ph, err = elfexec.FindProgHeaderForMapping(ef, offset, limit-start)
if err != nil {
return nil, fmt.Errorf("failed to find program header for file %q, mapping pgoff %x, memsz=%x: %v", name, offset, limit-start, err)
}
} else {
// For the kernel, find the program segment that includes the .text section.
ph = elfexec.FindTextProgHeader(ef)
}
base, err := elfexec.GetBase(&ef.FileHeader, ph, stextOffset, start, limit, offset)
if err != nil {
// Check that we can compute a base for the binary. This may not be the
// correct base value, so we don't save it. We delay computing the actual base
// value until we have a sample address for this mapping, so that we can
// correctly identify the associated program segment that is needed to compute
// the base.
if _, err := elfexec.GetBase(&ef.FileHeader, elfexec.FindTextProgHeader(ef), stextOffset, start, limit, offset); err != nil {
return nil, fmt.Errorf("could not identify base for %s: %v", name, err)
}
buildID := ""
if f, err := os.Open(name); err == nil {
if id, err := elfexec.GetBuildID(f); err == nil {
buildID = fmt.Sprintf("%x", id)
}
}
isData := ph != nil && ph.Flags&elf.PF_X == 0
if b.fast || (!b.addr2lineFound && !b.llvmSymbolizerFound) {
return &fileNM{file: file{b, name, base, buildID, isData}}, nil
return &fileNM{file: file{
b: b,
name: name,
buildID: buildID,
m: &elfMapping{start: start, limit: limit, offset: offset, stextOffset: stextOffset},
}}, nil
}
return &fileAddr2Line{file: file{b, name, base, buildID, isData}}, nil
return &fileAddr2Line{file: file{
b: b,
name: name,
buildID: buildID,
m: &elfMapping{start: start, limit: limit, offset: offset, stextOffset: stextOffset},
}}, nil
}
func (b *binrep) openPE(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
@ -511,21 +516,119 @@ func (b *binrep) openPE(name string, start, limit, offset uint64) (plugin.ObjFil
return &fileAddr2Line{file: file{b: b, name: name, base: base}}, nil
}
// elfMapping stores the parameters of a runtime mapping that are needed to
// identify the ELF segment associated with a mapping.
type elfMapping struct {
// Runtime mapping parameters.
start, limit, offset uint64
// Offset of _stext symbol. Only defined for kernel images, nil otherwise.
stextOffset *uint64
}
// file implements the binutils.ObjFile interface.
type file struct {
b *binrep
name string
base uint64
buildID string
isData bool
baseOnce sync.Once // Ensures the base, baseErr and isData are computed once.
base uint64
baseErr error // Any eventual error while computing the base.
isData bool
// Mapping information. Relevant only for ELF files, nil otherwise.
m *elfMapping
}
// computeBase computes the relocation base for the given binary file only if
// the elfMapping field is set. It populates the base and isData fields and
// returns an error.
func (f *file) computeBase(addr uint64) error {
if f == nil || f.m == nil {
return nil
}
if addr < f.m.start || addr >= f.m.limit {
return fmt.Errorf("specified address %x is outside the mapping range [%x, %x] for file %q", addr, f.m.start, f.m.limit, f.name)
}
ef, err := elfOpen(f.name)
if err != nil {
return fmt.Errorf("error parsing %s: %v", f.name, err)
}
defer ef.Close()
var ph *elf.ProgHeader
// For user space executables, find the actual program segment that is
// associated with the given mapping. Skip this search if limit <= start.
// We cannot use just a check on the start address of the mapping to tell if
// it's a kernel / .ko module mapping, because with quipper address remapping
// enabled, the address would be in the lower half of the address space.
if f.m.stextOffset == nil && f.m.start < f.m.limit && f.m.limit < (uint64(1)<<63) {
// Get all program headers associated with the mapping.
headers, hasLoadables := elfexec.ProgramHeadersForMapping(ef, f.m.offset, f.m.limit-f.m.start)
// Some ELF files don't contain any loadable program segments, e.g. .ko
// kernel modules. It's not an error to have no header in such cases.
if hasLoadables {
ph, err = matchUniqueHeader(headers, addr-f.m.start+f.m.offset)
if err != nil {
return fmt.Errorf("failed to find program header for file %q, ELF mapping %#v, address %x: %v", f.name, *f.m, addr, err)
}
}
} else {
// For the kernel, find the program segment that includes the .text section.
ph = elfexec.FindTextProgHeader(ef)
}
base, err := elfexec.GetBase(&ef.FileHeader, ph, f.m.stextOffset, f.m.start, f.m.limit, f.m.offset)
if err != nil {
return err
}
f.base = base
f.isData = ph != nil && ph.Flags&elf.PF_X == 0
return nil
}
// matchUniqueHeader attempts to identify a unique header from the given list,
// using the given file offset to disambiguate between multiple segments. It
// returns an error if the header list is empty or if it cannot identify a
// unique header.
func matchUniqueHeader(headers []*elf.ProgHeader, fileOffset uint64) (*elf.ProgHeader, error) {
if len(headers) == 0 {
return nil, errors.New("no program header matches mapping info")
}
if len(headers) == 1 {
// Don't use the file offset if we already have a single header.
return headers[0], nil
}
// We have multiple input segments. Attempt to identify a unique one
// based on the given file offset.
var ph *elf.ProgHeader
for _, h := range headers {
if fileOffset >= h.Off && fileOffset < h.Off+h.Memsz {
if ph != nil {
// Assuming no other bugs, this can only happen if we have two or
// more small program segments that fit on the same page, and a
// segment other than the last one includes uninitialized data.
return nil, fmt.Errorf("found second program header (%#v) that matches file offset %x, first program header is %#v. Does first program segment contain uninitialized data?", *h, fileOffset, *ph)
}
ph = h
}
}
if ph == nil {
return nil, fmt.Errorf("no program header matches file offset %x", fileOffset)
}
return ph, nil
}
func (f *file) Name() string {
return f.name
}
func (f *file) Base() uint64 {
return f.base
func (f *file) ObjAddr(addr uint64) (uint64, error) {
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return 0, f.baseErr
}
return addr - f.base, nil
}
func (f *file) BuildID() string {
@ -533,7 +636,11 @@ func (f *file) BuildID() string {
}
func (f *file) SourceLine(addr uint64) ([]plugin.Frame, error) {
return []plugin.Frame{}, nil
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return nil, f.baseErr
}
return nil, nil
}
func (f *file) Close() error {
@ -560,6 +667,10 @@ type fileNM struct {
}
func (f *fileNM) SourceLine(addr uint64) ([]plugin.Frame, error) {
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return nil, f.baseErr
}
if f.addr2linernm == nil {
addr2liner, err := newAddr2LinerNM(f.b.nm, f.name, f.base)
if err != nil {
@ -579,9 +690,14 @@ type fileAddr2Line struct {
file
addr2liner *addr2Liner
llvmSymbolizer *llvmSymbolizer
isData bool
}
func (f *fileAddr2Line) SourceLine(addr uint64) ([]plugin.Frame, error) {
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return nil, f.baseErr
}
f.once.Do(f.init)
if f.llvmSymbolizer != nil {
return f.llvmSymbolizer.addrInfo(addr)

View File

@ -284,83 +284,71 @@ func FindTextProgHeader(f *elf.File) *elf.ProgHeader {
return nil
}
// FindProgHeaderForMapping returns the loadable program segment header that is
// fully contained in the runtime mapping with file offset pgoff and memory size
// memsz, or an error if the segment cannot be determined. The function returns
// a nil program header and no error if the ELF binary has no loadable segments.
func FindProgHeaderForMapping(f *elf.File, pgoff, memsz uint64) (*elf.ProgHeader, error) {
// ProgramHeadersForMapping returns the loadable program segment headers that
// are fully contained in the runtime mapping with file offset pgoff and memory
// size memsz, and if the binary includes any loadable segments.
func ProgramHeadersForMapping(f *elf.File, pgoff, memsz uint64) ([]*elf.ProgHeader, bool) {
const (
// pageSize defines the virtual memory page size used by the loader. This
// value is dependent on the memory management unit of the CPU. The page
// size is 4KB virtually on all the architectures that we care about, so we
// define this metric as a constant. If we encounter architectures where
// page sie is not 4KB, we must try to guess the page size on the system
// where the profile was collected, possibly using the architecture
// specified in the ELF file header.
pageSize = 4096
pageOffsetMask = pageSize - 1
pageMask = ^uint64(pageOffsetMask)
)
var headers []*elf.ProgHeader
loadables := 0
hasLoadables := false
for _, p := range f.Progs {
// The segment must be fully included in the mapping.
if p.Type == elf.PT_LOAD && pgoff <= p.Off && p.Off+p.Memsz <= pgoff+memsz {
headers = append(headers, &p.ProgHeader)
alignedOffset := uint64(0)
if p.Off > (p.Vaddr & pageOffsetMask) {
alignedOffset = p.Off - (p.Vaddr & pageOffsetMask)
}
if alignedOffset <= pgoff {
headers = append(headers, &p.ProgHeader)
}
}
if p.Type == elf.PT_LOAD {
loadables++
hasLoadables = true
}
}
if len(headers) == 1 {
return headers[0], nil
}
// Some ELF files don't contain any program segments, e.g. .ko loadable kernel
// modules. Don't return an error in such cases.
if loadables == 0 {
return nil, nil
}
if len(headers) == 0 {
return nil, fmt.Errorf("no program header matches file offset %x and memory size %x", pgoff, memsz)
if len(headers) < 2 {
return headers, hasLoadables
}
// Segments are mapped page aligned. In some cases, segments may be smaller
// than a page, which causes the next segment to start at a file offset that
// is logically on the same page if we were to align file offsets by page.
// Example:
// LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
// 0x00000000000006fc 0x00000000000006fc R E 0x200000
// LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
// 0x0000000000000230 0x0000000000000238 RW 0x200000
//
// In this case, perf records the following mappings for this executable:
// 0 0 [0xc0]: PERF_RECORD_MMAP2 87867/87867: [0x400000(0x1000) @ 0 00:3c 512041 0]: r-xp exename
// 0 0 [0xc0]: PERF_RECORD_MMAP2 87867/87867: [0x600000(0x2000) @ 0 00:3c 512041 0]: rw-p exename
//
// Both mappings have file offset 0. The first mapping is one page length and
// it can include only the first loadable segment. Due to page alignment, the
// second mapping starts also at file offset 0, and it spans two pages. It can
// include both the first and the second loadable segments. We must return the
// correct program header to compute the correct base offset.
//
// We cannot use the mapping protections to distinguish between segments,
// because protections are not passed through to this function.
// We cannot use the start address to differentiate between segments, because
// with ASLR, the mapping start address can be any value.
//
// We use a heuristic to compute the minimum mapping size required for a
// segment, assuming mappings are 4k page aligned, and return the segment that
// matches the given mapping size.
const pageSize = 4096
// If we have more than one matching segments, try a strict check on the
// segment memory size. We use a heuristic to compute the minimum mapping size
// required for a segment, assuming mappings are page aligned.
// The memory size based heuristic makes sense only if the mapping size is a
// multiple of 4k page size.
// multiple of page size.
if memsz%pageSize != 0 {
return nil, fmt.Errorf("mapping size = %x and %d segments match the passed in mapping", memsz, len(headers))
return headers, hasLoadables
}
// Return an error if no segment, or multiple segments match the size, so we can debug.
// Return all found headers if we cannot narrow the selection to a single
// program segment.
var ph *elf.ProgHeader
pageMask := ^uint64(pageSize - 1)
for _, h := range headers {
wantSize := (h.Vaddr+h.Memsz+pageSize-1)&pageMask - (h.Vaddr & pageMask)
if wantSize != memsz {
continue
}
if ph != nil {
return nil, fmt.Errorf("found second program header (%#v) that matches memsz %x, first program header is %#v", *h, memsz, *ph)
// Found a second program header matching, so return all previously
// identified headers.
return headers, hasLoadables
}
ph = h
}
if ph == nil {
return nil, fmt.Errorf("found %d matching program headers, but none matches mapping size %x", len(headers), memsz)
// No matching header for the strict check. Return all previously identified
// headers.
return headers, hasLoadables
}
return ph, nil
return []*elf.ProgHeader{ph}, hasLoadables
}

View File

@ -131,8 +131,9 @@ type ObjFile interface {
// Name returns the underlyinf file name, if available
Name() string
// Base returns the base address to use when looking up symbols in the file.
Base() uint64
// ObjAddr returns the objdump (linker) address corresponding to a runtime
// address, and an error.
ObjAddr(addr uint64) (uint64, error)
// BuildID returns the GNU build ID of the file, or an empty string.
BuildID() string

View File

@ -445,7 +445,7 @@ func PrintAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFuncs int) e
return err
}
ns := annotateAssembly(insts, sns, s.base)
ns := annotateAssembly(insts, sns, s.file)
fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0])
for _, name := range s.sym.Name[1:] {
@ -534,7 +534,6 @@ func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regex
addr = *address
}
msyms, err := f.Symbols(rx, addr)
base := f.Base()
f.Close()
if err != nil {
continue
@ -543,7 +542,6 @@ func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regex
objSyms = append(objSyms,
&objSymbol{
sym: ms,
base: base,
file: f,
},
)
@ -558,7 +556,6 @@ func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regex
// added to correspond to sample addresses
type objSymbol struct {
sym *plugin.Sym
base uint64
file plugin.ObjFile
}
@ -578,8 +575,7 @@ func nodesPerSymbol(ns graph.Nodes, symbols []*objSymbol) map[*objSymbol]graph.N
for _, s := range symbols {
// Gather samples for this symbol.
for _, n := range ns {
address := n.Info.Address - s.base
if address >= s.sym.Start && address < s.sym.End {
if address, err := s.file.ObjAddr(n.Info.Address); err == nil && address >= s.sym.Start && address < s.sym.End {
symNodes[s] = append(symNodes[s], n)
}
}
@ -621,7 +617,7 @@ func (a *assemblyInstruction) cumValue() int64 {
// annotateAssembly annotates a set of assembly instructions with a
// set of samples. It returns a set of nodes to display. base is an
// offset to adjust the sample addresses.
func annotateAssembly(insts []plugin.Inst, samples graph.Nodes, base uint64) []assemblyInstruction {
func annotateAssembly(insts []plugin.Inst, samples graph.Nodes, file plugin.ObjFile) []assemblyInstruction {
// Add end marker to simplify printing loop.
insts = append(insts, plugin.Inst{
Addr: ^uint64(0),
@ -645,7 +641,10 @@ func annotateAssembly(insts []plugin.Inst, samples graph.Nodes, base uint64) []a
// Sum all the samples until the next instruction (to account
// for samples attributed to the middle of an instruction).
for next := insts[ix+1].Addr; s < len(samples) && samples[s].Info.Address-base < next; s++ {
for next := insts[ix+1].Addr; s < len(samples); s++ {
if addr, err := file.ObjAddr(samples[s].Info.Address); err != nil || addr >= next {
break
}
sample := samples[s]
n.flatDiv += sample.FlatDiv
n.flat += sample.Flat

View File

@ -132,6 +132,7 @@ func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
// sourcePrinter holds state needed for generating source+asm HTML listing.
type sourcePrinter struct {
reader *sourceReader
synth *synthCode
objectTool plugin.ObjTool
objects map[string]plugin.ObjFile // Opened object files
sym *regexp.Regexp // May be nil
@ -146,6 +147,12 @@ type sourcePrinter struct {
prettyNames map[string]string
}
// addrInfo holds information for an address we are interested in.
type addrInfo struct {
loc *profile.Location // Always non-nil
obj plugin.ObjFile // May be nil
}
// instructionInfo holds collected information for an instruction.
type instructionInfo struct {
objAddr uint64 // Address in object file (with base subtracted out)
@ -207,6 +214,7 @@ func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) er
func newSourcePrinter(rpt *Report, obj plugin.ObjTool, sourcePath string) *sourcePrinter {
sp := &sourcePrinter{
reader: newSourceReader(sourcePath, rpt.options.TrimPath),
synth: newSynthCode(rpt.prof.Mapping),
objectTool: obj,
objects: map[string]plugin.ObjFile{},
sym: rpt.options.Symbol,
@ -225,19 +233,21 @@ func newSourcePrinter(rpt *Report, obj plugin.ObjTool, sourcePath string) *sourc
}
}
addrs := map[uint64]bool{}
addrs := map[uint64]addrInfo{}
flat := map[uint64]int64{}
cum := map[uint64]int64{}
// Record an interest in the function corresponding to lines[index].
markInterest := func(addr uint64, lines []profile.Line, index int) {
fn := lines[index]
markInterest := func(addr uint64, loc *profile.Location, index int) {
fn := loc.Line[index]
if fn.Function == nil {
return
}
sp.interest[fn.Function.Name] = true
sp.interest[fn.Function.SystemName] = true
addrs[addr] = true
if _, ok := addrs[addr]; !ok {
addrs[addr] = addrInfo{loc, sp.objectFile(loc.Mapping)}
}
}
// See if sp.sym matches line.
@ -270,15 +280,21 @@ func newSourcePrinter(rpt *Report, obj plugin.ObjTool, sourcePath string) *sourc
sp.prettyNames[line.Function.SystemName] = line.Function.Name
}
cum[loc.Address] += value
if i == 0 {
flat[loc.Address] += value
addr := loc.Address
if addr == 0 {
// Some profiles are missing valid addresses.
addr = sp.synth.address(loc)
}
if sp.sym == nil || (address != nil && loc.Address == *address) {
cum[addr] += value
if i == 0 {
flat[addr] += value
}
if sp.sym == nil || (address != nil && addr == *address) {
// Interested in top-level entry of stack.
if len(loc.Line) > 0 {
markInterest(loc.Address, loc.Line, len(loc.Line)-1)
markInterest(addr, loc, len(loc.Line)-1)
}
continue
}
@ -287,7 +303,7 @@ func newSourcePrinter(rpt *Report, obj plugin.ObjTool, sourcePath string) *sourc
matchFile := (loc.Mapping != nil && sp.sym.MatchString(loc.Mapping.File))
for j, line := range loc.Line {
if (j == 0 && matchFile) || matches(line) {
markInterest(loc.Address, loc.Line, j)
markInterest(addr, loc, j)
}
}
}
@ -306,10 +322,11 @@ func (sp *sourcePrinter) close() {
}
}
func (sp *sourcePrinter) expandAddresses(rpt *Report, addrs map[uint64]bool, flat map[uint64]int64) {
func (sp *sourcePrinter) expandAddresses(rpt *Report, addrs map[uint64]addrInfo, flat map[uint64]int64) {
// We found interesting addresses (ones with non-zero samples) above.
// Get covering address ranges and disassemble the ranges.
ranges := sp.splitIntoRanges(rpt.prof, addrs, flat)
ranges, unprocessed := sp.splitIntoRanges(rpt.prof, addrs, flat)
sp.handleUnprocessed(addrs, unprocessed)
// Trim ranges if there are too many.
const maxRanges = 25
@ -321,9 +338,18 @@ func (sp *sourcePrinter) expandAddresses(rpt *Report, addrs map[uint64]bool, fla
}
for _, r := range ranges {
base := r.obj.Base()
insts, err := sp.objectTool.Disasm(r.mapping.File, r.begin-base, r.end-base,
rpt.options.IntelSyntax)
objBegin, err := r.obj.ObjAddr(r.begin)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range start %x: %v\n", r.begin, err)
continue
}
objEnd, err := r.obj.ObjAddr(r.end)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range end %x: %v\n", r.end, err)
continue
}
base := r.begin - objBegin
insts, err := sp.objectTool.Disasm(r.mapping.File, objBegin, objEnd, rpt.options.IntelSyntax)
if err != nil {
// TODO(sanjay): Report that the covered addresses are missing.
continue
@ -385,78 +411,115 @@ func (sp *sourcePrinter) expandAddresses(rpt *Report, addrs map[uint64]bool, fla
frames = lastFrames
}
// See if the stack contains a function we are interested in.
for i, f := range frames {
if !sp.interest[f.Func] {
continue
}
// Record sub-stack under frame's file/line.
fname := canonicalizeFileName(f.File)
file := sp.files[fname]
if file == nil {
file = &sourceFile{
fname: fname,
lines: map[int][]sourceInst{},
funcName: map[int]string{},
}
sp.files[fname] = file
}
callees := frames[:i]
stack := make([]callID, 0, len(callees))
for j := len(callees) - 1; j >= 0; j-- { // Reverse so caller is first
stack = append(stack, callID{
file: callees[j].File,
line: callees[j].Line,
})
}
file.lines[f.Line] = append(file.lines[f.Line], sourceInst{addr, stack})
// Remember the first function name encountered per source line
// and assume that that line belongs to that function.
if _, ok := file.funcName[f.Line]; !ok {
file.funcName[f.Line] = f.Func
}
}
sp.addStack(addr, frames)
}
}
}
// splitIntoRanges converts the set of addresses we are interested in into a set of address
// ranges to disassemble.
func (sp *sourcePrinter) splitIntoRanges(prof *profile.Profile, set map[uint64]bool, flat map[uint64]int64) []addressRange {
// List of mappings so we can stop expanding address ranges at mapping boundaries.
mappings := append([]*profile.Mapping{}, prof.Mapping...)
sort.Slice(mappings, func(i, j int) bool { return mappings[i].Start < mappings[j].Start })
func (sp *sourcePrinter) addStack(addr uint64, frames []plugin.Frame) {
// See if the stack contains a function we are interested in.
for i, f := range frames {
if !sp.interest[f.Func] {
continue
}
var result []addressRange
addrs := make([]uint64, 0, len(set))
for addr := range set {
addrs = append(addrs, addr)
// Record sub-stack under frame's file/line.
fname := canonicalizeFileName(f.File)
file := sp.files[fname]
if file == nil {
file = &sourceFile{
fname: fname,
lines: map[int][]sourceInst{},
funcName: map[int]string{},
}
sp.files[fname] = file
}
callees := frames[:i]
stack := make([]callID, 0, len(callees))
for j := len(callees) - 1; j >= 0; j-- { // Reverse so caller is first
stack = append(stack, callID{
file: callees[j].File,
line: callees[j].Line,
})
}
file.lines[f.Line] = append(file.lines[f.Line], sourceInst{addr, stack})
// Remember the first function name encountered per source line
// and assume that that line belongs to that function.
if _, ok := file.funcName[f.Line]; !ok {
file.funcName[f.Line] = f.Func
}
}
}
// synthAsm is the special disassembler value used for instructions without an object file.
const synthAsm = ""
// handleUnprocessed handles addresses that were skipped by splitIntoRanges because they
// did not belong to a known object file.
func (sp *sourcePrinter) handleUnprocessed(addrs map[uint64]addrInfo, unprocessed []uint64) {
// makeFrames synthesizes a []plugin.Frame list for the specified address.
// The result will typically have length 1, but may be longer if address corresponds
// to inlined calls.
makeFrames := func(addr uint64) []plugin.Frame {
loc := addrs[addr].loc
stack := make([]plugin.Frame, 0, len(loc.Line))
for _, line := range loc.Line {
fn := line.Function
if fn == nil {
continue
}
stack = append(stack, plugin.Frame{
Func: fn.Name,
File: fn.Filename,
Line: int(line.Line),
})
}
return stack
}
for _, addr := range unprocessed {
frames := makeFrames(addr)
x := instructionInfo{
objAddr: addr,
length: 1,
disasm: synthAsm,
}
if len(frames) > 0 {
x.file = frames[0].File
x.line = frames[0].Line
}
sp.insts[addr] = x
sp.addStack(addr, frames)
}
}
// splitIntoRanges converts the set of addresses we are interested in into a set of address
// ranges to disassemble. It also returns the set of addresses found that did not have an
// associated object file and were therefore not added to an address range.
func (sp *sourcePrinter) splitIntoRanges(prof *profile.Profile, addrMap map[uint64]addrInfo, flat map[uint64]int64) ([]addressRange, []uint64) {
// Partition addresses into two sets: ones with a known object file, and ones without.
var addrs, unprocessed []uint64
for addr, info := range addrMap {
if info.obj != nil {
addrs = append(addrs, addr)
} else {
unprocessed = append(unprocessed, addr)
}
}
sort.Slice(addrs, func(i, j int) bool { return addrs[i] < addrs[j] })
mappingIndex := 0
const expand = 500 // How much to expand range to pick up nearby addresses.
var result []addressRange
for i, n := 0, len(addrs); i < n; {
begin, end := addrs[i], addrs[i]
sum := flat[begin]
i++
// Advance to mapping containing addrs[i]
for mappingIndex < len(mappings) && mappings[mappingIndex].Limit <= begin {
mappingIndex++
}
if mappingIndex >= len(mappings) {
// TODO(sanjay): Report missed address and its samples.
break
}
m := mappings[mappingIndex]
obj := sp.objectFile(m)
if obj == nil {
// TODO(sanjay): Report missed address and its samples.
continue
}
info := addrMap[begin]
m := info.loc.Mapping
obj := info.obj // Non-nil because of the partitioning done above.
// Find following addresses that are close enough to addrs[i].
for i < n && addrs[i] <= end+2*expand && addrs[i] < m.Limit {
@ -479,7 +542,7 @@ func (sp *sourcePrinter) splitIntoRanges(prof *profile.Profile, set map[uint64]b
result = append(result, addressRange{begin, end, obj, m, sum})
}
return result
return result, unprocessed
}
func (sp *sourcePrinter) initSamples(flat, cum map[uint64]int64) {
@ -665,9 +728,12 @@ func (sp *sourcePrinter) functions(f *sourceFile) []sourceFunction {
return funcs
}
// objectFile return the object for the named file, opening it if necessary.
// objectFile return the object for the specified mapping, opening it if necessary.
// It returns nil on error.
func (sp *sourcePrinter) objectFile(m *profile.Mapping) plugin.ObjFile {
if m == nil {
return nil
}
if object, ok := sp.objects[m.File]; ok {
return object // May be nil if we detected an error earlier.
}
@ -725,12 +791,28 @@ func printFunctionSourceLine(w io.Writer, lineNo int, flat, cum int64, lineConte
return
}
nestedInfo := false
cl := "deadsrc"
for _, an := range assembly {
if len(an.inlineCalls) > 0 || an.instruction != synthAsm {
nestedInfo = true
cl = "livesrc"
}
}
fmt.Fprintf(w,
"<span class=line> %6d</span> <span class=deadsrc> %10s %10s %8s %s </span>",
lineNo,
"<span class=line> %6d</span> <span class=%s> %10s %10s %8s %s </span>",
lineNo, cl,
valueOrDot(flat, rpt), valueOrDot(cum, rpt),
"", template.HTMLEscapeString(lineContents))
srcIndent := indentation(lineContents)
if nestedInfo {
srcIndent := indentation(lineContents)
printNested(w, srcIndent, assembly, reader, rpt)
}
fmt.Fprintln(w)
}
func printNested(w io.Writer, srcIndent int, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) {
fmt.Fprint(w, "<span class=asm>")
var curCalls []callID
for i, an := range assembly {
@ -763,6 +845,9 @@ func printFunctionSourceLine(w io.Writer, lineNo int, flat, cum int64, lineConte
template.HTMLEscapeString(filepath.Base(c.file)), c.line)
}
curCalls = an.inlineCalls
if an.instruction == synthAsm {
continue
}
text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
fmt.Fprintf(w, " %8s %10s %10s %8x: %s <span class=unimportant>%s</span>\n",
"", valueOrDot(flat, rpt), valueOrDot(cum, rpt), an.address,
@ -772,7 +857,7 @@ func printFunctionSourceLine(w io.Writer, lineNo int, flat, cum int64, lineConte
// would cause double-escaping of file name.
fileline)
}
fmt.Fprintln(w, "</span>")
fmt.Fprint(w, "</span>")
}
// printFunctionClosing prints the end of a function in a weblist report.

View File

@ -40,14 +40,7 @@ h1 {
.inlinesrc {
color: #000066;
}
.deadsrc {
cursor: pointer;
}
.deadsrc:hover {
background-color: #eeeeee;
}
.livesrc {
color: #0000ff;
cursor: pointer;
}
.livesrc:hover {

View File

@ -0,0 +1,39 @@
package report
import (
"github.com/google/pprof/profile"
)
// synthCode assigns addresses to locations without an address.
type synthCode struct {
next uint64
addr map[*profile.Location]uint64 // Synthesized address assigned to a location
}
func newSynthCode(mappings []*profile.Mapping) *synthCode {
// Find a larger address than any mapping.
s := &synthCode{next: 1}
for _, m := range mappings {
if s.next < m.Limit {
s.next = m.Limit
}
}
return s
}
// address returns the synthetic address for loc, creating one if needed.
func (s *synthCode) address(loc *profile.Location) uint64 {
if loc.Address != 0 {
panic("can only synthesize addresses for locations without an address")
}
if addr, ok := s.addr[loc]; ok {
return addr
}
if s.addr == nil {
s.addr = map[*profile.Location]uint64{}
}
addr := s.next
s.next++
s.addr[loc] = addr
return addr
}

View File

@ -308,7 +308,7 @@ func (p *Profile) postDecode() error {
if l.strX != 0 {
value, err = getString(p.stringTable, &l.strX, err)
labels[key] = append(labels[key], value)
} else if l.numX != 0 {
} else if l.numX != 0 || l.unitX != 0 {
numValues := numLabels[key]
units := numUnits[key]
if l.unitX != 0 {

View File

@ -231,7 +231,6 @@ func (pm *profileMerger) mapLocation(src *Location) *Location {
}
if l, ok := pm.locationsByID[src.ID]; ok {
pm.locationsByID[src.ID] = l
return l
}

View File

@ -1,4 +1,4 @@
# github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5
# github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a
## explicit; go 1.14
github.com/google/pprof/driver
github.com/google/pprof/internal/binutils