diff options
author | Tianon Gravi <admwiggin@gmail.com> | 2015-01-15 11:54:00 -0700 |
---|---|---|
committer | Tianon Gravi <admwiggin@gmail.com> | 2015-01-15 11:54:00 -0700 |
commit | f154da9e12608589e8d5f0508f908a0c3e88a1bb (patch) | |
tree | f8255d51e10c6f1e0ed69702200b966c9556a431 /src/cmd/pprof/internal/driver | |
parent | 8d8329ed5dfb9622c82a9fbec6fd99a580f9c9f6 (diff) | |
download | golang-upstream/1.4.tar.gz |
Imported Upstream version 1.4upstream/1.4
Diffstat (limited to 'src/cmd/pprof/internal/driver')
-rw-r--r-- | src/cmd/pprof/internal/driver/driver.go | 1036 | ||||
-rw-r--r-- | src/cmd/pprof/internal/driver/interactive.go | 492 |
2 files changed, 1528 insertions, 0 deletions
diff --git a/src/cmd/pprof/internal/driver/driver.go b/src/cmd/pprof/internal/driver/driver.go new file mode 100644 index 000000000..68b5d1b61 --- /dev/null +++ b/src/cmd/pprof/internal/driver/driver.go @@ -0,0 +1,1036 @@ +// 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/profile" + "cmd/pprof/internal/report" + "cmd/pprof/internal/tempfile" +) + +// 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 + } + + 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, url.Host, time.Duration(30) * time.Second + } + } + 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 = "/profilez" + case "/protoz": + // Rewrite to /profilez?type=proto + url.Path = "/profilez" + 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 + + flagSVGPan *string // URL to fetch the SVG Pan library + 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 + 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"), + + flagSVGPan: flag.String("svgpan", "https://www.cyberz.org/projects/SVGPan/SVGPan.js", "URL for SVGPan Library"), + // 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"), + 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 + svgpan := &f.flagSVGPan + f.commands = commands.PProf(functionCompleter, interactive, svgpan) + + // 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") + + if flagDis || flagWebList { + // 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, 1, "delay", "-total_delay", err) + si, err = sampleIndex(p, &f.flagMeanDelay, si, 1, "delay", "-mean_delay", err) + si, err = sampleIndex(p, &f.flagContentions, si, 0, "contentions", "-contentions", err) + + si, err = sampleIndex(p, &f.flagInUseSpace, si, 1, "inuse_space", "-inuse_space", err) + si, err = sampleIndex(p, &f.flagInUseObjects, si, 0, "inuse_objects", "-inuse_objects", err) + si, err = sampleIndex(p, &f.flagAllocSpace, si, 1, "alloc_space", "-alloc_space", err) + si, err = sampleIndex(p, &f.flagAllocObjects, si, 0, "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, + newSampleIndex 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") + } + if newSampleIndex >= len(p.SampleType) || + p.SampleType[newSampleIndex].Type != sampleType { + return 0, fmt.Errorf("option %s not valid for this profile", option) + } + return newSampleIndex, nil +} + +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" + + " -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" + + " -show_bytes Display all space in bytes\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 temporary files (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.isFormat("callgrind"): + // Aggregate to file/line for callgrind. + fallthrough + 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"): + 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 + } + + 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) +} diff --git a/src/cmd/pprof/internal/driver/interactive.go b/src/cmd/pprof/internal/driver/interactive.go new file mode 100644 index 000000000..13009bf7e --- /dev/null +++ b/src/cmd/pprof/internal/driver/interactive.go @@ -0,0 +1,492 @@ +// 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 + +import ( + "fmt" + "io" + "regexp" + "sort" + "strconv" + "strings" + + "cmd/pprof/internal/commands" + "cmd/pprof/internal/plugin" + "cmd/pprof/internal/profile" +) + +var profileFunctionNames = []string{} + +// functionCompleter replaces provided substring with a function +// name retrieved from a profile if a single match exists. Otherwise, +// it returns unchanged substring. It defaults to no-op if the profile +// is not specified. +func functionCompleter(substring string) string { + found := "" + for _, fName := range profileFunctionNames { + if strings.Contains(fName, substring) { + if found != "" { + return substring + } + found = fName + } + } + if found != "" { + return found + } + return substring +} + +// updateAutoComplete enhances autocompletion with information that can be +// retrieved from the profile +func updateAutoComplete(p *profile.Profile) { + profileFunctionNames = nil // remove function names retrieved previously + for _, fn := range p.Function { + profileFunctionNames = append(profileFunctionNames, fn.Name) + } +} + +// splitCommand splits the command line input into tokens separated by +// spaces. Takes care to separate commands of the form 'top10' into +// two tokens: 'top' and '10' +func splitCommand(input string) []string { + fields := strings.Fields(input) + if num := strings.IndexAny(fields[0], "0123456789"); num != -1 { + inputNumber := fields[0][num:] + fields[0] = fields[0][:num] + fields = append([]string{fields[0], inputNumber}, fields[1:]...) + } + return fields +} + +// interactive displays a prompt and reads commands for profile +// manipulation/visualization. +func interactive(p *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error { + updateAutoComplete(p) + + // Enter command processing loop. + ui.Print("Entering interactive mode (type \"help\" for commands)") + ui.SetAutoComplete(commands.NewCompleter(f.commands)) + + for { + input, err := readCommand(p, ui, f) + if err != nil { + if err != io.EOF { + return err + } + if input == "" { + return nil + } + } + // Process simple commands. + switch input { + case "": + continue + case ":": + f.flagFocus = newString("") + f.flagIgnore = newString("") + f.flagTagFocus = newString("") + f.flagTagIgnore = newString("") + f.flagHide = newString("") + continue + } + + fields := splitCommand(input) + // Process report generation commands. + if _, ok := f.commands[fields[0]]; ok { + if err := generateReport(p, fields, obj, ui, f); err != nil { + if err == io.EOF { + return nil + } + ui.PrintErr(err) + } + continue + } + + switch cmd := fields[0]; cmd { + case "help": + commandHelp(fields, ui, f) + continue + case "exit", "quit": + return nil + } + + // Process option settings. + if of, err := optFlags(p, input, f); err == nil { + f = of + } else { + ui.PrintErr("Error: ", err.Error()) + } + } +} + +func generateReport(p *profile.Profile, cmd []string, obj plugin.ObjTool, ui plugin.UI, f *flags) error { + prof := p.Copy() + + cf, err := cmdFlags(prof, cmd, ui, f) + if err != nil { + return err + } + + return generate(true, prof, obj, ui, cf) +} + +// validateRegex checks if a string is a valid regular expression. +func validateRegex(v string) error { + _, err := regexp.Compile(v) + return err +} + +// readCommand prompts for and reads the next command. +func readCommand(p *profile.Profile, ui plugin.UI, f *flags) (string, error) { + //ui.Print("Options:\n", f.String(p)) + s, err := ui.ReadLine() + return strings.TrimSpace(s), err +} + +func commandHelp(_ []string, ui plugin.UI, f *flags) error { + help := ` + Commands: + cmd [n] [--cum] [focus_regex]* [-ignore_regex]* + Produce a text report with the top n entries. + Include samples matching focus_regex, and exclude ignore_regex. + Add --cum to sort using cumulative data. + Available commands: +` + var commands []string + for name, cmd := range f.commands { + commands = append(commands, fmt.Sprintf(" %-12s %s", name, cmd.Usage)) + } + sort.Strings(commands) + + help = help + strings.Join(commands, "\n") + ` + peek func_regex + Display callers and callees of functions matching func_regex. + + dot [n] [focus_regex]* [-ignore_regex]* [>file] + Produce an annotated callgraph with the top n entries. + Include samples matching focus_regex, and exclude ignore_regex. + For other outputs, replace dot with: + - Graphic formats: dot, svg, pdf, ps, gif, png (use > to name output file) + - Graph viewer: gv, web, evince, eog + + callgrind [n] [focus_regex]* [-ignore_regex]* [>file] + Produce a file in callgrind-compatible format. + Include samples matching focus_regex, and exclude ignore_regex. + + weblist func_regex [-ignore_regex]* + Show annotated source with interspersed assembly in a web browser. + + list func_regex [-ignore_regex]* + Print source for routines matching func_regex, and exclude ignore_regex. + + disasm func_regex [-ignore_regex]* + Disassemble routines matching func_regex, and exclude ignore_regex. + + tags tag_regex [-ignore_regex]* + List tags with key:value matching tag_regex and exclude ignore_regex. + + quit/exit/^D + Exit pprof. + + option=value + The following options can be set individually: + cum/flat: Sort entries based on cumulative or flat data + call_tree: Build context-sensitive call trees + nodecount: Max number of entries to display + nodefraction: Min frequency ratio of nodes to display + edgefraction: Min frequency ratio of edges to display + focus/ignore: Regexp to include/exclude samples by name/file + tagfocus/tagignore: Regexp or value range to filter samples by tag + eg "1mb", "1mb:2mb", ":64kb" + + functions: Level of aggregation for sample data + files: + lines: + addresses: + + unit: Measurement unit to use on reports + + Sample value selection by index: + sample_index: Index of sample value to display + mean: Average sample value over first value + + Sample value selection by name: + alloc_space for heap profiles + alloc_objects + inuse_space + inuse_objects + + total_delay for contention profiles + mean_delay + contentions + + : Clear focus/ignore/hide/tagfocus/tagignore` + + ui.Print(help) + return nil +} + +// cmdFlags parses the options of an interactive command and returns +// an updated flags object. +func cmdFlags(prof *profile.Profile, input []string, ui plugin.UI, f *flags) (*flags, error) { + cf := *f + + var focus, ignore string + output := *cf.flagOutput + nodeCount := *cf.flagNodeCount + cmd := input[0] + + // Update output flags based on parameters. + tokens := input[1:] + for p := 0; p < len(tokens); p++ { + t := tokens[p] + if t == "" { + continue + } + if c, err := strconv.ParseInt(t, 10, 32); err == nil { + nodeCount = int(c) + continue + } + switch t[0] { + case '>': + if len(t) > 1 { + output = t[1:] + continue + } + // find next token + for p++; p < len(tokens); p++ { + if tokens[p] != "" { + output = tokens[p] + break + } + } + case '-': + if t == "--cum" || t == "-cum" { + cf.flagCum = newBool(true) + continue + } + ignore = catRegex(ignore, t[1:]) + default: + focus = catRegex(focus, t) + } + } + + pcmd, ok := f.commands[cmd] + if !ok { + return nil, fmt.Errorf("Unexpected parse failure: %v", input) + } + // Reset flags + cf.flagCommands = make(map[string]*bool) + cf.flagParamCommands = make(map[string]*string) + + if !pcmd.HasParam { + cf.flagCommands[cmd] = newBool(true) + + switch cmd { + case "tags": + cf.flagTagFocus = newString(focus) + cf.flagTagIgnore = newString(ignore) + default: + cf.flagFocus = newString(catRegex(*cf.flagFocus, focus)) + cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore)) + } + } else { + if focus == "" { + focus = "." + } + cf.flagParamCommands[cmd] = newString(focus) + cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore)) + } + + if nodeCount < 0 { + switch cmd { + case "text", "top": + // Default text/top to 10 nodes on interactive mode + nodeCount = 10 + default: + nodeCount = 80 + } + } + + cf.flagNodeCount = newInt(nodeCount) + cf.flagOutput = newString(output) + + // Do regular flags processing + if err := processFlags(prof, ui, &cf); err != nil { + cf.usage(ui) + return nil, err + } + + return &cf, nil +} + +func catRegex(a, b string) string { + if a == "" { + return b + } + if b == "" { + return a + } + return a + "|" + b +} + +// optFlags parses an interactive option setting and returns +// an updated flags object. +func optFlags(p *profile.Profile, input string, f *flags) (*flags, error) { + inputs := strings.SplitN(input, "=", 2) + option := strings.ToLower(strings.TrimSpace(inputs[0])) + var value string + if len(inputs) == 2 { + value = strings.TrimSpace(inputs[1]) + } + + of := *f + + var err error + var bv bool + var uv uint64 + var fv float64 + + switch option { + case "cum": + if bv, err = parseBool(value); err != nil { + return nil, err + } + of.flagCum = newBool(bv) + case "flat": + if bv, err = parseBool(value); err != nil { + return nil, err + } + of.flagCum = newBool(!bv) + case "call_tree": + if bv, err = parseBool(value); err != nil { + return nil, err + } + of.flagCallTree = newBool(bv) + case "unit": + of.flagDisplayUnit = newString(value) + case "sample_index": + if uv, err = strconv.ParseUint(value, 10, 32); err != nil { + return nil, err + } + if ix := int(uv); ix < 0 || ix >= len(p.SampleType) { + return nil, fmt.Errorf("sample_index out of range [0..%d]", len(p.SampleType)-1) + } + of.flagSampleIndex = newInt(int(uv)) + case "mean": + if bv, err = parseBool(value); err != nil { + return nil, err + } + of.flagMean = newBool(bv) + case "nodecount": + if uv, err = strconv.ParseUint(value, 10, 32); err != nil { + return nil, err + } + of.flagNodeCount = newInt(int(uv)) + case "nodefraction": + if fv, err = strconv.ParseFloat(value, 64); err != nil { + return nil, err + } + of.flagNodeFraction = newFloat64(fv) + case "edgefraction": + if fv, err = strconv.ParseFloat(value, 64); err != nil { + return nil, err + } + of.flagEdgeFraction = newFloat64(fv) + case "focus": + if err = validateRegex(value); err != nil { + return nil, err + } + of.flagFocus = newString(value) + case "ignore": + if err = validateRegex(value); err != nil { + return nil, err + } + of.flagIgnore = newString(value) + case "tagfocus": + if err = validateRegex(value); err != nil { + return nil, err + } + of.flagTagFocus = newString(value) + case "tagignore": + if err = validateRegex(value); err != nil { + return nil, err + } + of.flagTagIgnore = newString(value) + case "hide": + if err = validateRegex(value); err != nil { + return nil, err + } + of.flagHide = newString(value) + case "addresses", "files", "lines", "functions": + if bv, err = parseBool(value); err != nil { + return nil, err + } + if !bv { + return nil, fmt.Errorf("select one of addresses/files/lines/functions") + } + setGranularityToggle(option, &of) + default: + if ix := findSampleIndex(p, "", option); ix >= 0 { + of.flagSampleIndex = newInt(ix) + } else if ix := findSampleIndex(p, "total_", option); ix >= 0 { + of.flagSampleIndex = newInt(ix) + of.flagMean = newBool(false) + } else if ix := findSampleIndex(p, "mean_", option); ix >= 1 { + of.flagSampleIndex = newInt(ix) + of.flagMean = newBool(true) + } else { + return nil, fmt.Errorf("unrecognized command: %s", input) + } + } + return &of, nil +} + +// parseBool parses a string as a boolean value. +func parseBool(v string) (bool, error) { + switch strings.ToLower(v) { + case "true", "t", "yes", "y", "1", "": + return true, nil + case "false", "f", "no", "n", "0": + return false, nil + } + return false, fmt.Errorf(`illegal input "%s" for bool value`, v) +} + +func findSampleIndex(p *profile.Profile, prefix, sampleType string) int { + if !strings.HasPrefix(sampleType, prefix) { + return -1 + } + sampleType = strings.TrimPrefix(sampleType, prefix) + for i, r := range p.SampleType { + if r.Type == sampleType { + return i + } + } + return -1 +} + +// setGranularityToggle manages the set of granularity options. These +// operate as a toggle; turning one on turns the others off. +func setGranularityToggle(o string, fl *flags) { + t, f := newBool(true), newBool(false) + fl.flagFunctions = f + fl.flagFiles = f + fl.flagLines = f + fl.flagAddresses = f + switch o { + case "functions": + fl.flagFunctions = t + case "files": + fl.flagFiles = t + case "lines": + fl.flagLines = t + case "addresses": + fl.flagAddresses = t + default: + panic(fmt.Errorf("unexpected option %s", o)) + } +} |