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