1
2
3
4
5
6
7
8
9
10
11
12
13
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 = "//:"
31 var tailDigitsRE = regexp.MustCompile("[0-9]+$")
32
33
34 func interactive(p *profile.Profile, o *plugin.Options) error {
35
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
41
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
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
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
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
124
125
126
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
142
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
160 func profileShortcuts(p *profile.Profile) shortcuts {
161 s := pprofShortcuts
162
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
196 v = st[len(st)-1]
197 }
198
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
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
218
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
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
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
313
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
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
346 case 1:
347
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
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
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
393
394
395
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