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