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 comment != "" {
   210  			comment = commentStart + " " + comment
   211  		}
   212  		args = append(args, fmt.Sprintf("  %-25s = %-20s %s", n, v, comment))
   213  	}
   214  	sort.Strings(args)
   215  	ui.Print(strings.Join(args, "\n"))
   216  }
   217  
   218  // parseCommandLine parses a command and returns the pprof command to
   219  // execute and the configuration to use for the report.
   220  func parseCommandLine(input []string) ([]string, config, error) {
   221  	cmd, args := input[:1], input[1:]
   222  	name := cmd[0]
   223  
   224  	c := pprofCommands[name]
   225  	if c == nil {
   226  		// Attempt splitting digits on abbreviated commands (eg top10)
   227  		if d := tailDigitsRE.FindString(name); d != "" && d != name {
   228  			name = name[:len(name)-len(d)]
   229  			cmd[0], args = name, append([]string{d}, args...)
   230  			c = pprofCommands[name]
   231  		}
   232  	}
   233  	if c == nil {
   234  		if _, ok := configHelp[name]; ok {
   235  			value := "<val>"
   236  			if len(args) > 0 {
   237  				value = args[0]
   238  			}
   239  			return nil, config{}, fmt.Errorf("did you mean: %s=%s", name, value)
   240  		}
   241  		return nil, config{}, fmt.Errorf("unrecognized command: %q", name)
   242  	}
   243  
   244  	if c.hasParam {
   245  		if len(args) == 0 {
   246  			return nil, config{}, fmt.Errorf("command %s requires an argument", name)
   247  		}
   248  		cmd = append(cmd, args[0])
   249  		args = args[1:]
   250  	}
   251  
   252  	// Copy config since options set in the command line should not persist.
   253  	vcopy := currentConfig()
   254  
   255  	var focus, ignore string
   256  	for i := 0; i < len(args); i++ {
   257  		t := args[i]
   258  		if n, err := strconv.ParseInt(t, 10, 32); err == nil {
   259  			vcopy.NodeCount = int(n)
   260  			continue
   261  		}
   262  		switch t[0] {
   263  		case '>':
   264  			outputFile := t[1:]
   265  			if outputFile == "" {
   266  				i++
   267  				if i >= len(args) {
   268  					return nil, config{}, fmt.Errorf("unexpected end of line after >")
   269  				}
   270  				outputFile = args[i]
   271  			}
   272  			vcopy.Output = outputFile
   273  		case '-':
   274  			if t == "--cum" || t == "-cum" {
   275  				vcopy.Sort = "cum"
   276  				continue
   277  			}
   278  			ignore = catRegex(ignore, t[1:])
   279  		default:
   280  			focus = catRegex(focus, t)
   281  		}
   282  	}
   283  
   284  	if name == "tags" {
   285  		if focus != "" {
   286  			vcopy.TagFocus = focus
   287  		}
   288  		if ignore != "" {
   289  			vcopy.TagIgnore = ignore
   290  		}
   291  	} else {
   292  		if focus != "" {
   293  			vcopy.Focus = focus
   294  		}
   295  		if ignore != "" {
   296  			vcopy.Ignore = ignore
   297  		}
   298  	}
   299  	if vcopy.NodeCount == -1 && (name == "text" || name == "top") {
   300  		vcopy.NodeCount = 10
   301  	}
   302  
   303  	return cmd, vcopy, nil
   304  }
   305  
   306  func catRegex(a, b string) string {
   307  	if a != "" && b != "" {
   308  		return a + "|" + b
   309  	}
   310  	return a + b
   311  }
   312  
   313  // commandHelp displays help and usage information for all Commands
   314  // and Variables or a specific Command or Variable.
   315  func commandHelp(args string, ui plugin.UI) {
   316  	if args == "" {
   317  		help := usage(false)
   318  		help = help + `
   319    :   Clear focus/ignore/hide/tagfocus/tagignore
   320  
   321    type "help <cmd|option>" for more information
   322  `
   323  
   324  		ui.Print(help)
   325  		return
   326  	}
   327  
   328  	if c := pprofCommands[args]; c != nil {
   329  		ui.Print(c.help(args))
   330  		return
   331  	}
   332  
   333  	if help, ok := configHelp[args]; ok {
   334  		ui.Print(help + "\n")
   335  		return
   336  	}
   337  
   338  	ui.PrintErr("Unknown command: " + args)
   339  }
   340  
   341  // newCompleter creates an autocompletion function for a set of commands.
   342  func newCompleter(fns []string) func(string) string {
   343  	return func(line string) string {
   344  		switch tokens := strings.Fields(line); len(tokens) {
   345  		case 0:
   346  			// Nothing to complete
   347  		case 1:
   348  			// Single token -- complete command name
   349  			if match := matchVariableOrCommand(tokens[0]); match != "" {
   350  				return match
   351  			}
   352  		case 2:
   353  			if tokens[0] == "help" {
   354  				if match := matchVariableOrCommand(tokens[1]); match != "" {
   355  					return tokens[0] + " " + match
   356  				}
   357  				return line
   358  			}
   359  			fallthrough
   360  		default:
   361  			// Multiple tokens -- complete using functions, except for tags
   362  			if cmd := pprofCommands[tokens[0]]; cmd != nil && tokens[0] != "tags" {
   363  				lastTokenIdx := len(tokens) - 1
   364  				lastToken := tokens[lastTokenIdx]
   365  				if strings.HasPrefix(lastToken, "-") {
   366  					lastToken = "-" + functionCompleter(lastToken[1:], fns)
   367  				} else {
   368  					lastToken = functionCompleter(lastToken, fns)
   369  				}
   370  				return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ")
   371  			}
   372  		}
   373  		return line
   374  	}
   375  }
   376  
   377  // matchVariableOrCommand attempts to match a string token to the prefix of a Command.
   378  func matchVariableOrCommand(token string) string {
   379  	token = strings.ToLower(token)
   380  	var matches []string
   381  	for cmd := range pprofCommands {
   382  		if strings.HasPrefix(cmd, token) {
   383  			matches = append(matches, cmd)
   384  		}
   385  	}
   386  	matches = append(matches, completeConfig(token)...)
   387  	if len(matches) == 1 {
   388  		return matches[0]
   389  	}
   390  	return ""
   391  }
   392  
   393  // functionCompleter replaces provided substring with a function
   394  // name retrieved from a profile if a single match exists. Otherwise,
   395  // it returns unchanged substring. It defaults to no-op if the profile
   396  // is not specified.
   397  func functionCompleter(substring string, fns []string) string {
   398  	found := ""
   399  	for _, fName := range fns {
   400  		if strings.Contains(fName, substring) {
   401  			if found != "" {
   402  				return substring
   403  			}
   404  			found = fName
   405  		}
   406  	}
   407  	if found != "" {
   408  		return found
   409  	}
   410  	return substring
   411  }
   412  
   413  func functionNames(p *profile.Profile) []string {
   414  	var fns []string
   415  	for _, fn := range p.Function {
   416  		fns = append(fns, fn.Name)
   417  	}
   418  	return fns
   419  }
   420  

View as plain text