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

View as plain text