| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | package driver |
| |
|
| | import ( |
| | "bytes" |
| | "fmt" |
| | "io" |
| | "net/http" |
| | "net/url" |
| | "os" |
| | "os/exec" |
| | "path/filepath" |
| | "runtime" |
| | "strconv" |
| | "strings" |
| | "sync" |
| | "time" |
| |
|
| | "github.com/google/pprof/internal/measurement" |
| | "github.com/google/pprof/internal/plugin" |
| | "github.com/google/pprof/profile" |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) { |
| | sources := make([]profileSource, 0, len(s.Sources)) |
| | for _, src := range s.Sources { |
| | sources = append(sources, profileSource{ |
| | addr: src, |
| | source: s, |
| | }) |
| | } |
| |
|
| | bases := make([]profileSource, 0, len(s.Base)) |
| | for _, src := range s.Base { |
| | bases = append(bases, profileSource{ |
| | addr: src, |
| | source: s, |
| | }) |
| | } |
| |
|
| | p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport) |
| | if err != nil { |
| | return nil, err |
| | } |
| |
|
| | if pbase != nil { |
| | if s.DiffBase { |
| | pbase.SetLabel("pprof::base", []string{"true"}) |
| | } |
| | if s.Normalize { |
| | err := p.Normalize(pbase) |
| | if err != nil { |
| | return nil, err |
| | } |
| | } |
| | pbase.Scale(-1) |
| | p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase}) |
| | if err != nil { |
| | return nil, err |
| | } |
| | } |
| |
|
| | |
| | if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil { |
| | return nil, err |
| | } |
| | p.RemoveUninteresting() |
| | unsourceMappings(p) |
| |
|
| | if s.Comment != "" { |
| | p.Comments = append(p.Comments, s.Comment) |
| | } |
| |
|
| | |
| | if save { |
| | dir, err := setTmpDir(o.UI) |
| | if err != nil { |
| | return nil, err |
| | } |
| |
|
| | prefix := "pprof." |
| | if len(p.Mapping) > 0 && p.Mapping[0].File != "" { |
| | prefix += filepath.Base(p.Mapping[0].File) + "." |
| | } |
| | for _, s := range p.SampleType { |
| | prefix += s.Type + "." |
| | } |
| |
|
| | tempFile, err := newTempFile(dir, prefix, ".pb.gz") |
| | if err == nil { |
| | if err = p.Write(tempFile); err == nil { |
| | o.UI.PrintErr("Saved profile in ", tempFile.Name()) |
| | } |
| | } |
| | if err != nil { |
| | o.UI.PrintErr("Could not save profile: ", err) |
| | } |
| | } |
| |
|
| | if err := p.CheckValid(); err != nil { |
| | return nil, err |
| | } |
| |
|
| | return p, nil |
| | } |
| |
|
| | func 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) { |
| | wg := sync.WaitGroup{} |
| | wg.Add(2) |
| | var psrc, pbase *profile.Profile |
| | var msrc, mbase plugin.MappingSources |
| | var savesrc, savebase bool |
| | var errsrc, errbase error |
| | var countsrc, countbase int |
| | go func() { |
| | defer wg.Done() |
| | psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr) |
| | }() |
| | go func() { |
| | defer wg.Done() |
| | pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui, tr) |
| | }() |
| | wg.Wait() |
| | save := savesrc || savebase |
| |
|
| | if errsrc != nil { |
| | return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc) |
| | } |
| | if errbase != nil { |
| | return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase) |
| | } |
| | if countsrc == 0 { |
| | return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles") |
| | } |
| | if countbase == 0 && len(bases) > 0 { |
| | return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles") |
| | } |
| | if want, got := len(sources), countsrc; want != got { |
| | ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want)) |
| | } |
| | if want, got := len(bases), countbase; want != got { |
| | ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want)) |
| | } |
| |
|
| | return psrc, pbase, msrc, mbase, save, nil |
| | } |
| |
|
| | |
| | |
| | |
| | func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) { |
| | const chunkSize = 128 |
| |
|
| | var p *profile.Profile |
| | var msrc plugin.MappingSources |
| | var save bool |
| | var count int |
| |
|
| | for start := 0; start < len(sources); start += chunkSize { |
| | end := min(start+chunkSize, len(sources)) |
| | chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui, tr) |
| | switch { |
| | case chunkErr != nil: |
| | return nil, nil, false, 0, chunkErr |
| | case chunkP == nil: |
| | continue |
| | case p == nil: |
| | p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount |
| | default: |
| | p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc}) |
| | if chunkErr != nil { |
| | return nil, nil, false, 0, chunkErr |
| | } |
| | if chunkSave { |
| | save = true |
| | } |
| | count += chunkCount |
| | } |
| | } |
| |
|
| | return p, msrc, save, count, nil |
| | } |
| |
|
| | |
| | func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) { |
| | wg := sync.WaitGroup{} |
| | wg.Add(len(sources)) |
| | for i := range sources { |
| | go func(s *profileSource) { |
| | defer wg.Done() |
| | s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr) |
| | }(&sources[i]) |
| | } |
| | wg.Wait() |
| |
|
| | var save bool |
| | profiles := make([]*profile.Profile, 0, len(sources)) |
| | msrcs := make([]plugin.MappingSources, 0, len(sources)) |
| | for i := range sources { |
| | s := &sources[i] |
| | if err := s.err; err != nil { |
| | ui.PrintErr(s.addr + ": " + err.Error()) |
| | continue |
| | } |
| | save = save || s.remote |
| | profiles = append(profiles, s.p) |
| | msrcs = append(msrcs, s.msrc) |
| | *s = profileSource{} |
| | } |
| |
|
| | if len(profiles) == 0 { |
| | return nil, nil, false, 0, nil |
| | } |
| |
|
| | p, msrc, err := combineProfiles(profiles, msrcs) |
| | if err != nil { |
| | return nil, nil, false, 0, err |
| | } |
| | return p, msrc, save, len(profiles), nil |
| | } |
| |
|
| | func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) { |
| | |
| | |
| | |
| | |
| | |
| | if err := profile.CompatibilizeSampleTypes(profiles); err != nil { |
| | return nil, nil, err |
| | } |
| | if err := measurement.ScaleProfiles(profiles); err != nil { |
| | return nil, nil, err |
| | } |
| |
|
| | |
| | if len(profiles) == 1 && len(msrcs) == 1 { |
| | return profiles[0], msrcs[0], nil |
| | } |
| |
|
| | p, err := profile.Merge(profiles) |
| | if err != nil { |
| | return nil, nil, err |
| | } |
| |
|
| | |
| | msrc := make(plugin.MappingSources) |
| | for _, ms := range msrcs { |
| | for m, s := range ms { |
| | msrc[m] = append(msrc[m], s...) |
| | } |
| | } |
| | return p, msrc, nil |
| | } |
| |
|
| | type profileSource struct { |
| | addr string |
| | source *source |
| |
|
| | p *profile.Profile |
| | msrc plugin.MappingSources |
| | remote bool |
| | err error |
| | } |
| |
|
| | func homeEnv() string { |
| | switch runtime.GOOS { |
| | case "windows": |
| | return "USERPROFILE" |
| | case "plan9": |
| | return "home" |
| | default: |
| | return "HOME" |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | func setTmpDir(ui plugin.UI) (string, error) { |
| | var dirs []string |
| | if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" { |
| | dirs = append(dirs, profileDir) |
| | } |
| | if homeDir := os.Getenv(homeEnv()); homeDir != "" { |
| | dirs = append(dirs, filepath.Join(homeDir, "pprof")) |
| | } |
| | dirs = append(dirs, os.TempDir()) |
| | for _, tmpDir := range dirs { |
| | if err := os.MkdirAll(tmpDir, 0755); err != nil { |
| | ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error()) |
| | continue |
| | } |
| | return tmpDir, nil |
| | } |
| | return "", fmt.Errorf("failed to identify temp dir") |
| | } |
| |
|
| | const testSourceAddress = "pproftest.local" |
| |
|
| | |
| | |
| | |
| | func 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) { |
| | var src string |
| | duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second |
| | if fetcher != nil { |
| | p, src, err = fetcher.Fetch(source, duration, timeout) |
| | if err != nil { |
| | return |
| | } |
| | } |
| | if err != nil || p == nil { |
| | |
| | p, src, err = fetch(source, duration, timeout, ui, tr) |
| | if err != nil { |
| | return |
| | } |
| | } |
| |
|
| | if err = p.CheckValid(); err != nil { |
| | return |
| | } |
| |
|
| | |
| | locateBinaries(p, s, obj, ui) |
| |
|
| | |
| | if src != "" { |
| | msrc = collectMappingSources(p, src) |
| | remote = true |
| | if strings.HasPrefix(src, "http://"+testSourceAddress) { |
| | |
| | |
| | remote = false |
| | } |
| | } |
| | return |
| | } |
| |
|
| | |
| | func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources { |
| | ms := plugin.MappingSources{} |
| | for _, m := range p.Mapping { |
| | src := struct { |
| | Source string |
| | Start uint64 |
| | }{ |
| | source, m.Start, |
| | } |
| | key := m.BuildID |
| | if key == "" { |
| | key = m.File |
| | } |
| | if key == "" { |
| | |
| | |
| | |
| | |
| | |
| | m.File = source |
| | key = source |
| | } |
| | ms[key] = append(ms[key], src) |
| | } |
| | return ms |
| | } |
| |
|
| | |
| | |
| | func unsourceMappings(p *profile.Profile) { |
| | for _, m := range p.Mapping { |
| | if m.BuildID == "" && filepath.VolumeName(m.File) == "" { |
| | if u, err := url.Parse(m.File); err == nil && u.IsAbs() { |
| | m.File = "" |
| | } |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) { |
| | |
| | searchPath := os.Getenv("PPROF_BINARY_PATH") |
| | if searchPath == "" { |
| | |
| | searchPath = filepath.Join(os.Getenv(homeEnv()), "pprof", "binaries") |
| | } |
| | mapping: |
| | for _, m := range p.Mapping { |
| | var noVolumeFile string |
| | var baseName string |
| | var dirName string |
| | if m.File != "" { |
| | noVolumeFile = strings.TrimPrefix(m.File, filepath.VolumeName(m.File)) |
| | baseName = filepath.Base(m.File) |
| | dirName = filepath.Dir(noVolumeFile) |
| | } |
| |
|
| | for _, path := range filepath.SplitList(searchPath) { |
| | var fileNames []string |
| | if m.BuildID != "" { |
| | fileNames = []string{filepath.Join(path, m.BuildID, baseName)} |
| | if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil { |
| | fileNames = append(fileNames, matches...) |
| | } |
| | fileNames = append(fileNames, filepath.Join(path, noVolumeFile, m.BuildID)) |
| | |
| | |
| | |
| | fileNames = append(fileNames, filepath.Join(path, m.BuildID[:2], m.BuildID[2:]+".debug")) |
| | } |
| | if m.File != "" { |
| | |
| | |
| | fileNames = append(fileNames, filepath.Join(path, baseName)) |
| | fileNames = append(fileNames, filepath.Join(path, noVolumeFile)) |
| | |
| | |
| | fileNames = append(fileNames, filepath.Join(path, noVolumeFile+".debug")) |
| | fileNames = append(fileNames, filepath.Join(path, dirName, ".debug", baseName+".debug")) |
| | fileNames = append(fileNames, filepath.Join(path, "usr", "lib", "debug", dirName, baseName+".debug")) |
| | } |
| | for _, name := range fileNames { |
| | if f, err := obj.Open(name, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol); err == nil { |
| | defer f.Close() |
| | fileBuildID := f.BuildID() |
| | if m.BuildID != "" && m.BuildID != fileBuildID { |
| | ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")") |
| | } else { |
| | |
| | |
| | m.File = name |
| | continue mapping |
| | } |
| | } |
| | } |
| | } |
| | } |
| | if len(p.Mapping) == 0 { |
| | |
| | |
| | |
| | |
| | m := &profile.Mapping{ID: 1} |
| | p.Mapping = []*profile.Mapping{m} |
| | for _, l := range p.Location { |
| | l.Mapping = m |
| | } |
| | } |
| | |
| | |
| | if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" { |
| | m := p.Mapping[0] |
| | if execName != "" { |
| | |
| | |
| | m.File = execName |
| | } |
| | |
| | |
| | |
| | if buildID != "" && m.BuildID == "" { |
| | m.BuildID = buildID |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) { |
| | var f io.ReadCloser |
| |
|
| | |
| | if _, err = os.Stat(source); err == nil { |
| | if isPerfFile(source) { |
| | f, err = convertPerfData(source, ui) |
| | } else { |
| | f, err = os.Open(source) |
| | } |
| | } else { |
| | sourceURL, timeout := adjustURL(source, duration, timeout) |
| | if sourceURL != "" { |
| | ui.Print("Fetching profile over HTTP from " + sourceURL) |
| | if duration > 0 { |
| | ui.Print(fmt.Sprintf("Please wait... (%v)", duration)) |
| | } |
| | f, err = fetchURL(sourceURL, timeout, tr) |
| | src = sourceURL |
| | } |
| | } |
| | if err == nil { |
| | defer f.Close() |
| | p, err = profile.Parse(f) |
| | } |
| | return |
| | } |
| |
|
| | |
| | func fetchURL(source string, timeout time.Duration, tr http.RoundTripper) (io.ReadCloser, error) { |
| | client := &http.Client{ |
| | Transport: tr, |
| | Timeout: timeout + 5*time.Second, |
| | } |
| | resp, err := client.Get(source) |
| | if err != nil { |
| | return nil, fmt.Errorf("http fetch: %v", err) |
| | } |
| | if resp.StatusCode != http.StatusOK { |
| | defer resp.Body.Close() |
| | return nil, statusCodeError(resp) |
| | } |
| |
|
| | return resp.Body, nil |
| | } |
| |
|
| | func statusCodeError(resp *http.Response) error { |
| | if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") { |
| | |
| | if body, err := io.ReadAll(resp.Body); err == nil { |
| | return fmt.Errorf("server response: %s - %s", resp.Status, body) |
| | } |
| | } |
| | return fmt.Errorf("server response: %s", resp.Status) |
| | } |
| |
|
| | |
| | |
| | func isPerfFile(path string) bool { |
| | sourceFile, openErr := os.Open(path) |
| | if openErr != nil { |
| | return false |
| | } |
| | defer sourceFile.Close() |
| |
|
| | |
| | |
| | perfHeader := []byte("PERFILE2") |
| | actualHeader := make([]byte, len(perfHeader)) |
| | if _, readErr := sourceFile.Read(actualHeader); readErr != nil { |
| | return false |
| | } |
| | return bytes.Equal(actualHeader, perfHeader) |
| | } |
| |
|
| | |
| | |
| | |
| | func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) { |
| | ui.Print(fmt.Sprintf( |
| | "Converting %s to a profile.proto... (May take a few minutes)", |
| | perfPath)) |
| | profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz") |
| | if err != nil { |
| | return nil, err |
| | } |
| | deferDeleteTempFile(profile.Name()) |
| | cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f") |
| | cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr |
| | if err := cmd.Run(); err != nil { |
| | profile.Close() |
| | return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err) |
| | } |
| | return profile, nil |
| | } |
| |
|
| | |
| | |
| | |
| | func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) { |
| | u, err := url.Parse(source) |
| | if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") { |
| | |
| | |
| | u, err = url.Parse("http://" + source) |
| | } |
| | if err != nil || u.Host == "" { |
| | return "", 0 |
| | } |
| |
|
| | |
| | values := u.Query() |
| | if duration > 0 { |
| | values.Set("seconds", fmt.Sprint(int(duration.Seconds()))) |
| | } else { |
| | if urlSeconds := values.Get("seconds"); urlSeconds != "" { |
| | if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil { |
| | duration = time.Duration(us) * time.Second |
| | } |
| | } |
| | } |
| | if timeout <= 0 { |
| | if duration > 0 { |
| | timeout = duration + duration/2 |
| | } else { |
| | timeout = 60 * time.Second |
| | } |
| | } |
| | u.RawQuery = values.Encode() |
| | return u.String(), timeout |
| | } |
| |
|