mirror of
https://github.com/golang/go
synced 2024-11-13 20:10:32 -07:00
90b7058ec4
Restores functionality added in https://golang.org/cl/35564/ which was lost in https://golang.org/cl/36798/ by the addition of the custom fetcher to src/cmd/pprof/pprof.go. The custom fetcher overrides the upstream default. Change-Id: Ic71e5e475d043276d916298ab5acb5c9b9ad063e Reviewed-on: https://go-review.googlesource.com/45812 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
372 lines
8.5 KiB
Go
372 lines
8.5 KiB
Go
// Copyright 2014 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.
|
|
|
|
// pprof is a tool for visualization of profile.data. It is based on
|
|
// the upstream version at github.com/google/pprof, with minor
|
|
// modifications specific to the Go distribution. Please consider
|
|
// upstreaming any modifications to these packages.
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"debug/dwarf"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"cmd/internal/objfile"
|
|
|
|
"github.com/google/pprof/driver"
|
|
"github.com/google/pprof/profile"
|
|
)
|
|
|
|
func main() {
|
|
options := &driver.Options{
|
|
Fetch: new(fetcher),
|
|
Obj: new(objTool),
|
|
}
|
|
if err := driver.PProf(options); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
type fetcher struct {
|
|
}
|
|
|
|
func (f *fetcher) Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error) {
|
|
sourceURL, timeout := adjustURL(src, duration, timeout)
|
|
if sourceURL == "" {
|
|
// Could not recognize URL, let regular pprof attempt to fetch the profile (eg. from a file)
|
|
return nil, "", nil
|
|
}
|
|
fmt.Fprintln(os.Stderr, "Fetching profile over HTTP from", sourceURL)
|
|
if duration > 0 {
|
|
fmt.Fprintf(os.Stderr, "Please wait... (%v)\n", duration)
|
|
}
|
|
p, err := getProfile(sourceURL, timeout)
|
|
return p, sourceURL, err
|
|
}
|
|
|
|
func getProfile(source string, timeout time.Duration) (*profile.Profile, error) {
|
|
url, err := url.Parse(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tlsConfig *tls.Config
|
|
if url.Scheme == "https+insecure" {
|
|
tlsConfig = &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
url.Scheme = "https"
|
|
source = url.String()
|
|
}
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
ResponseHeaderTimeout: timeout + 5*time.Second,
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: tlsConfig,
|
|
},
|
|
}
|
|
resp, err := client.Get(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
defer resp.Body.Close()
|
|
return nil, statusCodeError(resp)
|
|
}
|
|
return profile.Parse(resp.Body)
|
|
}
|
|
|
|
func statusCodeError(resp *http.Response) error {
|
|
if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
|
|
// error is from pprof endpoint
|
|
if body, err := ioutil.ReadAll(resp.Body); err == nil {
|
|
return fmt.Errorf("server response: %s - %s", resp.Status, body)
|
|
}
|
|
}
|
|
return fmt.Errorf("server response: %s", resp.Status)
|
|
}
|
|
|
|
// cpuProfileHandler is the Go pprof CPU profile handler URL.
|
|
const cpuProfileHandler = "/debug/pprof/profile"
|
|
|
|
// adjustURL applies the duration/timeout values and Go specific defaults
|
|
func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
|
|
u, err := url.Parse(source)
|
|
if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
|
|
// Try adding http:// to catch sources of the form hostname:port/path.
|
|
// url.Parse treats "hostname" as the scheme.
|
|
u, err = url.Parse("http://" + source)
|
|
}
|
|
if err != nil || u.Host == "" {
|
|
return "", 0
|
|
}
|
|
|
|
if u.Path == "" || u.Path == "/" {
|
|
u.Path = cpuProfileHandler
|
|
}
|
|
|
|
// Apply duration/timeout overrides to URL.
|
|
values := u.Query()
|
|
if duration > 0 {
|
|
values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
|
|
} else {
|
|
if urlSeconds := values.Get("seconds"); urlSeconds != "" {
|
|
if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
|
|
duration = time.Duration(us) * time.Second
|
|
}
|
|
}
|
|
}
|
|
if timeout <= 0 {
|
|
if duration > 0 {
|
|
timeout = duration + duration/2
|
|
} else {
|
|
timeout = 60 * time.Second
|
|
}
|
|
}
|
|
u.RawQuery = values.Encode()
|
|
return u.String(), timeout
|
|
}
|
|
|
|
// objTool implements driver.ObjTool using Go libraries
|
|
// (instead of invoking GNU binutils).
|
|
type objTool struct {
|
|
mu sync.Mutex
|
|
disasmCache map[string]*objfile.Disasm
|
|
}
|
|
|
|
func (*objTool) Open(name string, start, limit, offset uint64) (driver.ObjFile, error) {
|
|
of, err := objfile.Open(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := &file{
|
|
name: name,
|
|
file: of,
|
|
}
|
|
if start != 0 {
|
|
if load, err := of.LoadAddress(); err == nil {
|
|
f.offset = start - load
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (*objTool) Demangle(names []string) (map[string]string, error) {
|
|
// No C++, nothing to demangle.
|
|
return make(map[string]string), nil
|
|
}
|
|
|
|
func (t *objTool) Disasm(file string, start, end uint64) ([]driver.Inst, error) {
|
|
d, err := t.cachedDisasm(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var asm []driver.Inst
|
|
d.Decode(start, end, nil, func(pc, size uint64, file string, line int, text string) {
|
|
asm = append(asm, driver.Inst{Addr: pc, File: file, Line: line, Text: text})
|
|
})
|
|
return asm, nil
|
|
}
|
|
|
|
func (t *objTool) cachedDisasm(file string) (*objfile.Disasm, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.disasmCache == nil {
|
|
t.disasmCache = make(map[string]*objfile.Disasm)
|
|
}
|
|
d := t.disasmCache[file]
|
|
if d != nil {
|
|
return d, nil
|
|
}
|
|
f, err := objfile.Open(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d, err = f.Disasm()
|
|
f.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.disasmCache[file] = d
|
|
return d, nil
|
|
}
|
|
|
|
func (*objTool) SetConfig(config string) {
|
|
// config is usually used to say what binaries to invoke.
|
|
// Ignore entirely.
|
|
}
|
|
|
|
// file implements driver.ObjFile using Go libraries
|
|
// (instead of invoking GNU binutils).
|
|
// A file represents a single executable being analyzed.
|
|
type file struct {
|
|
name string
|
|
offset uint64
|
|
sym []objfile.Sym
|
|
file *objfile.File
|
|
pcln objfile.Liner
|
|
|
|
triedDwarf bool
|
|
dwarf *dwarf.Data
|
|
}
|
|
|
|
func (f *file) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f *file) Base() uint64 {
|
|
// No support for shared libraries.
|
|
return 0
|
|
}
|
|
|
|
func (f *file) BuildID() string {
|
|
// No support for build ID.
|
|
return ""
|
|
}
|
|
|
|
func (f *file) SourceLine(addr uint64) ([]driver.Frame, error) {
|
|
if f.pcln == nil {
|
|
pcln, err := f.file.PCLineTable()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f.pcln = pcln
|
|
}
|
|
addr -= f.offset
|
|
file, line, fn := f.pcln.PCToLine(addr)
|
|
if fn != nil {
|
|
frame := []driver.Frame{
|
|
{
|
|
Func: fn.Name,
|
|
File: file,
|
|
Line: line,
|
|
},
|
|
}
|
|
return frame, nil
|
|
}
|
|
|
|
frames := f.dwarfSourceLine(addr)
|
|
if frames != nil {
|
|
return frames, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("no line information for PC=%#x", addr)
|
|
}
|
|
|
|
// dwarfSourceLine tries to get file/line information using DWARF.
|
|
// This is for C functions that appear in the profile.
|
|
// Returns nil if there is no information available.
|
|
func (f *file) dwarfSourceLine(addr uint64) []driver.Frame {
|
|
if f.dwarf == nil && !f.triedDwarf {
|
|
// Ignore any error--we don't care exactly why there
|
|
// is no DWARF info.
|
|
f.dwarf, _ = f.file.DWARF()
|
|
f.triedDwarf = true
|
|
}
|
|
|
|
if f.dwarf != nil {
|
|
r := f.dwarf.Reader()
|
|
unit, err := r.SeekPC(addr)
|
|
if err == nil {
|
|
if frames := f.dwarfSourceLineEntry(r, unit, addr); frames != nil {
|
|
return frames
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// dwarfSourceLineEntry tries to get file/line information from a
|
|
// DWARF compilation unit. Returns nil if it doesn't find anything.
|
|
func (f *file) dwarfSourceLineEntry(r *dwarf.Reader, entry *dwarf.Entry, addr uint64) []driver.Frame {
|
|
lines, err := f.dwarf.LineReader(entry)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var lentry dwarf.LineEntry
|
|
if err := lines.SeekPC(addr, &lentry); err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Try to find the function name.
|
|
name := ""
|
|
FindName:
|
|
for entry, err := r.Next(); entry != nil && err == nil; entry, err = r.Next() {
|
|
if entry.Tag == dwarf.TagSubprogram {
|
|
ranges, err := f.dwarf.Ranges(entry)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
for _, pcs := range ranges {
|
|
if pcs[0] <= addr && addr < pcs[1] {
|
|
var ok bool
|
|
// TODO: AT_linkage_name, AT_MIPS_linkage_name.
|
|
name, ok = entry.Val(dwarf.AttrName).(string)
|
|
if ok {
|
|
break FindName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Report inlined functions.
|
|
|
|
frames := []driver.Frame{
|
|
{
|
|
Func: name,
|
|
File: lentry.File.Name,
|
|
Line: lentry.Line,
|
|
},
|
|
}
|
|
|
|
return frames
|
|
}
|
|
|
|
func (f *file) Symbols(r *regexp.Regexp, addr uint64) ([]*driver.Sym, error) {
|
|
if f.sym == nil {
|
|
sym, err := f.file.Symbols()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f.sym = sym
|
|
}
|
|
var out []*driver.Sym
|
|
for _, s := range f.sym {
|
|
// Ignore a symbol with address 0 and size 0.
|
|
// An ELF STT_FILE symbol will look like that.
|
|
if s.Addr == 0 && s.Size == 0 {
|
|
continue
|
|
}
|
|
if (r == nil || r.MatchString(s.Name)) && (addr == 0 || s.Addr <= addr && addr < s.Addr+uint64(s.Size)) {
|
|
out = append(out, &driver.Sym{
|
|
Name: []string{s.Name},
|
|
File: f.name,
|
|
Start: s.Addr,
|
|
End: s.Addr + uint64(s.Size) - 1,
|
|
})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *file) Close() error {
|
|
f.file.Close()
|
|
return nil
|
|
}
|