Home | History | Annotate | Download | only in pprof
      1 // Copyright 2014 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 // pprof is a tool for visualization of profile.data. It is based on
      6 // the upstream version at github.com/google/pprof, with minor
      7 // modifications specific to the Go distribution. Please consider
      8 // upstreaming any modifications to these packages.
      9 
     10 package main
     11 
     12 import (
     13 	"crypto/tls"
     14 	"debug/dwarf"
     15 	"fmt"
     16 	"io/ioutil"
     17 	"net/http"
     18 	"net/url"
     19 	"os"
     20 	"regexp"
     21 	"strconv"
     22 	"strings"
     23 	"sync"
     24 	"time"
     25 
     26 	"cmd/internal/objfile"
     27 
     28 	"github.com/google/pprof/driver"
     29 	"github.com/google/pprof/profile"
     30 )
     31 
     32 func main() {
     33 	options := &driver.Options{
     34 		Fetch: new(fetcher),
     35 		Obj:   new(objTool),
     36 	}
     37 	if err := driver.PProf(options); err != nil {
     38 		fmt.Fprintf(os.Stderr, "%v\n", err)
     39 		os.Exit(2)
     40 	}
     41 }
     42 
     43 type fetcher struct {
     44 }
     45 
     46 func (f *fetcher) Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error) {
     47 	sourceURL, timeout := adjustURL(src, duration, timeout)
     48 	if sourceURL == "" {
     49 		// Could not recognize URL, let regular pprof attempt to fetch the profile (eg. from a file)
     50 		return nil, "", nil
     51 	}
     52 	fmt.Fprintln(os.Stderr, "Fetching profile over HTTP from", sourceURL)
     53 	if duration > 0 {
     54 		fmt.Fprintf(os.Stderr, "Please wait... (%v)\n", duration)
     55 	}
     56 	p, err := getProfile(sourceURL, timeout)
     57 	return p, sourceURL, err
     58 }
     59 
     60 func getProfile(source string, timeout time.Duration) (*profile.Profile, error) {
     61 	url, err := url.Parse(source)
     62 	if err != nil {
     63 		return nil, err
     64 	}
     65 
     66 	var tlsConfig *tls.Config
     67 	if url.Scheme == "https+insecure" {
     68 		tlsConfig = &tls.Config{
     69 			InsecureSkipVerify: true,
     70 		}
     71 		url.Scheme = "https"
     72 		source = url.String()
     73 	}
     74 
     75 	client := &http.Client{
     76 		Transport: &http.Transport{
     77 			ResponseHeaderTimeout: timeout + 5*time.Second,
     78 			Proxy:           http.ProxyFromEnvironment,
     79 			TLSClientConfig: tlsConfig,
     80 		},
     81 	}
     82 	resp, err := client.Get(source)
     83 	if err != nil {
     84 		return nil, err
     85 	}
     86 	if resp.StatusCode != http.StatusOK {
     87 		defer resp.Body.Close()
     88 		return nil, statusCodeError(resp)
     89 	}
     90 	return profile.Parse(resp.Body)
     91 }
     92 
     93 func statusCodeError(resp *http.Response) error {
     94 	if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
     95 		// error is from pprof endpoint
     96 		if body, err := ioutil.ReadAll(resp.Body); err == nil {
     97 			return fmt.Errorf("server response: %s - %s", resp.Status, body)
     98 		}
     99 	}
    100 	return fmt.Errorf("server response: %s", resp.Status)
    101 }
    102 
    103 // cpuProfileHandler is the Go pprof CPU profile handler URL.
    104 const cpuProfileHandler = "/debug/pprof/profile"
    105 
    106 // adjustURL applies the duration/timeout values and Go specific defaults
    107 func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
    108 	u, err := url.Parse(source)
    109 	if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
    110 		// Try adding http:// to catch sources of the form hostname:port/path.
    111 		// url.Parse treats "hostname" as the scheme.
    112 		u, err = url.Parse("http://" + source)
    113 	}
    114 	if err != nil || u.Host == "" {
    115 		return "", 0
    116 	}
    117 
    118 	if u.Path == "" || u.Path == "/" {
    119 		u.Path = cpuProfileHandler
    120 	}
    121 
    122 	// Apply duration/timeout overrides to URL.
    123 	values := u.Query()
    124 	if duration > 0 {
    125 		values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
    126 	} else {
    127 		if urlSeconds := values.Get("seconds"); urlSeconds != "" {
    128 			if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
    129 				duration = time.Duration(us) * time.Second
    130 			}
    131 		}
    132 	}
    133 	if timeout <= 0 {
    134 		if duration > 0 {
    135 			timeout = duration + duration/2
    136 		} else {
    137 			timeout = 60 * time.Second
    138 		}
    139 	}
    140 	u.RawQuery = values.Encode()
    141 	return u.String(), timeout
    142 }
    143 
    144 // objTool implements driver.ObjTool using Go libraries
    145 // (instead of invoking GNU binutils).
    146 type objTool struct {
    147 	mu          sync.Mutex
    148 	disasmCache map[string]*objfile.Disasm
    149 }
    150 
    151 func (*objTool) Open(name string, start, limit, offset uint64) (driver.ObjFile, error) {
    152 	of, err := objfile.Open(name)
    153 	if err != nil {
    154 		return nil, err
    155 	}
    156 	f := &file{
    157 		name: name,
    158 		file: of,
    159 	}
    160 	if start != 0 {
    161 		if load, err := of.LoadAddress(); err == nil {
    162 			f.offset = start - load
    163 		}
    164 	}
    165 	return f, nil
    166 }
    167 
    168 func (*objTool) Demangle(names []string) (map[string]string, error) {
    169 	// No C++, nothing to demangle.
    170 	return make(map[string]string), nil
    171 }
    172 
    173 func (t *objTool) Disasm(file string, start, end uint64) ([]driver.Inst, error) {
    174 	d, err := t.cachedDisasm(file)
    175 	if err != nil {
    176 		return nil, err
    177 	}
    178 	var asm []driver.Inst
    179 	d.Decode(start, end, nil, func(pc, size uint64, file string, line int, text string) {
    180 		asm = append(asm, driver.Inst{Addr: pc, File: file, Line: line, Text: text})
    181 	})
    182 	return asm, nil
    183 }
    184 
    185 func (t *objTool) cachedDisasm(file string) (*objfile.Disasm, error) {
    186 	t.mu.Lock()
    187 	defer t.mu.Unlock()
    188 	if t.disasmCache == nil {
    189 		t.disasmCache = make(map[string]*objfile.Disasm)
    190 	}
    191 	d := t.disasmCache[file]
    192 	if d != nil {
    193 		return d, nil
    194 	}
    195 	f, err := objfile.Open(file)
    196 	if err != nil {
    197 		return nil, err
    198 	}
    199 	d, err = f.Disasm()
    200 	f.Close()
    201 	if err != nil {
    202 		return nil, err
    203 	}
    204 	t.disasmCache[file] = d
    205 	return d, nil
    206 }
    207 
    208 func (*objTool) SetConfig(config string) {
    209 	// config is usually used to say what binaries to invoke.
    210 	// Ignore entirely.
    211 }
    212 
    213 // file implements driver.ObjFile using Go libraries
    214 // (instead of invoking GNU binutils).
    215 // A file represents a single executable being analyzed.
    216 type file struct {
    217 	name   string
    218 	offset uint64
    219 	sym    []objfile.Sym
    220 	file   *objfile.File
    221 	pcln   objfile.Liner
    222 
    223 	triedDwarf bool
    224 	dwarf      *dwarf.Data
    225 }
    226 
    227 func (f *file) Name() string {
    228 	return f.name
    229 }
    230 
    231 func (f *file) Base() uint64 {
    232 	// No support for shared libraries.
    233 	return 0
    234 }
    235 
    236 func (f *file) BuildID() string {
    237 	// No support for build ID.
    238 	return ""
    239 }
    240 
    241 func (f *file) SourceLine(addr uint64) ([]driver.Frame, error) {
    242 	if f.pcln == nil {
    243 		pcln, err := f.file.PCLineTable()
    244 		if err != nil {
    245 			return nil, err
    246 		}
    247 		f.pcln = pcln
    248 	}
    249 	addr -= f.offset
    250 	file, line, fn := f.pcln.PCToLine(addr)
    251 	if fn != nil {
    252 		frame := []driver.Frame{
    253 			{
    254 				Func: fn.Name,
    255 				File: file,
    256 				Line: line,
    257 			},
    258 		}
    259 		return frame, nil
    260 	}
    261 
    262 	frames := f.dwarfSourceLine(addr)
    263 	if frames != nil {
    264 		return frames, nil
    265 	}
    266 
    267 	return nil, fmt.Errorf("no line information for PC=%#x", addr)
    268 }
    269 
    270 // dwarfSourceLine tries to get file/line information using DWARF.
    271 // This is for C functions that appear in the profile.
    272 // Returns nil if there is no information available.
    273 func (f *file) dwarfSourceLine(addr uint64) []driver.Frame {
    274 	if f.dwarf == nil && !f.triedDwarf {
    275 		// Ignore any error--we don't care exactly why there
    276 		// is no DWARF info.
    277 		f.dwarf, _ = f.file.DWARF()
    278 		f.triedDwarf = true
    279 	}
    280 
    281 	if f.dwarf != nil {
    282 		r := f.dwarf.Reader()
    283 		unit, err := r.SeekPC(addr)
    284 		if err == nil {
    285 			if frames := f.dwarfSourceLineEntry(r, unit, addr); frames != nil {
    286 				return frames
    287 			}
    288 		}
    289 	}
    290 
    291 	return nil
    292 }
    293 
    294 // dwarfSourceLineEntry tries to get file/line information from a
    295 // DWARF compilation unit. Returns nil if it doesn't find anything.
    296 func (f *file) dwarfSourceLineEntry(r *dwarf.Reader, entry *dwarf.Entry, addr uint64) []driver.Frame {
    297 	lines, err := f.dwarf.LineReader(entry)
    298 	if err != nil {
    299 		return nil
    300 	}
    301 	var lentry dwarf.LineEntry
    302 	if err := lines.SeekPC(addr, &lentry); err != nil {
    303 		return nil
    304 	}
    305 
    306 	// Try to find the function name.
    307 	name := ""
    308 FindName:
    309 	for entry, err := r.Next(); entry != nil && err == nil; entry, err = r.Next() {
    310 		if entry.Tag == dwarf.TagSubprogram {
    311 			ranges, err := f.dwarf.Ranges(entry)
    312 			if err != nil {
    313 				return nil
    314 			}
    315 			for _, pcs := range ranges {
    316 				if pcs[0] <= addr && addr < pcs[1] {
    317 					var ok bool
    318 					// TODO: AT_linkage_name, AT_MIPS_linkage_name.
    319 					name, ok = entry.Val(dwarf.AttrName).(string)
    320 					if ok {
    321 						break FindName
    322 					}
    323 				}
    324 			}
    325 		}
    326 	}
    327 
    328 	// TODO: Report inlined functions.
    329 
    330 	frames := []driver.Frame{
    331 		{
    332 			Func: name,
    333 			File: lentry.File.Name,
    334 			Line: lentry.Line,
    335 		},
    336 	}
    337 
    338 	return frames
    339 }
    340 
    341 func (f *file) Symbols(r *regexp.Regexp, addr uint64) ([]*driver.Sym, error) {
    342 	if f.sym == nil {
    343 		sym, err := f.file.Symbols()
    344 		if err != nil {
    345 			return nil, err
    346 		}
    347 		f.sym = sym
    348 	}
    349 	var out []*driver.Sym
    350 	for _, s := range f.sym {
    351 		// Ignore a symbol with address 0 and size 0.
    352 		// An ELF STT_FILE symbol will look like that.
    353 		if s.Addr == 0 && s.Size == 0 {
    354 			continue
    355 		}
    356 		if (r == nil || r.MatchString(s.Name)) && (addr == 0 || s.Addr <= addr && addr < s.Addr+uint64(s.Size)) {
    357 			out = append(out, &driver.Sym{
    358 				Name:  []string{s.Name},
    359 				File:  f.name,
    360 				Start: s.Addr,
    361 				End:   s.Addr + uint64(s.Size) - 1,
    362 			})
    363 		}
    364 	}
    365 	return out, nil
    366 }
    367 
    368 func (f *file) Close() error {
    369 	f.file.Close()
    370 	return nil
    371 }
    372