Source file src/cmd/pprof/pprof.go

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

View as plain text