...

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

View as plain text