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

View as plain text