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