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
15package driver
16
17import (
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
30var commentStart = "//:" // Sentinel for comments on options
31var tailDigitsRE = regexp.MustCompile("[0-9]+$")
32
33// interactive starts a shell to read pprof commands.
34func 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
123var generateReportWrapper = generateReport // For testing purposes.
124
125// greetings prints a brief welcome and some overall profile
126// information before accepting interactive commands.
127func 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.
143type shortcuts map[string][]string
144
145func (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
155var pprofShortcuts = shortcuts{
156	":": []string{"focus=", "ignore=", "hide=", "tagfocus=", "tagignore="},
157}
158
159// profileShortcuts creates macros for convenience and backward compatibility.
160func 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
172func 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
180func 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.
219func 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
305func 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.
314func 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.
341func 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.
377func 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.
396func 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
412func 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