golang.org/toolchain@v0.0.1-go1.9rc2.windows-amd64/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go (about)

     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  
    15  package driver
    16  
    17  import (
    18  	"bytes"
    19  	"crypto/tls"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"runtime"
    29  	"strconv"
    30  	"strings"
    31  	"sync"
    32  	"time"
    33  
    34  	"github.com/google/pprof/internal/measurement"
    35  	"github.com/google/pprof/internal/plugin"
    36  	"github.com/google/pprof/profile"
    37  )
    38  
    39  // fetchProfiles fetches and symbolizes the profiles specified by s.
    40  // It will merge all the profiles it is able to retrieve, even if
    41  // there are some failures. It will return an error if it is unable to
    42  // fetch any profiles.
    43  func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
    44  	sources := make([]profileSource, 0, len(s.Sources)+len(s.Base))
    45  	for _, src := range s.Sources {
    46  		sources = append(sources, profileSource{
    47  			addr:   src,
    48  			source: s,
    49  			scale:  1,
    50  		})
    51  	}
    52  	for _, src := range s.Base {
    53  		sources = append(sources, profileSource{
    54  			addr:   src,
    55  			source: s,
    56  			scale:  -1,
    57  		})
    58  	}
    59  	p, msrcs, save, cnt, err := chunkedGrab(sources, o.Fetch, o.Obj, o.UI)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	if cnt == 0 {
    64  		return nil, fmt.Errorf("failed to fetch any profiles")
    65  	}
    66  	if want, got := len(sources), cnt; want != got {
    67  		o.UI.PrintErr(fmt.Sprintf("fetched %d profiles out of %d", got, want))
    68  	}
    69  
    70  	// Symbolize the merged profile.
    71  	if err := o.Sym.Symbolize(s.Symbolize, msrcs, p); err != nil {
    72  		return nil, err
    73  	}
    74  	p.RemoveUninteresting()
    75  	unsourceMappings(p)
    76  
    77  	// Save a copy of the merged profile if there is at least one remote source.
    78  	if save {
    79  		dir, err := setTmpDir(o.UI)
    80  		if err != nil {
    81  			return nil, err
    82  		}
    83  
    84  		prefix := "pprof."
    85  		if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
    86  			prefix += filepath.Base(p.Mapping[0].File) + "."
    87  		}
    88  		for _, s := range p.SampleType {
    89  			prefix += s.Type + "."
    90  		}
    91  
    92  		tempFile, err := newTempFile(dir, prefix, ".pb.gz")
    93  		if err == nil {
    94  			if err = p.Write(tempFile); err == nil {
    95  				o.UI.PrintErr("Saved profile in ", tempFile.Name())
    96  			}
    97  		}
    98  		if err != nil {
    99  			o.UI.PrintErr("Could not save profile: ", err)
   100  		}
   101  	}
   102  
   103  	if err := p.CheckValid(); err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return p, nil
   108  }
   109  
   110  // chunkedGrab fetches the profiles described in source and merges them into
   111  // a single profile. It fetches a chunk of profiles concurrently, with a maximum
   112  // chunk size to limit its memory usage.
   113  func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (*profile.Profile, plugin.MappingSources, bool, int, error) {
   114  	const chunkSize = 64
   115  
   116  	var p *profile.Profile
   117  	var msrc plugin.MappingSources
   118  	var save bool
   119  	var count int
   120  
   121  	for start := 0; start < len(sources); start += chunkSize {
   122  		end := start + chunkSize
   123  		if end > len(sources) {
   124  			end = len(sources)
   125  		}
   126  		chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui)
   127  		switch {
   128  		case chunkErr != nil:
   129  			return nil, nil, false, 0, chunkErr
   130  		case chunkP == nil:
   131  			continue
   132  		case p == nil:
   133  			p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount
   134  		default:
   135  			p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc})
   136  			if chunkErr != nil {
   137  				return nil, nil, false, 0, chunkErr
   138  			}
   139  			if chunkSave {
   140  				save = true
   141  			}
   142  			count += chunkCount
   143  		}
   144  	}
   145  	return p, msrc, save, count, nil
   146  }
   147  
   148  // concurrentGrab fetches multiple profiles concurrently
   149  func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (*profile.Profile, plugin.MappingSources, bool, int, error) {
   150  	wg := sync.WaitGroup{}
   151  	wg.Add(len(sources))
   152  	for i := range sources {
   153  		go func(s *profileSource) {
   154  			defer wg.Done()
   155  			s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, s.scale, fetch, obj, ui)
   156  		}(&sources[i])
   157  	}
   158  	wg.Wait()
   159  
   160  	var save bool
   161  	profiles := make([]*profile.Profile, 0, len(sources))
   162  	msrcs := make([]plugin.MappingSources, 0, len(sources))
   163  	for i := range sources {
   164  		s := &sources[i]
   165  		if err := s.err; err != nil {
   166  			ui.PrintErr(s.addr + ": " + err.Error())
   167  			continue
   168  		}
   169  		save = save || s.remote
   170  		profiles = append(profiles, s.p)
   171  		msrcs = append(msrcs, s.msrc)
   172  		*s = profileSource{}
   173  	}
   174  
   175  	if len(profiles) == 0 {
   176  		return nil, nil, false, 0, nil
   177  	}
   178  
   179  	p, msrc, err := combineProfiles(profiles, msrcs)
   180  	if err != nil {
   181  		return nil, nil, false, 0, err
   182  	}
   183  	return p, msrc, save, len(profiles), nil
   184  }
   185  
   186  func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) {
   187  	// Merge profiles.
   188  	if err := measurement.ScaleProfiles(profiles); err != nil {
   189  		return nil, nil, err
   190  	}
   191  
   192  	p, err := profile.Merge(profiles)
   193  	if err != nil {
   194  		return nil, nil, err
   195  	}
   196  
   197  	// Combine mapping sources.
   198  	msrc := make(plugin.MappingSources)
   199  	for _, ms := range msrcs {
   200  		for m, s := range ms {
   201  			msrc[m] = append(msrc[m], s...)
   202  		}
   203  	}
   204  	return p, msrc, nil
   205  }
   206  
   207  type profileSource struct {
   208  	addr   string
   209  	source *source
   210  	scale  float64
   211  
   212  	p      *profile.Profile
   213  	msrc   plugin.MappingSources
   214  	remote bool
   215  	err    error
   216  }
   217  
   218  func homeEnv() string {
   219  	switch runtime.GOOS {
   220  	case "windows":
   221  		return "USERPROFILE"
   222  	case "plan9":
   223  		return "home"
   224  	default:
   225  		return "HOME"
   226  	}
   227  }
   228  
   229  // setTmpDir prepares the directory to use to save profiles retrieved
   230  // remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof.
   231  func setTmpDir(ui plugin.UI) (string, error) {
   232  	if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" {
   233  		return profileDir, nil
   234  	}
   235  	for _, tmpDir := range []string{os.Getenv(homeEnv()) + "/pprof", os.TempDir()} {
   236  		if err := os.MkdirAll(tmpDir, 0755); err != nil {
   237  			ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error())
   238  			continue
   239  		}
   240  		return tmpDir, nil
   241  	}
   242  	return "", fmt.Errorf("failed to identify temp dir")
   243  }
   244  
   245  // grabProfile fetches a profile. Returns the profile, sources for the
   246  // profile mappings, a bool indicating if the profile was fetched
   247  // remotely, and an error.
   248  func grabProfile(s *source, source string, scale float64, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
   249  	var src string
   250  	duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
   251  	if fetcher != nil {
   252  		p, src, err = fetcher.Fetch(source, duration, timeout)
   253  		if err != nil {
   254  			return
   255  		}
   256  	}
   257  	if err != nil || p == nil {
   258  		// Fetch the profile over HTTP or from a file.
   259  		p, src, err = fetch(source, duration, timeout, ui)
   260  		if err != nil {
   261  			return
   262  		}
   263  	}
   264  
   265  	if err = p.CheckValid(); err != nil {
   266  		return
   267  	}
   268  
   269  	// Apply local changes to the profile.
   270  	p.Scale(scale)
   271  
   272  	// Update the binary locations from command line and paths.
   273  	locateBinaries(p, s, obj, ui)
   274  
   275  	// Collect the source URL for all mappings.
   276  	if src != "" {
   277  		msrc = collectMappingSources(p, src)
   278  		remote = true
   279  	}
   280  	return
   281  }
   282  
   283  // collectMappingSources saves the mapping sources of a profile.
   284  func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources {
   285  	ms := plugin.MappingSources{}
   286  	for _, m := range p.Mapping {
   287  		src := struct {
   288  			Source string
   289  			Start  uint64
   290  		}{
   291  			source, m.Start,
   292  		}
   293  		key := m.BuildID
   294  		if key == "" {
   295  			key = m.File
   296  		}
   297  		if key == "" {
   298  			// If there is no build id or source file, use the source as the
   299  			// mapping file. This will enable remote symbolization for this
   300  			// mapping, in particular for Go profiles on the legacy format.
   301  			// The source is reset back to empty string by unsourceMapping
   302  			// which is called after symbolization is finished.
   303  			m.File = source
   304  			key = source
   305  		}
   306  		ms[key] = append(ms[key], src)
   307  	}
   308  	return ms
   309  }
   310  
   311  // unsourceMappings iterates over the mappings in a profile and replaces file
   312  // set to the remote source URL by collectMappingSources back to empty string.
   313  func unsourceMappings(p *profile.Profile) {
   314  	for _, m := range p.Mapping {
   315  		if m.BuildID == "" {
   316  			if u, err := url.Parse(m.File); err == nil && u.IsAbs() {
   317  				m.File = ""
   318  			}
   319  		}
   320  	}
   321  }
   322  
   323  // locateBinaries searches for binary files listed in the profile and, if found,
   324  // updates the profile accordingly.
   325  func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) {
   326  	// Construct search path to examine
   327  	searchPath := os.Getenv("PPROF_BINARY_PATH")
   328  	if searchPath == "" {
   329  		// Use $HOME/pprof/binaries as default directory for local symbolization binaries
   330  		searchPath = filepath.Join(os.Getenv(homeEnv()), "pprof", "binaries")
   331  	}
   332  mapping:
   333  	for _, m := range p.Mapping {
   334  		var baseName string
   335  		if m.File != "" {
   336  			baseName = filepath.Base(m.File)
   337  		}
   338  
   339  		for _, path := range filepath.SplitList(searchPath) {
   340  			var fileNames []string
   341  			if m.BuildID != "" {
   342  				fileNames = []string{filepath.Join(path, m.BuildID, baseName)}
   343  				if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
   344  					fileNames = append(fileNames, matches...)
   345  				}
   346  			}
   347  			if m.File != "" {
   348  				// Try both the basename and the full path, to support the same directory
   349  				// structure as the perf symfs option.
   350  				if baseName != "" {
   351  					fileNames = append(fileNames, filepath.Join(path, baseName))
   352  				}
   353  				fileNames = append(fileNames, filepath.Join(path, m.File))
   354  			}
   355  			for _, name := range fileNames {
   356  				if f, err := obj.Open(name, m.Start, m.Limit, m.Offset); err == nil {
   357  					defer f.Close()
   358  					fileBuildID := f.BuildID()
   359  					if m.BuildID != "" && m.BuildID != fileBuildID {
   360  						ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")")
   361  					} else {
   362  						m.File = name
   363  						continue mapping
   364  					}
   365  				}
   366  			}
   367  		}
   368  	}
   369  	// Replace executable filename/buildID with the overrides from source.
   370  	// Assumes the executable is the first Mapping entry.
   371  	if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" {
   372  		if len(p.Mapping) == 0 {
   373  			// If there are no mappings, add a fake mapping to attempt symbolization.
   374  			// This is useful for some profiles generated by the golang runtime, which
   375  			// do not include any mappings. Symbolization with a fake mapping will only
   376  			// be successful against a non-PIE binary.
   377  			m := &profile.Mapping{ID: 1}
   378  			p.Mapping = []*profile.Mapping{m}
   379  			for _, l := range p.Location {
   380  				l.Mapping = m
   381  			}
   382  		}
   383  		m := p.Mapping[0]
   384  		if execName != "" {
   385  			m.File = execName
   386  		}
   387  		if buildID != "" {
   388  			m.BuildID = buildID
   389  		}
   390  	}
   391  }
   392  
   393  // fetch fetches a profile from source, within the timeout specified,
   394  // producing messages through the ui. It returns the profile and the
   395  // url of the actual source of the profile for remote profiles.
   396  func fetch(source string, duration, timeout time.Duration, ui plugin.UI) (p *profile.Profile, src string, err error) {
   397  	var f io.ReadCloser
   398  
   399  	if sourceURL, timeout := adjustURL(source, duration, timeout); sourceURL != "" {
   400  		ui.Print("Fetching profile over HTTP from " + sourceURL)
   401  		if duration > 0 {
   402  			ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
   403  		}
   404  		f, err = fetchURL(sourceURL, timeout)
   405  		src = sourceURL
   406  	} else if isPerfFile(source) {
   407  		f, err = convertPerfData(source, ui)
   408  	} else {
   409  		f, err = os.Open(source)
   410  	}
   411  	if err == nil {
   412  		defer f.Close()
   413  		p, err = profile.Parse(f)
   414  	}
   415  	return
   416  }
   417  
   418  // fetchURL fetches a profile from a URL using HTTP.
   419  func fetchURL(source string, timeout time.Duration) (io.ReadCloser, error) {
   420  	resp, err := httpGet(source, timeout)
   421  	if err != nil {
   422  		return nil, fmt.Errorf("http fetch: %v", err)
   423  	}
   424  	if resp.StatusCode != http.StatusOK {
   425  		defer resp.Body.Close()
   426  		return nil, statusCodeError(resp)
   427  	}
   428  
   429  	return resp.Body, nil
   430  }
   431  
   432  func statusCodeError(resp *http.Response) error {
   433  	if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
   434  		// error is from pprof endpoint
   435  		if body, err := ioutil.ReadAll(resp.Body); err == nil {
   436  			return fmt.Errorf("server response: %s - %s", resp.Status, body)
   437  		}
   438  	}
   439  	return fmt.Errorf("server response: %s", resp.Status)
   440  }
   441  
   442  // isPerfFile checks if a file is in perf.data format. It also returns false
   443  // if it encounters an error during the check.
   444  func isPerfFile(path string) bool {
   445  	sourceFile, openErr := os.Open(path)
   446  	if openErr != nil {
   447  		return false
   448  	}
   449  	defer sourceFile.Close()
   450  
   451  	// If the file is the output of a perf record command, it should begin
   452  	// with the string PERFILE2.
   453  	perfHeader := []byte("PERFILE2")
   454  	actualHeader := make([]byte, len(perfHeader))
   455  	if _, readErr := sourceFile.Read(actualHeader); readErr != nil {
   456  		return false
   457  	}
   458  	return bytes.Equal(actualHeader, perfHeader)
   459  }
   460  
   461  // convertPerfData converts the file at path which should be in perf.data format
   462  // using the perf_to_profile tool and returns the file containing the
   463  // profile.proto formatted data.
   464  func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
   465  	ui.Print(fmt.Sprintf(
   466  		"Converting %s to a profile.proto... (May take a few minutes)",
   467  		perfPath))
   468  	profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz")
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  	deferDeleteTempFile(profile.Name())
   473  	cmd := exec.Command("perf_to_profile", perfPath, profile.Name())
   474  	if err := cmd.Run(); err != nil {
   475  		profile.Close()
   476  		return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
   477  	}
   478  	return profile, nil
   479  }
   480  
   481  // adjustURL validates if a profile source is a URL and returns an
   482  // cleaned up URL and the timeout to use for retrieval over HTTP.
   483  // If the source cannot be recognized as a URL it returns an empty string.
   484  func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
   485  	u, err := url.Parse(source)
   486  	if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
   487  		// Try adding http:// to catch sources of the form hostname:port/path.
   488  		// url.Parse treats "hostname" as the scheme.
   489  		u, err = url.Parse("http://" + source)
   490  	}
   491  	if err != nil || u.Host == "" {
   492  		return "", 0
   493  	}
   494  
   495  	// Apply duration/timeout overrides to URL.
   496  	values := u.Query()
   497  	if duration > 0 {
   498  		values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
   499  	} else {
   500  		if urlSeconds := values.Get("seconds"); urlSeconds != "" {
   501  			if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
   502  				duration = time.Duration(us) * time.Second
   503  			}
   504  		}
   505  	}
   506  	if timeout <= 0 {
   507  		if duration > 0 {
   508  			timeout = duration + duration/2
   509  		} else {
   510  			timeout = 60 * time.Second
   511  		}
   512  	}
   513  	u.RawQuery = values.Encode()
   514  	return u.String(), timeout
   515  }
   516  
   517  // httpGet is a wrapper around http.Get; it is defined as a variable
   518  // so it can be redefined during for testing.
   519  var httpGet = func(source string, timeout time.Duration) (*http.Response, error) {
   520  	url, err := url.Parse(source)
   521  	if err != nil {
   522  		return nil, err
   523  	}
   524  
   525  	var tlsConfig *tls.Config
   526  	if url.Scheme == "https+insecure" {
   527  		tlsConfig = &tls.Config{
   528  			InsecureSkipVerify: true,
   529  		}
   530  		url.Scheme = "https"
   531  		source = url.String()
   532  	}
   533  
   534  	client := &http.Client{
   535  		Transport: &http.Transport{
   536  			ResponseHeaderTimeout: timeout + 5*time.Second,
   537  			Proxy:           http.ProxyFromEnvironment,
   538  			TLSClientConfig: tlsConfig,
   539  		},
   540  	}
   541  	return client.Get(source)
   542  }