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