1// Copyright 2014 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package driver 16 17import ( 18 "bytes" 19 "fmt" 20 "io" 21 "net/http" 22 "net/url" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "runtime" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 32 "github.com/google/pprof/internal/measurement" 33 "github.com/google/pprof/internal/plugin" 34 "github.com/google/pprof/profile" 35) 36 37// fetchProfiles fetches and symbolizes the profiles specified by s. 38// It will merge all the profiles it is able to retrieve, even if 39// there are some failures. It will return an error if it is unable to 40// fetch any profiles. 41func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) { 42 sources := make([]profileSource, 0, len(s.Sources)) 43 for _, src := range s.Sources { 44 sources = append(sources, profileSource{ 45 addr: src, 46 source: s, 47 }) 48 } 49 50 bases := make([]profileSource, 0, len(s.Base)) 51 for _, src := range s.Base { 52 bases = append(bases, profileSource{ 53 addr: src, 54 source: s, 55 }) 56 } 57 58 p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport) 59 if err != nil { 60 return nil, err 61 } 62 63 if pbase != nil { 64 if s.DiffBase { 65 pbase.SetLabel("pprof::base", []string{"true"}) 66 } 67 if s.Normalize { 68 err := p.Normalize(pbase) 69 if err != nil { 70 return nil, err 71 } 72 } 73 pbase.Scale(-1) 74 p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase}) 75 if err != nil { 76 return nil, err 77 } 78 } 79 80 // Symbolize the merged profile. 81 if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil { 82 return nil, err 83 } 84 p.RemoveUninteresting() 85 unsourceMappings(p) 86 87 if s.Comment != "" { 88 p.Comments = append(p.Comments, s.Comment) 89 } 90 91 // Save a copy of the merged profile if there is at least one remote source. 92 if save { 93 dir, err := setTmpDir(o.UI) 94 if err != nil { 95 return nil, err 96 } 97 98 prefix := "pprof." 99 if len(p.Mapping) > 0 && p.Mapping[0].File != "" { 100 prefix += filepath.Base(p.Mapping[0].File) + "." 101 } 102 for _, s := range p.SampleType { 103 prefix += s.Type + "." 104 } 105 106 tempFile, err := newTempFile(dir, prefix, ".pb.gz") 107 if err == nil { 108 if err = p.Write(tempFile); err == nil { 109 o.UI.PrintErr("Saved profile in ", tempFile.Name()) 110 } 111 } 112 if err != nil { 113 o.UI.PrintErr("Could not save profile: ", err) 114 } 115 } 116 117 if err := p.CheckValid(); err != nil { 118 return nil, err 119 } 120 121 return p, nil 122} 123 124func grabSourcesAndBases(sources, bases []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, *profile.Profile, plugin.MappingSources, plugin.MappingSources, bool, error) { 125 wg := sync.WaitGroup{} 126 wg.Add(2) 127 var psrc, pbase *profile.Profile 128 var msrc, mbase plugin.MappingSources 129 var savesrc, savebase bool 130 var errsrc, errbase error 131 var countsrc, countbase int 132 go func() { 133 defer wg.Done() 134 psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr) 135 }() 136 go func() { 137 defer wg.Done() 138 pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui, tr) 139 }() 140 wg.Wait() 141 save := savesrc || savebase 142 143 if errsrc != nil { 144 return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc) 145 } 146 if errbase != nil { 147 return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase) 148 } 149 if countsrc == 0 { 150 return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles") 151 } 152 if countbase == 0 && len(bases) > 0 { 153 return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles") 154 } 155 if want, got := len(sources), countsrc; want != got { 156 ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want)) 157 } 158 if want, got := len(bases), countbase; want != got { 159 ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want)) 160 } 161 162 return psrc, pbase, msrc, mbase, save, nil 163} 164 165// chunkedGrab fetches the profiles described in source and merges them into 166// a single profile. It fetches a chunk of profiles concurrently, with a maximum 167// chunk size to limit its memory usage. 168func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) { 169 const chunkSize = 128 170 171 var p *profile.Profile 172 var msrc plugin.MappingSources 173 var save bool 174 var count int 175 176 for start := 0; start < len(sources); start += chunkSize { 177 end := start + chunkSize 178 if end > len(sources) { 179 end = len(sources) 180 } 181 chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui, tr) 182 switch { 183 case chunkErr != nil: 184 return nil, nil, false, 0, chunkErr 185 case chunkP == nil: 186 continue 187 case p == nil: 188 p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount 189 default: 190 p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc}) 191 if chunkErr != nil { 192 return nil, nil, false, 0, chunkErr 193 } 194 if chunkSave { 195 save = true 196 } 197 count += chunkCount 198 } 199 } 200 201 return p, msrc, save, count, nil 202} 203 204// concurrentGrab fetches multiple profiles concurrently 205func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) { 206 wg := sync.WaitGroup{} 207 wg.Add(len(sources)) 208 for i := range sources { 209 go func(s *profileSource) { 210 defer wg.Done() 211 s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr) 212 }(&sources[i]) 213 } 214 wg.Wait() 215 216 var save bool 217 profiles := make([]*profile.Profile, 0, len(sources)) 218 msrcs := make([]plugin.MappingSources, 0, len(sources)) 219 for i := range sources { 220 s := &sources[i] 221 if err := s.err; err != nil { 222 ui.PrintErr(s.addr + ": " + err.Error()) 223 continue 224 } 225 save = save || s.remote 226 profiles = append(profiles, s.p) 227 msrcs = append(msrcs, s.msrc) 228 *s = profileSource{} 229 } 230 231 if len(profiles) == 0 { 232 return nil, nil, false, 0, nil 233 } 234 235 p, msrc, err := combineProfiles(profiles, msrcs) 236 if err != nil { 237 return nil, nil, false, 0, err 238 } 239 return p, msrc, save, len(profiles), nil 240} 241 242func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) { 243 // Merge profiles. 244 // 245 // The merge call below only treats exactly matching sample type lists as 246 // compatible and will fail otherwise. Make the profiles' sample types 247 // compatible for the merge, see CompatibilizeSampleTypes() doc for details. 248 if err := profile.CompatibilizeSampleTypes(profiles); err != nil { 249 return nil, nil, err 250 } 251 if err := measurement.ScaleProfiles(profiles); err != nil { 252 return nil, nil, err 253 } 254 255 // Avoid expensive work for the common case of a single profile/src. 256 if len(profiles) == 1 && len(msrcs) == 1 { 257 return profiles[0], msrcs[0], nil 258 } 259 260 p, err := profile.Merge(profiles) 261 if err != nil { 262 return nil, nil, err 263 } 264 265 // Combine mapping sources. 266 msrc := make(plugin.MappingSources) 267 for _, ms := range msrcs { 268 for m, s := range ms { 269 msrc[m] = append(msrc[m], s...) 270 } 271 } 272 return p, msrc, nil 273} 274 275type profileSource struct { 276 addr string 277 source *source 278 279 p *profile.Profile 280 msrc plugin.MappingSources 281 remote bool 282 err error 283} 284 285func homeEnv() string { 286 switch runtime.GOOS { 287 case "windows": 288 return "USERPROFILE" 289 case "plan9": 290 return "home" 291 default: 292 return "HOME" 293 } 294} 295 296// setTmpDir prepares the directory to use to save profiles retrieved 297// remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof, and, if 298// $HOME is not set, falls back to os.TempDir(). 299func setTmpDir(ui plugin.UI) (string, error) { 300 var dirs []string 301 if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" { 302 dirs = append(dirs, profileDir) 303 } 304 if homeDir := os.Getenv(homeEnv()); homeDir != "" { 305 dirs = append(dirs, filepath.Join(homeDir, "pprof")) 306 } 307 dirs = append(dirs, os.TempDir()) 308 for _, tmpDir := range dirs { 309 if err := os.MkdirAll(tmpDir, 0755); err != nil { 310 ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error()) 311 continue 312 } 313 return tmpDir, nil 314 } 315 return "", fmt.Errorf("failed to identify temp dir") 316} 317 318const testSourceAddress = "pproftest.local" 319 320// grabProfile fetches a profile. Returns the profile, sources for the 321// profile mappings, a bool indicating if the profile was fetched 322// remotely, and an error. 323func grabProfile(s *source, source string, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) { 324 var src string 325 duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second 326 if fetcher != nil { 327 p, src, err = fetcher.Fetch(source, duration, timeout) 328 if err != nil { 329 return 330 } 331 } 332 if err != nil || p == nil { 333 // Fetch the profile over HTTP or from a file. 334 p, src, err = fetch(source, duration, timeout, ui, tr) 335 if err != nil { 336 return 337 } 338 } 339 340 if err = p.CheckValid(); err != nil { 341 return 342 } 343 344 // Update the binary locations from command line and paths. 345 locateBinaries(p, s, obj, ui) 346 347 // Collect the source URL for all mappings. 348 if src != "" { 349 msrc = collectMappingSources(p, src) 350 remote = true 351 if strings.HasPrefix(src, "http://"+testSourceAddress) { 352 // Treat test inputs as local to avoid saving 353 // testcase profiles during driver testing. 354 remote = false 355 } 356 } 357 return 358} 359 360// collectMappingSources saves the mapping sources of a profile. 361func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources { 362 ms := plugin.MappingSources{} 363 for _, m := range p.Mapping { 364 src := struct { 365 Source string 366 Start uint64 367 }{ 368 source, m.Start, 369 } 370 key := m.BuildID 371 if key == "" { 372 key = m.File 373 } 374 if key == "" { 375 // If there is no build id or source file, use the source as the 376 // mapping file. This will enable remote symbolization for this 377 // mapping, in particular for Go profiles on the legacy format. 378 // The source is reset back to empty string by unsourceMapping 379 // which is called after symbolization is finished. 380 m.File = source 381 key = source 382 } 383 ms[key] = append(ms[key], src) 384 } 385 return ms 386} 387 388// unsourceMappings iterates over the mappings in a profile and replaces file 389// set to the remote source URL by collectMappingSources back to empty string. 390func unsourceMappings(p *profile.Profile) { 391 for _, m := range p.Mapping { 392 if m.BuildID == "" && filepath.VolumeName(m.File) == "" { 393 if u, err := url.Parse(m.File); err == nil && u.IsAbs() { 394 m.File = "" 395 } 396 } 397 } 398} 399 400// locateBinaries searches for binary files listed in the profile and, if found, 401// updates the profile accordingly. 402func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) { 403 // Construct search path to examine 404 searchPath := os.Getenv("PPROF_BINARY_PATH") 405 if searchPath == "" { 406 // Use $HOME/pprof/binaries as default directory for local symbolization binaries 407 searchPath = filepath.Join(os.Getenv(homeEnv()), "pprof", "binaries") 408 } 409mapping: 410 for _, m := range p.Mapping { 411 var noVolumeFile string 412 var baseName string 413 var dirName string 414 if m.File != "" { 415 noVolumeFile = strings.TrimPrefix(m.File, filepath.VolumeName(m.File)) 416 baseName = filepath.Base(m.File) 417 dirName = filepath.Dir(noVolumeFile) 418 } 419 420 for _, path := range filepath.SplitList(searchPath) { 421 var fileNames []string 422 if m.BuildID != "" { 423 fileNames = []string{filepath.Join(path, m.BuildID, baseName)} 424 if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil { 425 fileNames = append(fileNames, matches...) 426 } 427 fileNames = append(fileNames, filepath.Join(path, noVolumeFile, m.BuildID)) // perf path format 428 // Llvm buildid protocol: the first two characters of the build id 429 // are used as directory, and the remaining part is in the filename. 430 // e.g. `/ab/cdef0123456.debug` 431 fileNames = append(fileNames, filepath.Join(path, m.BuildID[:2], m.BuildID[2:]+".debug")) 432 } 433 if m.File != "" { 434 // Try both the basename and the full path, to support the same directory 435 // structure as the perf symfs option. 436 fileNames = append(fileNames, filepath.Join(path, baseName)) 437 fileNames = append(fileNames, filepath.Join(path, noVolumeFile)) 438 // Other locations: use the same search paths as GDB, according to 439 // https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html 440 fileNames = append(fileNames, filepath.Join(path, noVolumeFile+".debug")) 441 fileNames = append(fileNames, filepath.Join(path, dirName, ".debug", baseName+".debug")) 442 fileNames = append(fileNames, filepath.Join(path, "usr", "lib", "debug", dirName, baseName+".debug")) 443 } 444 for _, name := range fileNames { 445 if f, err := obj.Open(name, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol); err == nil { 446 defer f.Close() 447 fileBuildID := f.BuildID() 448 if m.BuildID != "" && m.BuildID != fileBuildID { 449 ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")") 450 } else { 451 // Explicitly do not update KernelRelocationSymbol -- 452 // the new local file name is most likely missing it. 453 m.File = name 454 continue mapping 455 } 456 } 457 } 458 } 459 } 460 if len(p.Mapping) == 0 { 461 // If there are no mappings, add a fake mapping to attempt symbolization. 462 // This is useful for some profiles generated by the golang runtime, which 463 // do not include any mappings. Symbolization with a fake mapping will only 464 // be successful against a non-PIE binary. 465 m := &profile.Mapping{ID: 1} 466 p.Mapping = []*profile.Mapping{m} 467 for _, l := range p.Location { 468 l.Mapping = m 469 } 470 } 471 // If configured, apply executable filename override and (maybe, see below) 472 // build ID override from source. Assume the executable is the first mapping. 473 if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" { 474 m := p.Mapping[0] 475 if execName != "" { 476 // Explicitly do not update KernelRelocationSymbol -- 477 // the source override is most likely missing it. 478 m.File = execName 479 } 480 // Only apply the build ID override if the build ID in the main mapping is 481 // missing. Overwriting the build ID in case it's present is very likely a 482 // wrong thing to do so we refuse to do that. 483 if buildID != "" && m.BuildID == "" { 484 m.BuildID = buildID 485 } 486 } 487} 488 489// fetch fetches a profile from source, within the timeout specified, 490// producing messages through the ui. It returns the profile and the 491// url of the actual source of the profile for remote profiles. 492func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) { 493 var f io.ReadCloser 494 495 // First determine whether the source is a file, if not, it will be treated as a URL. 496 if _, err = os.Stat(source); err == nil { 497 if isPerfFile(source) { 498 f, err = convertPerfData(source, ui) 499 } else { 500 f, err = os.Open(source) 501 } 502 } else { 503 sourceURL, timeout := adjustURL(source, duration, timeout) 504 if sourceURL != "" { 505 ui.Print("Fetching profile over HTTP from " + sourceURL) 506 if duration > 0 { 507 ui.Print(fmt.Sprintf("Please wait... (%v)", duration)) 508 } 509 f, err = fetchURL(sourceURL, timeout, tr) 510 src = sourceURL 511 } 512 } 513 if err == nil { 514 defer f.Close() 515 p, err = profile.Parse(f) 516 } 517 return 518} 519 520// fetchURL fetches a profile from a URL using HTTP. 521func fetchURL(source string, timeout time.Duration, tr http.RoundTripper) (io.ReadCloser, error) { 522 client := &http.Client{ 523 Transport: tr, 524 Timeout: timeout + 5*time.Second, 525 } 526 resp, err := client.Get(source) 527 if err != nil { 528 return nil, fmt.Errorf("http fetch: %v", err) 529 } 530 if resp.StatusCode != http.StatusOK { 531 defer resp.Body.Close() 532 return nil, statusCodeError(resp) 533 } 534 535 return resp.Body, nil 536} 537 538func statusCodeError(resp *http.Response) error { 539 if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") { 540 // error is from pprof endpoint 541 if body, err := io.ReadAll(resp.Body); err == nil { 542 return fmt.Errorf("server response: %s - %s", resp.Status, body) 543 } 544 } 545 return fmt.Errorf("server response: %s", resp.Status) 546} 547 548// isPerfFile checks if a file is in perf.data format. It also returns false 549// if it encounters an error during the check. 550func isPerfFile(path string) bool { 551 sourceFile, openErr := os.Open(path) 552 if openErr != nil { 553 return false 554 } 555 defer sourceFile.Close() 556 557 // If the file is the output of a perf record command, it should begin 558 // with the string PERFILE2. 559 perfHeader := []byte("PERFILE2") 560 actualHeader := make([]byte, len(perfHeader)) 561 if _, readErr := sourceFile.Read(actualHeader); readErr != nil { 562 return false 563 } 564 return bytes.Equal(actualHeader, perfHeader) 565} 566 567// convertPerfData converts the file at path which should be in perf.data format 568// using the perf_to_profile tool and returns the file containing the 569// profile.proto formatted data. 570func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) { 571 ui.Print(fmt.Sprintf( 572 "Converting %s to a profile.proto... (May take a few minutes)", 573 perfPath)) 574 profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz") 575 if err != nil { 576 return nil, err 577 } 578 deferDeleteTempFile(profile.Name()) 579 cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f") 580 cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr 581 if err := cmd.Run(); err != nil { 582 profile.Close() 583 return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err) 584 } 585 return profile, nil 586} 587 588// adjustURL validates if a profile source is a URL and returns an 589// cleaned up URL and the timeout to use for retrieval over HTTP. 590// If the source cannot be recognized as a URL it returns an empty string. 591func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) { 592 u, err := url.Parse(source) 593 if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") { 594 // Try adding http:// to catch sources of the form hostname:port/path. 595 // url.Parse treats "hostname" as the scheme. 596 u, err = url.Parse("http://" + source) 597 } 598 if err != nil || u.Host == "" { 599 return "", 0 600 } 601 602 // Apply duration/timeout overrides to URL. 603 values := u.Query() 604 if duration > 0 { 605 values.Set("seconds", fmt.Sprint(int(duration.Seconds()))) 606 } else { 607 if urlSeconds := values.Get("seconds"); urlSeconds != "" { 608 if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil { 609 duration = time.Duration(us) * time.Second 610 } 611 } 612 } 613 if timeout <= 0 { 614 if duration > 0 { 615 timeout = duration + duration/2 616 } else { 617 timeout = 60 * time.Second 618 } 619 } 620 u.RawQuery = values.Encode() 621 return u.String(), timeout 622} 623