• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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