summaryrefslogtreecommitdiff
path: root/src/cmd/pprof/internal/driver/interactive.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/pprof/internal/driver/interactive.go')
-rw-r--r--src/cmd/pprof/internal/driver/interactive.go492
1 files changed, 492 insertions, 0 deletions
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))
+ }
+}