| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package driver implements the core pprof functionality. It can be |
| // parameterized with a flag implementation, fetch and symbolize |
| // mechanisms. |
| package driver |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "net/url" |
| "os" |
| "path/filepath" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "cmd/pprof/internal/commands" |
| "cmd/pprof/internal/plugin" |
| "cmd/pprof/internal/report" |
| "cmd/pprof/internal/tempfile" |
| "internal/pprof/profile" |
| ) |
| |
| // cpuProfileHandler is the Go pprof CPU profile handler URL. |
| const cpuProfileHandler = "/debug/pprof/profile" |
| |
| // PProf acquires a profile, and symbolizes it using a profile |
| // manager. Then it generates a report formatted according to the |
| // options selected through the flags package. |
| func PProf(flagset plugin.FlagSet, fetch plugin.Fetcher, sym plugin.Symbolizer, obj plugin.ObjTool, ui plugin.UI, overrides commands.Commands) error { |
| // Remove any temporary files created during pprof processing. |
| defer tempfile.Cleanup() |
| |
| f, err := getFlags(flagset, overrides, ui) |
| if err != nil { |
| return err |
| } |
| |
| obj.SetConfig(*f.flagTools) |
| |
| sources := f.profileSource |
| if len(sources) > 1 { |
| source := sources[0] |
| // If the first argument is a supported object file, treat as executable. |
| if file, err := obj.Open(source, 0); err == nil { |
| file.Close() |
| f.profileExecName = source |
| sources = sources[1:] |
| } else if *f.flagBuildID == "" && isBuildID(source) { |
| f.flagBuildID = &source |
| sources = sources[1:] |
| } |
| } |
| |
| // errMu protects concurrent accesses to errset and err. errset is set if an |
| // error is encountered by one of the goroutines grabbing a profile. |
| errMu, errset := sync.Mutex{}, false |
| |
| // Fetch profiles. |
| wg := sync.WaitGroup{} |
| profs := make([]*profile.Profile, len(sources)) |
| for i, source := range sources { |
| wg.Add(1) |
| go func(i int, src string) { |
| defer wg.Done() |
| p, grabErr := grabProfile(src, f.profileExecName, *f.flagBuildID, fetch, sym, obj, ui, f) |
| if grabErr != nil { |
| errMu.Lock() |
| defer errMu.Unlock() |
| errset, err = true, grabErr |
| return |
| } |
| profs[i] = p |
| }(i, source) |
| } |
| wg.Wait() |
| if errset { |
| return err |
| } |
| |
| // Merge profiles. |
| prof := profs[0] |
| for _, p := range profs[1:] { |
| if err = prof.Merge(p, 1); err != nil { |
| return err |
| } |
| } |
| |
| if *f.flagBase != "" { |
| // Fetch base profile and subtract from current profile. |
| base, err := grabProfile(*f.flagBase, f.profileExecName, *f.flagBuildID, fetch, sym, obj, ui, f) |
| if err != nil { |
| return err |
| } |
| |
| if err = prof.Merge(base, -1); err != nil { |
| return err |
| } |
| } |
| |
| if err := processFlags(prof, ui, f); err != nil { |
| return err |
| } |
| |
| if !*f.flagRuntime { |
| prof.RemoveUninteresting() |
| } |
| |
| if *f.flagInteractive { |
| return interactive(prof, obj, ui, f) |
| } |
| |
| return generate(false, prof, obj, ui, f) |
| } |
| |
| // isBuildID determines if the profile may contain a build ID, by |
| // checking that it is a string of hex digits. |
| func isBuildID(id string) bool { |
| return strings.Trim(id, "0123456789abcdefABCDEF") == "" |
| } |
| |
| // adjustURL updates the profile source URL based on heuristics. It |
| // will append ?seconds=sec for CPU profiles if not already |
| // specified. Returns the hostname if the profile is remote. |
| func adjustURL(source string, sec int, ui plugin.UI) (adjusted, host string, duration time.Duration) { |
| // If there is a local file with this name, just use it. |
| if _, err := os.Stat(source); err == nil { |
| return source, "", 0 |
| } |
| |
| url, err := url.Parse(source) |
| |
| // Automatically add http:// to URLs of the form hostname:port/path. |
| // url.Parse treats "hostname" as the Scheme. |
| if err != nil || (url.Host == "" && url.Scheme != "" && url.Scheme != "file") { |
| url, err = url.Parse("http://" + source) |
| if err != nil { |
| return source, "", 0 |
| } |
| } |
| if scheme := strings.ToLower(url.Scheme); scheme == "" || scheme == "file" { |
| url.Scheme = "" |
| return url.String(), "", 0 |
| } |
| |
| values := url.Query() |
| if urlSeconds := values.Get("seconds"); urlSeconds != "" { |
| if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil { |
| if sec >= 0 { |
| ui.PrintErr("Overriding -seconds for URL ", source) |
| } |
| sec = int(us) |
| } |
| } |
| |
| switch strings.ToLower(url.Path) { |
| case "", "/": |
| // Apply default /profilez. |
| url.Path = cpuProfileHandler |
| case "/protoz": |
| // Rewrite to /profilez?type=proto |
| url.Path = cpuProfileHandler |
| values.Set("type", "proto") |
| } |
| |
| if hasDuration(url.Path) { |
| if sec > 0 { |
| duration = time.Duration(sec) * time.Second |
| values.Set("seconds", fmt.Sprintf("%d", sec)) |
| } else { |
| // Assume default duration: 30 seconds |
| duration = 30 * time.Second |
| } |
| } |
| url.RawQuery = values.Encode() |
| return url.String(), url.Host, duration |
| } |
| |
| func hasDuration(path string) bool { |
| for _, trigger := range []string{"profilez", "wallz", "/profile"} { |
| if strings.Contains(path, trigger) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // preprocess does filtering and aggregation of a profile based on the |
| // requested options. |
| func preprocess(prof *profile.Profile, ui plugin.UI, f *flags) error { |
| if *f.flagFocus != "" || *f.flagIgnore != "" || *f.flagHide != "" { |
| focus, ignore, hide, err := compileFocusIgnore(*f.flagFocus, *f.flagIgnore, *f.flagHide) |
| if err != nil { |
| return err |
| } |
| fm, im, hm := prof.FilterSamplesByName(focus, ignore, hide) |
| |
| warnNoMatches(fm, *f.flagFocus, "Focus", ui) |
| warnNoMatches(im, *f.flagIgnore, "Ignore", ui) |
| warnNoMatches(hm, *f.flagHide, "Hide", ui) |
| } |
| |
| if *f.flagTagFocus != "" || *f.flagTagIgnore != "" { |
| focus, err := compileTagFilter(*f.flagTagFocus, ui) |
| if err != nil { |
| return err |
| } |
| ignore, err := compileTagFilter(*f.flagTagIgnore, ui) |
| if err != nil { |
| return err |
| } |
| fm, im := prof.FilterSamplesByTag(focus, ignore) |
| |
| warnNoMatches(fm, *f.flagTagFocus, "TagFocus", ui) |
| warnNoMatches(im, *f.flagTagIgnore, "TagIgnore", ui) |
| } |
| |
| return aggregate(prof, f) |
| } |
| |
| func compileFocusIgnore(focus, ignore, hide string) (f, i, h *regexp.Regexp, err error) { |
| if focus != "" { |
| if f, err = regexp.Compile(focus); err != nil { |
| return nil, nil, nil, fmt.Errorf("parsing focus regexp: %v", err) |
| } |
| } |
| |
| if ignore != "" { |
| if i, err = regexp.Compile(ignore); err != nil { |
| return nil, nil, nil, fmt.Errorf("parsing ignore regexp: %v", err) |
| } |
| } |
| |
| if hide != "" { |
| if h, err = regexp.Compile(hide); err != nil { |
| return nil, nil, nil, fmt.Errorf("parsing hide regexp: %v", err) |
| } |
| } |
| return |
| } |
| |
| func compileTagFilter(filter string, ui plugin.UI) (f func(string, string, int64) bool, err error) { |
| if filter == "" { |
| return nil, nil |
| } |
| if numFilter := parseTagFilterRange(filter); numFilter != nil { |
| ui.PrintErr("Interpreted '", filter, "' as range, not regexp") |
| return func(key, val string, num int64) bool { |
| if val != "" { |
| return false |
| } |
| return numFilter(num, key) |
| }, nil |
| } |
| fx, err := regexp.Compile(filter) |
| if err != nil { |
| return nil, err |
| } |
| |
| return func(key, val string, num int64) bool { |
| if val == "" { |
| return false |
| } |
| return fx.MatchString(key + ":" + val) |
| }, nil |
| } |
| |
| var tagFilterRangeRx = regexp.MustCompile("([[:digit:]]+)([[:alpha:]]+)") |
| |
| // parseTagFilterRange returns a function to checks if a value is |
| // contained on the range described by a string. It can recognize |
| // strings of the form: |
| // "32kb" -- matches values == 32kb |
| // ":64kb" -- matches values <= 64kb |
| // "4mb:" -- matches values >= 4mb |
| // "12kb:64mb" -- matches values between 12kb and 64mb (both included). |
| func parseTagFilterRange(filter string) func(int64, string) bool { |
| ranges := tagFilterRangeRx.FindAllStringSubmatch(filter, 2) |
| if len(ranges) == 0 { |
| return nil // No ranges were identified |
| } |
| v, err := strconv.ParseInt(ranges[0][1], 10, 64) |
| if err != nil { |
| panic(fmt.Errorf("Failed to parse int %s: %v", ranges[0][1], err)) |
| } |
| value, unit := report.ScaleValue(v, ranges[0][2], ranges[0][2]) |
| if len(ranges) == 1 { |
| switch match := ranges[0][0]; filter { |
| case match: |
| return func(v int64, u string) bool { |
| sv, su := report.ScaleValue(v, u, unit) |
| return su == unit && sv == value |
| } |
| case match + ":": |
| return func(v int64, u string) bool { |
| sv, su := report.ScaleValue(v, u, unit) |
| return su == unit && sv >= value |
| } |
| case ":" + match: |
| return func(v int64, u string) bool { |
| sv, su := report.ScaleValue(v, u, unit) |
| return su == unit && sv <= value |
| } |
| } |
| return nil |
| } |
| if filter != ranges[0][0]+":"+ranges[1][0] { |
| return nil |
| } |
| if v, err = strconv.ParseInt(ranges[1][1], 10, 64); err != nil { |
| panic(fmt.Errorf("Failed to parse int %s: %v", ranges[1][1], err)) |
| } |
| value2, unit2 := report.ScaleValue(v, ranges[1][2], unit) |
| if unit != unit2 { |
| return nil |
| } |
| return func(v int64, u string) bool { |
| sv, su := report.ScaleValue(v, u, unit) |
| return su == unit && sv >= value && sv <= value2 |
| } |
| } |
| |
| func warnNoMatches(match bool, rx, option string, ui plugin.UI) { |
| if !match && rx != "" && rx != "." { |
| ui.PrintErr(option + " expression matched no samples: " + rx) |
| } |
| } |
| |
| // grabProfile fetches and symbolizes a profile. |
| func grabProfile(source, exec, buildid string, fetch plugin.Fetcher, sym plugin.Symbolizer, obj plugin.ObjTool, ui plugin.UI, f *flags) (*profile.Profile, error) { |
| source, host, duration := adjustURL(source, *f.flagSeconds, ui) |
| remote := host != "" |
| |
| if remote { |
| ui.Print("Fetching profile from ", source) |
| if duration != 0 { |
| ui.Print("Please wait... (" + duration.String() + ")") |
| } |
| } |
| |
| now := time.Now() |
| // Fetch profile from source. |
| // Give 50% slack on the timeout. |
| p, err := fetch(source, duration+duration/2, ui) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Update the time/duration if the profile source doesn't include it. |
| // TODO(rsilvera): Remove this when we remove support for legacy profiles. |
| if remote { |
| if p.TimeNanos == 0 { |
| p.TimeNanos = now.UnixNano() |
| } |
| if duration != 0 && p.DurationNanos == 0 { |
| p.DurationNanos = int64(duration) |
| } |
| } |
| |
| // Replace executable/buildID with the options provided in the |
| // command line. Assume the executable is the first Mapping entry. |
| if exec != "" || buildid != "" { |
| if len(p.Mapping) == 0 { |
| // Create a fake mapping to hold the user option, and associate |
| // all samples to it. |
| m := &profile.Mapping{ |
| ID: 1, |
| } |
| for _, l := range p.Location { |
| l.Mapping = m |
| } |
| p.Mapping = []*profile.Mapping{m} |
| } |
| if exec != "" { |
| p.Mapping[0].File = exec |
| } |
| if buildid != "" { |
| p.Mapping[0].BuildID = buildid |
| } |
| } |
| |
| if err := sym(*f.flagSymbolize, source, p, obj, ui); err != nil { |
| return nil, err |
| } |
| |
| // Save a copy of any remote profiles, unless the user is explicitly |
| // saving it. |
| if remote && !f.isFormat("proto") { |
| prefix := "pprof." |
| if len(p.Mapping) > 0 && p.Mapping[0].File != "" { |
| prefix = prefix + filepath.Base(p.Mapping[0].File) + "." |
| } |
| if !strings.ContainsRune(host, os.PathSeparator) { |
| prefix = prefix + host + "." |
| } |
| for _, s := range p.SampleType { |
| prefix = prefix + s.Type + "." |
| } |
| |
| dir := os.Getenv("PPROF_TMPDIR") |
| tempFile, err := tempfile.New(dir, prefix, ".pb.gz") |
| if err == nil { |
| if err = p.Write(tempFile); err == nil { |
| ui.PrintErr("Saved profile in ", tempFile.Name()) |
| } |
| } |
| if err != nil { |
| ui.PrintErr("Could not save profile: ", err) |
| } |
| } |
| |
| if err := p.Demangle(obj.Demangle); err != nil { |
| ui.PrintErr("Failed to demangle profile: ", err) |
| } |
| |
| if err := p.CheckValid(); err != nil { |
| return nil, fmt.Errorf("Grab %s: %v", source, err) |
| } |
| |
| return p, nil |
| } |
| |
| type flags struct { |
| flagInteractive *bool // Accept commands interactively |
| flagCommands map[string]*bool // pprof commands without parameters |
| flagParamCommands map[string]*string // pprof commands with parameters |
| |
| flagOutput *string // Output file name |
| |
| flagCum *bool // Sort by cumulative data |
| flagCallTree *bool // generate a context-sensitive call tree |
| |
| flagAddresses *bool // Report at address level |
| flagLines *bool // Report at source line level |
| flagFiles *bool // Report at file level |
| flagFunctions *bool // Report at function level [default] |
| |
| flagSymbolize *string // Symbolization options (=none to disable) |
| flagBuildID *string // Override build if for first mapping |
| |
| flagNodeCount *int // Max number of nodes to show |
| flagNodeFraction *float64 // Hide nodes below <f>*total |
| flagEdgeFraction *float64 // Hide edges below <f>*total |
| flagTrim *bool // Set to false to ignore NodeCount/*Fraction |
| flagRuntime *bool // Show runtime call frames in memory profiles |
| flagFocus *string // Restricts to paths going through a node matching regexp |
| flagIgnore *string // Skips paths going through any nodes matching regexp |
| flagHide *string // Skips sample locations matching regexp |
| flagTagFocus *string // Restrict to samples tagged with key:value matching regexp |
| flagTagIgnore *string // Discard samples tagged with key:value matching regexp |
| flagDropNegative *bool // Skip negative values |
| |
| flagBase *string // Source for base profile to user for comparison |
| |
| flagSeconds *int // Length of time for dynamic profiles |
| |
| flagTotalDelay *bool // Display total delay at each region |
| flagContentions *bool // Display number of delays at each region |
| flagMeanDelay *bool // Display mean delay at each region |
| |
| flagInUseSpace *bool // Display in-use memory size |
| flagInUseObjects *bool // Display in-use object counts |
| flagAllocSpace *bool // Display allocated memory size |
| flagAllocObjects *bool // Display allocated object counts |
| flagDisplayUnit *string // Measurement unit to use on reports |
| flagDivideBy *float64 // Ratio to divide sample values |
| |
| flagSampleIndex *int // Sample value to use in reports. |
| flagMean *bool // Use mean of sample_index over count |
| |
| flagTools *string |
| profileSource []string |
| profileExecName string |
| |
| extraUsage string |
| commands commands.Commands |
| } |
| |
| func (f *flags) isFormat(format string) bool { |
| if fl := f.flagCommands[format]; fl != nil { |
| return *fl |
| } |
| if fl := f.flagParamCommands[format]; fl != nil { |
| return *fl != "" |
| } |
| return false |
| } |
| |
| // String provides a printable representation for the current set of flags. |
| func (f *flags) String(p *profile.Profile) string { |
| var ret string |
| |
| if ix := *f.flagSampleIndex; ix != -1 { |
| ret += fmt.Sprintf(" %-25s : %d (%s)\n", "sample_index", ix, p.SampleType[ix].Type) |
| } |
| if ix := *f.flagMean; ix { |
| ret += boolFlagString("mean") |
| } |
| if *f.flagDisplayUnit != "minimum" { |
| ret += stringFlagString("unit", *f.flagDisplayUnit) |
| } |
| |
| switch { |
| case *f.flagInteractive: |
| ret += boolFlagString("interactive") |
| } |
| for name, fl := range f.flagCommands { |
| if *fl { |
| ret += boolFlagString(name) |
| } |
| } |
| |
| if *f.flagCum { |
| ret += boolFlagString("cum") |
| } |
| if *f.flagCallTree { |
| ret += boolFlagString("call_tree") |
| } |
| |
| switch { |
| case *f.flagAddresses: |
| ret += boolFlagString("addresses") |
| case *f.flagLines: |
| ret += boolFlagString("lines") |
| case *f.flagFiles: |
| ret += boolFlagString("files") |
| case *f.flagFunctions: |
| ret += boolFlagString("functions") |
| } |
| |
| if *f.flagNodeCount != -1 { |
| ret += intFlagString("nodecount", *f.flagNodeCount) |
| } |
| |
| ret += floatFlagString("nodefraction", *f.flagNodeFraction) |
| ret += floatFlagString("edgefraction", *f.flagEdgeFraction) |
| |
| if *f.flagFocus != "" { |
| ret += stringFlagString("focus", *f.flagFocus) |
| } |
| if *f.flagIgnore != "" { |
| ret += stringFlagString("ignore", *f.flagIgnore) |
| } |
| if *f.flagHide != "" { |
| ret += stringFlagString("hide", *f.flagHide) |
| } |
| |
| if *f.flagTagFocus != "" { |
| ret += stringFlagString("tagfocus", *f.flagTagFocus) |
| } |
| if *f.flagTagIgnore != "" { |
| ret += stringFlagString("tagignore", *f.flagTagIgnore) |
| } |
| |
| return ret |
| } |
| |
| func boolFlagString(label string) string { |
| return fmt.Sprintf(" %-25s : true\n", label) |
| } |
| |
| func stringFlagString(label, value string) string { |
| return fmt.Sprintf(" %-25s : %s\n", label, value) |
| } |
| |
| func intFlagString(label string, value int) string { |
| return fmt.Sprintf(" %-25s : %d\n", label, value) |
| } |
| |
| func floatFlagString(label string, value float64) string { |
| return fmt.Sprintf(" %-25s : %f\n", label, value) |
| } |
| |
| // Utility routines to set flag values. |
| func newBool(b bool) *bool { |
| return &b |
| } |
| |
| func newString(s string) *string { |
| return &s |
| } |
| |
| func newFloat64(fl float64) *float64 { |
| return &fl |
| } |
| |
| func newInt(i int) *int { |
| return &i |
| } |
| |
| func (f *flags) usage(ui plugin.UI) { |
| var commandMsg []string |
| for name, cmd := range f.commands { |
| if cmd.HasParam { |
| name = name + "=p" |
| } |
| commandMsg = append(commandMsg, |
| fmt.Sprintf(" -%-16s %s", name, cmd.Usage)) |
| } |
| |
| sort.Strings(commandMsg) |
| |
| text := usageMsgHdr + strings.Join(commandMsg, "\n") + "\n" + usageMsg + "\n" |
| if f.extraUsage != "" { |
| text += f.extraUsage + "\n" |
| } |
| text += usageMsgVars |
| ui.Print(text) |
| } |
| |
| func getFlags(flag plugin.FlagSet, overrides commands.Commands, ui plugin.UI) (*flags, error) { |
| f := &flags{ |
| flagInteractive: flag.Bool("interactive", false, "Accepts commands interactively"), |
| flagCommands: make(map[string]*bool), |
| flagParamCommands: make(map[string]*string), |
| |
| // Filename for file-based output formats, stdout by default. |
| flagOutput: flag.String("output", "", "Output filename for file-based outputs "), |
| // Comparisons. |
| flagBase: flag.String("base", "", "Source for base profile for comparison"), |
| flagDropNegative: flag.Bool("drop_negative", false, "Ignore negative differences"), |
| |
| // Data sorting criteria. |
| flagCum: flag.Bool("cum", false, "Sort by cumulative data"), |
| // Graph handling options. |
| flagCallTree: flag.Bool("call_tree", false, "Create a context-sensitive call tree"), |
| // Granularity of output resolution. |
| flagAddresses: flag.Bool("addresses", false, "Report at address level"), |
| flagLines: flag.Bool("lines", false, "Report at source line level"), |
| flagFiles: flag.Bool("files", false, "Report at source file level"), |
| flagFunctions: flag.Bool("functions", false, "Report at function level [default]"), |
| // Internal options. |
| flagSymbolize: flag.String("symbolize", "", "Options for profile symbolization"), |
| flagBuildID: flag.String("buildid", "", "Override build id for first mapping"), |
| // Filtering options |
| flagNodeCount: flag.Int("nodecount", -1, "Max number of nodes to show"), |
| flagNodeFraction: flag.Float64("nodefraction", 0.005, "Hide nodes below <f>*total"), |
| flagEdgeFraction: flag.Float64("edgefraction", 0.001, "Hide edges below <f>*total"), |
| flagTrim: flag.Bool("trim", true, "Honor nodefraction/edgefraction/nodecount defaults"), |
| flagRuntime: flag.Bool("runtime", false, "Show runtime call frames in memory profiles"), |
| flagFocus: flag.String("focus", "", "Restricts to paths going through a node matching regexp"), |
| flagIgnore: flag.String("ignore", "", "Skips paths going through any nodes matching regexp"), |
| flagHide: flag.String("hide", "", "Skips nodes matching regexp"), |
| flagTagFocus: flag.String("tagfocus", "", "Restrict to samples with tags in range or matched by regexp"), |
| flagTagIgnore: flag.String("tagignore", "", "Discard samples with tags in range or matched by regexp"), |
| // CPU profile options |
| flagSeconds: flag.Int("seconds", -1, "Length of time for dynamic profiles"), |
| // Heap profile options |
| flagInUseSpace: flag.Bool("inuse_space", false, "Display in-use memory size"), |
| flagInUseObjects: flag.Bool("inuse_objects", false, "Display in-use object counts"), |
| flagAllocSpace: flag.Bool("alloc_space", false, "Display allocated memory size"), |
| flagAllocObjects: flag.Bool("alloc_objects", false, "Display allocated object counts"), |
| flagDisplayUnit: flag.String("unit", "minimum", "Measurement units to display"), |
| flagDivideBy: flag.Float64("divide_by", 1.0, "Ratio to divide all samples before visualization"), |
| flagSampleIndex: flag.Int("sample_index", -1, "Index of sample value to report"), |
| flagMean: flag.Bool("mean", false, "Average sample value over first value (count)"), |
| // Contention profile options |
| flagTotalDelay: flag.Bool("total_delay", false, "Display total delay at each region"), |
| flagContentions: flag.Bool("contentions", false, "Display number of delays at each region"), |
| flagMeanDelay: flag.Bool("mean_delay", false, "Display mean delay at each region"), |
| flagTools: flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames"), |
| extraUsage: flag.ExtraUsage(), |
| } |
| |
| // Flags used during command processing |
| interactive := &f.flagInteractive |
| f.commands = commands.PProf(functionCompleter, interactive) |
| |
| // Override commands |
| for name, cmd := range overrides { |
| f.commands[name] = cmd |
| } |
| |
| for name, cmd := range f.commands { |
| if cmd.HasParam { |
| f.flagParamCommands[name] = flag.String(name, "", "Generate a report in "+name+" format, matching regexp") |
| } else { |
| f.flagCommands[name] = flag.Bool(name, false, "Generate a report in "+name+" format") |
| } |
| } |
| |
| args := flag.Parse(func() { f.usage(ui) }) |
| if len(args) == 0 { |
| return nil, fmt.Errorf("no profile source specified") |
| } |
| |
| f.profileSource = args |
| |
| // Instruct legacy heapz parsers to grab historical allocation data, |
| // instead of the default in-use data. Not available with tcmalloc. |
| if *f.flagAllocSpace || *f.flagAllocObjects { |
| profile.LegacyHeapAllocated = true |
| } |
| |
| if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir == "" { |
| profileDir = os.Getenv("HOME") + "/pprof" |
| os.Setenv("PPROF_TMPDIR", profileDir) |
| if err := os.MkdirAll(profileDir, 0755); err != nil { |
| return nil, fmt.Errorf("failed to access temp dir %s: %v", profileDir, err) |
| } |
| } |
| |
| return f, nil |
| } |
| |
| func processFlags(p *profile.Profile, ui plugin.UI, f *flags) error { |
| flagDis := f.isFormat("disasm") |
| flagPeek := f.isFormat("peek") |
| flagWebList := f.isFormat("weblist") |
| flagList := f.isFormat("list") |
| flagCallgrind := f.isFormat("callgrind") |
| |
| if flagDis || flagWebList || flagCallgrind { |
| // Collect all samples at address granularity for assembly |
| // listing. |
| f.flagNodeCount = newInt(0) |
| f.flagAddresses = newBool(true) |
| f.flagLines = newBool(false) |
| f.flagFiles = newBool(false) |
| f.flagFunctions = newBool(false) |
| } |
| |
| if flagPeek { |
| // Collect all samples at function granularity for peek command |
| f.flagNodeCount = newInt(0) |
| f.flagAddresses = newBool(false) |
| f.flagLines = newBool(false) |
| f.flagFiles = newBool(false) |
| f.flagFunctions = newBool(true) |
| } |
| |
| if flagList { |
| // Collect all samples at fileline granularity for source |
| // listing. |
| f.flagNodeCount = newInt(0) |
| f.flagAddresses = newBool(false) |
| f.flagLines = newBool(true) |
| f.flagFiles = newBool(false) |
| f.flagFunctions = newBool(false) |
| } |
| |
| if !*f.flagTrim { |
| f.flagNodeCount = newInt(0) |
| f.flagNodeFraction = newFloat64(0) |
| f.flagEdgeFraction = newFloat64(0) |
| } |
| |
| if oc := countFlagMap(f.flagCommands, f.flagParamCommands); oc == 0 { |
| f.flagInteractive = newBool(true) |
| } else if oc > 1 { |
| f.usage(ui) |
| return fmt.Errorf("must set at most one output format") |
| } |
| |
| // Apply nodecount defaults for non-interactive mode. The |
| // interactive shell will apply defaults for the interactive mode. |
| if *f.flagNodeCount < 0 && !*f.flagInteractive { |
| switch { |
| default: |
| f.flagNodeCount = newInt(80) |
| case f.isFormat("text"): |
| f.flagNodeCount = newInt(0) |
| } |
| } |
| |
| // Apply legacy options and diagnose conflicts. |
| if rc := countFlags([]*bool{f.flagAddresses, f.flagLines, f.flagFiles, f.flagFunctions}); rc == 0 { |
| f.flagFunctions = newBool(true) |
| } else if rc > 1 { |
| f.usage(ui) |
| return fmt.Errorf("must set at most one granularity option") |
| } |
| |
| var err error |
| si, sm := *f.flagSampleIndex, *f.flagMean || *f.flagMeanDelay |
| si, err = sampleIndex(p, &f.flagTotalDelay, si, "delay", "-total_delay", err) |
| si, err = sampleIndex(p, &f.flagMeanDelay, si, "delay", "-mean_delay", err) |
| si, err = sampleIndex(p, &f.flagContentions, si, "contentions", "-contentions", err) |
| |
| si, err = sampleIndex(p, &f.flagInUseSpace, si, "inuse_space", "-inuse_space", err) |
| si, err = sampleIndex(p, &f.flagInUseObjects, si, "inuse_objects", "-inuse_objects", err) |
| si, err = sampleIndex(p, &f.flagAllocSpace, si, "alloc_space", "-alloc_space", err) |
| si, err = sampleIndex(p, &f.flagAllocObjects, si, "alloc_objects", "-alloc_objects", err) |
| |
| if si == -1 { |
| // Use last value if none is requested. |
| si = len(p.SampleType) - 1 |
| } else if si < 0 || si >= len(p.SampleType) { |
| err = fmt.Errorf("sample_index value %d out of range [0..%d]", si, len(p.SampleType)-1) |
| } |
| |
| if err != nil { |
| f.usage(ui) |
| return err |
| } |
| f.flagSampleIndex, f.flagMean = newInt(si), newBool(sm) |
| return nil |
| } |
| |
| func sampleIndex(p *profile.Profile, flag **bool, |
| sampleIndex int, |
| sampleType, option string, |
| err error) (int, error) { |
| if err != nil || !**flag { |
| return sampleIndex, err |
| } |
| *flag = newBool(false) |
| if sampleIndex != -1 { |
| return 0, fmt.Errorf("set at most one sample value selection option") |
| } |
| for index, s := range p.SampleType { |
| if sampleType == s.Type { |
| return index, nil |
| } |
| } |
| return 0, fmt.Errorf("option %s not valid for this profile", option) |
| } |
| |
| func countFlags(bs []*bool) int { |
| var c int |
| for _, b := range bs { |
| if *b { |
| c++ |
| } |
| } |
| return c |
| } |
| |
| func countFlagMap(bms map[string]*bool, bmrxs map[string]*string) int { |
| var c int |
| for _, b := range bms { |
| if *b { |
| c++ |
| } |
| } |
| for _, s := range bmrxs { |
| if *s != "" { |
| c++ |
| } |
| } |
| return c |
| } |
| |
| var usageMsgHdr = "usage: pprof [options] [binary] <profile source> ...\n" + |
| "Output format (only set one):\n" |
| |
| var usageMsg = "Output file parameters (for file-based output formats):\n" + |
| " -output=f Generate output on file f (stdout by default)\n" + |
| "Output granularity (only set one):\n" + |
| " -functions Report at function level [default]\n" + |
| " -files Report at source file level\n" + |
| " -lines Report at source line level\n" + |
| " -addresses Report at address level\n" + |
| "Comparison options:\n" + |
| " -base <profile> Show delta from this profile\n" + |
| " -drop_negative Ignore negative differences\n" + |
| "Sorting options:\n" + |
| " -cum Sort by cumulative data\n\n" + |
| "Dynamic profile options:\n" + |
| " -seconds=N Length of time for dynamic profiles\n" + |
| "Profile trimming options:\n" + |
| " -nodecount=N Max number of nodes to show\n" + |
| " -nodefraction=f Hide nodes below <f>*total\n" + |
| " -edgefraction=f Hide edges below <f>*total\n" + |
| "Sample value selection option (by index):\n" + |
| " -sample_index Index of sample value to display\n" + |
| " -mean Average sample value over first value\n" + |
| "Sample value selection option (for heap profiles):\n" + |
| " -inuse_space Display in-use memory size\n" + |
| " -inuse_objects Display in-use object counts\n" + |
| " -alloc_space Display allocated memory size\n" + |
| " -alloc_objects Display allocated object counts\n" + |
| "Sample value selection option (for contention profiles):\n" + |
| " -total_delay Display total delay at each region\n" + |
| " -contentions Display number of delays at each region\n" + |
| " -mean_delay Display mean delay at each region\n" + |
| "Filtering options:\n" + |
| " -runtime Show runtime call frames in memory profiles\n" + |
| " -focus=r Restricts to paths going through a node matching regexp\n" + |
| " -ignore=r Skips paths going through any nodes matching regexp\n" + |
| " -tagfocus=r Restrict to samples tagged with key:value matching regexp\n" + |
| " Restrict to samples with numeric tags in range (eg \"32kb:1mb\")\n" + |
| " -tagignore=r Discard samples tagged with key:value matching regexp\n" + |
| " Avoid samples with numeric tags in range (eg \"1mb:\")\n" + |
| "Miscellaneous:\n" + |
| " -call_tree Generate a context-sensitive call tree\n" + |
| " -unit=u Convert all samples to unit u for display\n" + |
| " -divide_by=f Scale all samples by dividing them by f\n" + |
| " -buildid=id Override build id for main binary in profile\n" + |
| " -tools=path Search path for object-level tools\n" + |
| " -help This message" |
| |
| var usageMsgVars = "Environment Variables:\n" + |
| " PPROF_TMPDIR Location for saved profiles (default $HOME/pprof)\n" + |
| " PPROF_TOOLS Search path for object-level tools\n" + |
| " PPROF_BINARY_PATH Search path for local binary files\n" + |
| " default: $HOME/pprof/binaries\n" + |
| " finds binaries by $name and $buildid/$name" |
| |
| func aggregate(prof *profile.Profile, f *flags) error { |
| switch { |
| case f.isFormat("proto"), f.isFormat("raw"): |
| // No aggregation for raw profiles. |
| case *f.flagLines: |
| return prof.Aggregate(true, true, true, true, false) |
| case *f.flagFiles: |
| return prof.Aggregate(true, false, true, false, false) |
| case *f.flagFunctions: |
| return prof.Aggregate(true, true, false, false, false) |
| case f.isFormat("weblist"), f.isFormat("disasm"), f.isFormat("callgrind"): |
| return prof.Aggregate(false, true, true, true, true) |
| } |
| return nil |
| } |
| |
| // parseOptions parses the options into report.Options |
| // Returns a function to postprocess the report after generation. |
| func parseOptions(f *flags) (o *report.Options, p commands.PostProcessor, err error) { |
| |
| if *f.flagDivideBy == 0 { |
| return nil, nil, fmt.Errorf("zero divisor specified") |
| } |
| |
| o = &report.Options{ |
| CumSort: *f.flagCum, |
| CallTree: *f.flagCallTree, |
| PrintAddresses: *f.flagAddresses, |
| DropNegative: *f.flagDropNegative, |
| Ratio: 1 / *f.flagDivideBy, |
| |
| NodeCount: *f.flagNodeCount, |
| NodeFraction: *f.flagNodeFraction, |
| EdgeFraction: *f.flagEdgeFraction, |
| OutputUnit: *f.flagDisplayUnit, |
| } |
| |
| for cmd, b := range f.flagCommands { |
| if *b { |
| pcmd := f.commands[cmd] |
| o.OutputFormat = pcmd.Format |
| return o, pcmd.PostProcess, nil |
| } |
| } |
| |
| for cmd, rx := range f.flagParamCommands { |
| if *rx != "" { |
| pcmd := f.commands[cmd] |
| if o.Symbol, err = regexp.Compile(*rx); err != nil { |
| return nil, nil, fmt.Errorf("parsing -%s regexp: %v", cmd, err) |
| } |
| o.OutputFormat = pcmd.Format |
| return o, pcmd.PostProcess, nil |
| } |
| } |
| |
| return nil, nil, fmt.Errorf("no output format selected") |
| } |
| |
| type sampleValueFunc func(*profile.Sample) int64 |
| |
| // sampleFormat returns a function to extract values out of a profile.Sample, |
| // and the type/units of those values. |
| func sampleFormat(p *profile.Profile, f *flags) (sampleValueFunc, string, string) { |
| valueIndex := *f.flagSampleIndex |
| |
| if *f.flagMean { |
| return meanExtractor(valueIndex), "mean_" + p.SampleType[valueIndex].Type, p.SampleType[valueIndex].Unit |
| } |
| |
| return valueExtractor(valueIndex), p.SampleType[valueIndex].Type, p.SampleType[valueIndex].Unit |
| } |
| |
| func valueExtractor(ix int) sampleValueFunc { |
| return func(s *profile.Sample) int64 { |
| return s.Value[ix] |
| } |
| } |
| |
| func meanExtractor(ix int) sampleValueFunc { |
| return func(s *profile.Sample) int64 { |
| if s.Value[0] == 0 { |
| return 0 |
| } |
| return s.Value[ix] / s.Value[0] |
| } |
| } |
| |
| func generate(interactive bool, prof *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error { |
| o, postProcess, err := parseOptions(f) |
| if err != nil { |
| return err |
| } |
| |
| var w io.Writer |
| if *f.flagOutput == "" { |
| w = os.Stdout |
| } else { |
| ui.PrintErr("Generating report in ", *f.flagOutput) |
| outputFile, err := os.Create(*f.flagOutput) |
| if err != nil { |
| return err |
| } |
| defer outputFile.Close() |
| w = outputFile |
| } |
| |
| if prof.Empty() { |
| return fmt.Errorf("profile is empty") |
| } |
| |
| value, stype, unit := sampleFormat(prof, f) |
| o.SampleType = stype |
| rpt := report.New(prof, *o, value, unit) |
| |
| // Do not apply filters if we're just generating a proto, so we |
| // still have all the data. |
| if o.OutputFormat != report.Proto { |
| // Delay applying focus/ignore until after creating the report so |
| // the report reflects the total number of samples. |
| if err := preprocess(prof, ui, f); err != nil { |
| return err |
| } |
| } |
| |
| if postProcess == nil { |
| return report.Generate(w, rpt, obj) |
| } |
| |
| var dot bytes.Buffer |
| if err = report.Generate(&dot, rpt, obj); err != nil { |
| return err |
| } |
| |
| return postProcess(&dot, w, ui) |
| } |