Source file src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go

     1  // Copyright 2014 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package driver
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"regexp"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/google/pprof/internal/plugin"
    26  	"github.com/google/pprof/internal/report"
    27  	"github.com/google/pprof/profile"
    28  )
    29  
    30  var commentStart = "//:" // Sentinel for comments on options
    31  var tailDigitsRE = regexp.MustCompile("[0-9]+$")
    32  
    33  // interactive starts a shell to read pprof commands.
    34  func interactive(p *profile.Profile, o *plugin.Options) error {
    35  	// Enter command processing loop.
    36  	o.UI.SetAutoComplete(newCompleter(functionNames(p)))
    37  	configure("compact_labels", "true")
    38  	configHelp["sample_index"] += fmt.Sprintf("Or use sample_index=name, with name in %v.\n", sampleTypes(p))
    39  
    40  	// Do not wait for the visualizer to complete, to allow multiple
    41  	// graphs to be visualized simultaneously.
    42  	interactiveMode = true
    43  	shortcuts := profileShortcuts(p)
    44  
    45  	copier := makeProfileCopier(p)
    46  	greetings(p, o.UI)
    47  	for {
    48  		input, err := o.UI.ReadLine("(pprof) ")
    49  		if err != nil {
    50  			if err != io.EOF {
    51  				return err
    52  			}
    53  			if input == "" {
    54  				return nil
    55  			}
    56  		}
    57  
    58  		for _, input := range shortcuts.expand(input) {
    59  			// Process assignments of the form variable=value
    60  			if s := strings.SplitN(input, "=", 2); len(s) > 0 {
    61  				name := strings.TrimSpace(s[0])
    62  				var value string
    63  				if len(s) == 2 {
    64  					value = s[1]
    65  					if comment := strings.LastIndex(value, commentStart); comment != -1 {
    66  						value = value[:comment]
    67  					}
    68  					value = strings.TrimSpace(value)
    69  				}
    70  				if isConfigurable(name) {
    71  					// All non-bool options require inputs
    72  					if len(s) == 1 && !isBoolConfig(name) {
    73  						o.UI.PrintErr(fmt.Errorf("please specify a value, e.g. %s=<val>", name))
    74  						continue
    75  					}
    76  					if name == "sample_index" {
    77  						// Error check sample_index=xxx to ensure xxx is a valid sample type.
    78  						index, err := p.SampleIndexByName(value)
    79  						if err != nil {
    80  							o.UI.PrintErr(err)
    81  							continue
    82  						}
    83  						if index < 0 || index >= len(p.SampleType) {
    84  							o.UI.PrintErr(fmt.Errorf("invalid sample_index %q", value))
    85  							continue
    86  						}
    87  						value = p.SampleType[index].Type
    88  					}
    89  					if err := configure(name, value); err != nil {
    90  						o.UI.PrintErr(err)
    91  					}
    92  					continue
    93  				}
    94  			}
    95  
    96  			tokens := strings.Fields(input)
    97  			if len(tokens) == 0 {
    98  				continue
    99  			}
   100  
   101  			switch tokens[0] {
   102  			case "o", "options":
   103  				printCurrentOptions(p, o.UI)
   104  				continue
   105  			case "exit", "quit", "q":
   106  				return nil
   107  			case "help":
   108  				commandHelp(strings.Join(tokens[1:], " "), o.UI)
   109  				continue
   110  			}
   111  
   112  			args, cfg, err := parseCommandLine(tokens)
   113  			if err == nil {
   114  				err = generateReportWrapper(copier.newCopy(), args, cfg, o)
   115  			}
   116  
   117  			if err != nil {
   118  				o.UI.PrintErr(err)
   119  			}
   120  		}
   121  	}
   122  }
   123  
   124  var generateReportWrapper = generateReport // For testing purposes.
   125  
   126  // greetings prints a brief welcome and some overall profile
   127  // information before accepting interactive commands.
   128  func greetings(p *profile.Profile, ui plugin.UI) {
   129  	numLabelUnits := identifyNumLabelUnits(p, ui)
   130  	ropt, err := reportOptions(p, numLabelUnits, currentConfig())
   131  	if err == nil {
   132  		rpt := report.New(p, ropt)
   133  		ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
   134  		if rpt.Total() == 0 && len(p.SampleType) > 1 {
   135  			ui.Print(`No samples were found with the default sample value type.`)
   136  			ui.Print(`Try "sample_index" command to analyze different sample values.`, "\n")
   137  		}
   138  	}
   139  	ui.Print(`Entering interactive mode (type "help" for commands, "o" for options)`)
   140  }
   141  
   142  // shortcuts represents composite commands that expand into a sequence
   143  // of other commands.
   144  type shortcuts map[string][]string
   145  
   146  func (a shortcuts) expand(input string) []string {
   147  	input = strings.TrimSpace(input)
   148  	if a != nil {
   149  		if r, ok := a[input]; ok {
   150  			return r
   151  		}
   152  	}
   153  	return []string{input}
   154  }
   155  
   156  var pprofShortcuts = shortcuts{
   157  	":": []string{"focus=", "ignore=", "hide=", "tagfocus=", "tagignore="},
   158  }
   159  
   160  // profileShortcuts creates macros for convenience and backward compatibility.
   161  func profileShortcuts(p *profile.Profile) shortcuts {
   162  	s := pprofShortcuts
   163  	// Add shortcuts for sample types
   164  	for _, st := range p.SampleType {
   165  		command := fmt.Sprintf("sample_index=%s", st.Type)
   166  		s[st.Type] = []string{command}
   167  		s["total_"+st.Type] = []string{"mean=0", command}
   168  		s["mean_"+st.Type] = []string{"mean=1", command}
   169  	}
   170  	return s
   171  }
   172  
   173  func sampleTypes(p *profile.Profile) []string {
   174  	types := make([]string, len(p.SampleType))
   175  	for i, t := range p.SampleType {
   176  		types[i] = t.Type
   177  	}
   178  	return types
   179  }
   180  
   181  func printCurrentOptions(p *profile.Profile, ui plugin.UI) {
   182  	var args []string
   183  	current := currentConfig()
   184  	for _, f := range configFields {
   185  		n := f.name
   186  		v := current.get(f)
   187  		comment := ""
   188  		switch {
   189  		case len(f.choices) > 0:
   190  			values := append([]string{}, f.choices...)
   191  			sort.Strings(values)
   192  			comment = "[" + strings.Join(values, " | ") + "]"
   193  		case n == "sample_index":
   194  			st := sampleTypes(p)
   195  			if v == "" {
   196  				// Apply default (last sample index).
   197  				v = st[len(st)-1]
   198  			}
   199  			// Add comments for all sample types in profile.
   200  			comment = "[" + strings.Join(st, " | ") + "]"
   201  		case n == "source_path":
   202  			continue
   203  		case n == "nodecount" && v == "-1":
   204  			comment = "default"
   205  		case v == "":
   206  			// Add quotes for empty values.
   207  			v = `""`
   208  		}
   209  		if n == "granularity" && v == "" {
   210  			v = "(default)"
   211  		}
   212  		if comment != "" {
   213  			comment = commentStart + " " + comment
   214  		}
   215  		args = append(args, fmt.Sprintf("  %-25s = %-20s %s", n, v, comment))
   216  	}
   217  	sort.Strings(args)
   218  	ui.Print(strings.Join(args, "\n"))
   219  }
   220  
   221  // parseCommandLine parses a command and returns the pprof command to
   222  // execute and the configuration to use for the report.
   223  func parseCommandLine(input []string) ([]string, config, error) {
   224  	cmd, args := input[:1], input[1:]
   225  	name := cmd[0]
   226  
   227  	c := pprofCommands[name]
   228  	if c == nil {
   229  		// Attempt splitting digits on abbreviated commands (eg top10)
   230  		if d := tailDigitsRE.FindString(name); d != "" && d != name {
   231  			name = name[:len(name)-len(d)]
   232  			cmd[0], args = name, append([]string{d}, args...)
   233  			c = pprofCommands[name]
   234  		}
   235  	}
   236  	if c == nil {
   237  		if _, ok := configHelp[name]; ok {
   238  			value := "<val>"
   239  			if len(args) > 0 {
   240  				value = args[0]
   241  			}
   242  			return nil, config{}, fmt.Errorf("did you mean: %s=%s", name, value)
   243  		}
   244  		return nil, config{}, fmt.Errorf("unrecognized command: %q", name)
   245  	}
   246  
   247  	if c.hasParam {
   248  		if len(args) == 0 {
   249  			return nil, config{}, fmt.Errorf("command %s requires an argument", name)
   250  		}
   251  		cmd = append(cmd, args[0])
   252  		args = args[1:]
   253  	}
   254  
   255  	// Copy config since options set in the command line should not persist.
   256  	vcopy := currentConfig()
   257  
   258  	var focus, ignore string
   259  	for i := 0; i < len(args); i++ {
   260  		t := args[i]
   261  		if n, err := strconv.ParseInt(t, 10, 32); err == nil {
   262  			vcopy.NodeCount = int(n)
   263  			continue
   264  		}
   265  		switch t[0] {
   266  		case '>':
   267  			outputFile := t[1:]
   268  			if outputFile == "" {
   269  				i++
   270  				if i >= len(args) {
   271  					return nil, config{}, fmt.Errorf("unexpected end of line after >")
   272  				}
   273  				outputFile = args[i]
   274  			}
   275  			vcopy.Output = outputFile
   276  		case '-':
   277  			if t == "--cum" || t == "-cum" {
   278  				vcopy.Sort = "cum"
   279  				continue
   280  			}
   281  			ignore = catRegex(ignore, t[1:])
   282  		default:
   283  			focus = catRegex(focus, t)
   284  		}
   285  	}
   286  
   287  	if name == "tags" {
   288  		if focus != "" {
   289  			vcopy.TagFocus = focus
   290  		}
   291  		if ignore != "" {
   292  			vcopy.TagIgnore = ignore
   293  		}
   294  	} else {
   295  		if focus != "" {
   296  			vcopy.Focus = focus
   297  		}
   298  		if ignore != "" {
   299  			vcopy.Ignore = ignore
   300  		}
   301  	}
   302  	if vcopy.NodeCount == -1 && (name == "text" || name == "top") {
   303  		vcopy.NodeCount = 10
   304  	}
   305  
   306  	return cmd, vcopy, nil
   307  }
   308  
   309  func catRegex(a, b string) string {
   310  	if a != "" && b != "" {
   311  		return a + "|" + b
   312  	}
   313  	return a + b
   314  }
   315  
   316  // commandHelp displays help and usage information for all Commands
   317  // and Variables or a specific Command or Variable.
   318  func commandHelp(args string, ui plugin.UI) {
   319  	if args == "" {
   320  		help := usage(false)
   321  		help = help + `
   322    :   Clear focus/ignore/hide/tagfocus/tagignore
   323  
   324    type "help <cmd|option>" for more information
   325  `
   326  
   327  		ui.Print(help)
   328  		return
   329  	}
   330  
   331  	if c := pprofCommands[args]; c != nil {
   332  		ui.Print(c.help(args))
   333  		return
   334  	}
   335  
   336  	if help, ok := configHelp[args]; ok {
   337  		ui.Print(help + "\n")
   338  		return
   339  	}
   340  
   341  	ui.PrintErr("Unknown command: " + args)
   342  }
   343  
   344  // newCompleter creates an autocompletion function for a set of commands.
   345  func newCompleter(fns []string) func(string) string {
   346  	return func(line string) string {
   347  		switch tokens := strings.Fields(line); len(tokens) {
   348  		case 0:
   349  			// Nothing to complete
   350  		case 1:
   351  			// Single token -- complete command name
   352  			if match := matchVariableOrCommand(tokens[0]); match != "" {
   353  				return match
   354  			}
   355  		case 2:
   356  			if tokens[0] == "help" {
   357  				if match := matchVariableOrCommand(tokens[1]); match != "" {
   358  					return tokens[0] + " " + match
   359  				}
   360  				return line
   361  			}
   362  			fallthrough
   363  		default:
   364  			// Multiple tokens -- complete using functions, except for tags
   365  			if cmd := pprofCommands[tokens[0]]; cmd != nil && tokens[0] != "tags" {
   366  				lastTokenIdx := len(tokens) - 1
   367  				lastToken := tokens[lastTokenIdx]
   368  				if strings.HasPrefix(lastToken, "-") {
   369  					lastToken = "-" + functionCompleter(lastToken[1:], fns)
   370  				} else {
   371  					lastToken = functionCompleter(lastToken, fns)
   372  				}
   373  				return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ")
   374  			}
   375  		}
   376  		return line
   377  	}
   378  }
   379  
   380  // matchVariableOrCommand attempts to match a string token to the prefix of a Command.
   381  func matchVariableOrCommand(token string) string {
   382  	token = strings.ToLower(token)
   383  	var matches []string
   384  	for cmd := range pprofCommands {
   385  		if strings.HasPrefix(cmd, token) {
   386  			matches = append(matches, cmd)
   387  		}
   388  	}
   389  	matches = append(matches, completeConfig(token)...)
   390  	if len(matches) == 1 {
   391  		return matches[0]
   392  	}
   393  	return ""
   394  }
   395  
   396  // functionCompleter replaces provided substring with a function
   397  // name retrieved from a profile if a single match exists. Otherwise,
   398  // it returns unchanged substring. It defaults to no-op if the profile
   399  // is not specified.
   400  func functionCompleter(substring string, fns []string) string {
   401  	found := ""
   402  	for _, fName := range fns {
   403  		if strings.Contains(fName, substring) {
   404  			if found != "" {
   405  				return substring
   406  			}
   407  			found = fName
   408  		}
   409  	}
   410  	if found != "" {
   411  		return found
   412  	}
   413  	return substring
   414  }
   415  
   416  func functionNames(p *profile.Profile) []string {
   417  	var fns []string
   418  	for _, fn := range p.Function {
   419  		fns = append(fns, fn.Name)
   420  	}
   421  	return fns
   422  }
   423  

View as plain text