// Copyright 2014 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package driver import ( "fmt" "io" "regexp" "sort" "strconv" "strings" "github.com/google/pprof/internal/plugin" "github.com/google/pprof/internal/report" "github.com/google/pprof/profile" ) var commentStart = "//:" // Sentinel for comments on options var tailDigitsRE = regexp.MustCompile("[0-9]+$") // interactive starts a shell to read pprof commands. func interactive(p *profile.Profile, o *plugin.Options) error { // Enter command processing loop. o.UI.SetAutoComplete(newCompleter(functionNames(p))) configure("compact_labels", "true") configHelp["sample_index"] += fmt.Sprintf("Or use sample_index=name, with name in %v.\n", sampleTypes(p)) // Do not wait for the visualizer to complete, to allow multiple // graphs to be visualized simultaneously. interactiveMode = true shortcuts := profileShortcuts(p) greetings(p, o.UI) for { input, err := o.UI.ReadLine("(pprof) ") if err != nil { if err != io.EOF { return err } if input == "" { return nil } } for _, input := range shortcuts.expand(input) { // Process assignments of the form variable=value if s := strings.SplitN(input, "=", 2); len(s) > 0 { name := strings.TrimSpace(s[0]) var value string if len(s) == 2 { value = s[1] if comment := strings.LastIndex(value, commentStart); comment != -1 { value = value[:comment] } value = strings.TrimSpace(value) } if isConfigurable(name) { // All non-bool options require inputs if len(s) == 1 && !isBoolConfig(name) { o.UI.PrintErr(fmt.Errorf("please specify a value, e.g. %s=", name)) continue } if name == "sample_index" { // Error check sample_index=xxx to ensure xxx is a valid sample type. index, err := p.SampleIndexByName(value) if err != nil { o.UI.PrintErr(err) continue } if index < 0 || index >= len(p.SampleType) { o.UI.PrintErr(fmt.Errorf("invalid sample_index %q", value)) continue } value = p.SampleType[index].Type } if err := configure(name, value); err != nil { o.UI.PrintErr(err) } continue } } tokens := strings.Fields(input) if len(tokens) == 0 { continue } switch tokens[0] { case "o", "options": printCurrentOptions(p, o.UI) continue case "exit", "quit", "q": return nil case "help": commandHelp(strings.Join(tokens[1:], " "), o.UI) continue } args, cfg, err := parseCommandLine(tokens) if err == nil { err = generateReportWrapper(p, args, cfg, o) } if err != nil { o.UI.PrintErr(err) } } } } var generateReportWrapper = generateReport // For testing purposes. // greetings prints a brief welcome and some overall profile // information before accepting interactive commands. func greetings(p *profile.Profile, ui plugin.UI) { numLabelUnits := identifyNumLabelUnits(p, ui) ropt, err := reportOptions(p, numLabelUnits, currentConfig()) if err == nil { rpt := report.New(p, ropt) ui.Print(strings.Join(report.ProfileLabels(rpt), "\n")) if rpt.Total() == 0 && len(p.SampleType) > 1 { ui.Print(`No samples were found with the default sample value type.`) ui.Print(`Try "sample_index" command to analyze different sample values.`, "\n") } } ui.Print(`Entering interactive mode (type "help" for commands, "o" for options)`) } // shortcuts represents composite commands that expand into a sequence // of other commands. type shortcuts map[string][]string func (a shortcuts) expand(input string) []string { input = strings.TrimSpace(input) if a != nil { if r, ok := a[input]; ok { return r } } return []string{input} } var pprofShortcuts = shortcuts{ ":": []string{"focus=", "ignore=", "hide=", "tagfocus=", "tagignore="}, } // profileShortcuts creates macros for convenience and backward compatibility. func profileShortcuts(p *profile.Profile) shortcuts { s := pprofShortcuts // Add shortcuts for sample types for _, st := range p.SampleType { command := fmt.Sprintf("sample_index=%s", st.Type) s[st.Type] = []string{command} s["total_"+st.Type] = []string{"mean=0", command} s["mean_"+st.Type] = []string{"mean=1", command} } return s } func sampleTypes(p *profile.Profile) []string { types := make([]string, len(p.SampleType)) for i, t := range p.SampleType { types[i] = t.Type } return types } func printCurrentOptions(p *profile.Profile, ui plugin.UI) { var args []string current := currentConfig() for _, f := range configFields { n := f.name v := current.get(f) comment := "" switch { case len(f.choices) > 0: values := append([]string{}, f.choices...) sort.Strings(values) comment = "[" + strings.Join(values, " | ") + "]" case n == "sample_index": st := sampleTypes(p) if v == "" { // Apply default (last sample index). v = st[len(st)-1] } // Add comments for all sample types in profile. comment = "[" + strings.Join(st, " | ") + "]" case n == "source_path": continue case n == "nodecount" && v == "-1": comment = "default" case v == "": // Add quotes for empty values. v = `""` } if comment != "" { comment = commentStart + " " + comment } args = append(args, fmt.Sprintf(" %-25s = %-20s %s", n, v, comment)) } sort.Strings(args) ui.Print(strings.Join(args, "\n")) } // parseCommandLine parses a command and returns the pprof command to // execute and the configuration to use for the report. func parseCommandLine(input []string) ([]string, config, error) { cmd, args := input[:1], input[1:] name := cmd[0] c := pprofCommands[name] if c == nil { // Attempt splitting digits on abbreviated commands (eg top10) if d := tailDigitsRE.FindString(name); d != "" && d != name { name = name[:len(name)-len(d)] cmd[0], args = name, append([]string{d}, args...) c = pprofCommands[name] } } if c == nil { if _, ok := configHelp[name]; ok { value := "" if len(args) > 0 { value = args[0] } return nil, config{}, fmt.Errorf("did you mean: %s=%s", name, value) } return nil, config{}, fmt.Errorf("unrecognized command: %q", name) } if c.hasParam { if len(args) == 0 { return nil, config{}, fmt.Errorf("command %s requires an argument", name) } cmd = append(cmd, args[0]) args = args[1:] } // Copy config since options set in the command line should not persist. vcopy := currentConfig() var focus, ignore string for i := 0; i < len(args); i++ { t := args[i] if n, err := strconv.ParseInt(t, 10, 32); err == nil { vcopy.NodeCount = int(n) continue } switch t[0] { case '>': outputFile := t[1:] if outputFile == "" { i++ if i >= len(args) { return nil, config{}, fmt.Errorf("unexpected end of line after >") } outputFile = args[i] } vcopy.Output = outputFile case '-': if t == "--cum" || t == "-cum" { vcopy.Sort = "cum" continue } ignore = catRegex(ignore, t[1:]) default: focus = catRegex(focus, t) } } if name == "tags" { if focus != "" { vcopy.TagFocus = focus } if ignore != "" { vcopy.TagIgnore = ignore } } else { if focus != "" { vcopy.Focus = focus } if ignore != "" { vcopy.Ignore = ignore } } if vcopy.NodeCount == -1 && (name == "text" || name == "top") { vcopy.NodeCount = 10 } return cmd, vcopy, nil } func catRegex(a, b string) string { if a != "" && b != "" { return a + "|" + b } return a + b } // commandHelp displays help and usage information for all Commands // and Variables or a specific Command or Variable. func commandHelp(args string, ui plugin.UI) { if args == "" { help := usage(false) help = help + ` : Clear focus/ignore/hide/tagfocus/tagignore type "help " for more information ` ui.Print(help) return } if c := pprofCommands[args]; c != nil { ui.Print(c.help(args)) return } if help, ok := configHelp[args]; ok { ui.Print(help + "\n") return } ui.PrintErr("Unknown command: " + args) } // newCompleter creates an autocompletion function for a set of commands. func newCompleter(fns []string) func(string) string { return func(line string) string { switch tokens := strings.Fields(line); len(tokens) { case 0: // Nothing to complete case 1: // Single token -- complete command name if match := matchVariableOrCommand(tokens[0]); match != "" { return match } case 2: if tokens[0] == "help" { if match := matchVariableOrCommand(tokens[1]); match != "" { return tokens[0] + " " + match } return line } fallthrough default: // Multiple tokens -- complete using functions, except for tags if cmd := pprofCommands[tokens[0]]; cmd != nil && tokens[0] != "tags" { lastTokenIdx := len(tokens) - 1 lastToken := tokens[lastTokenIdx] if strings.HasPrefix(lastToken, "-") { lastToken = "-" + functionCompleter(lastToken[1:], fns) } else { lastToken = functionCompleter(lastToken, fns) } return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ") } } return line } } // matchVariableOrCommand attempts to match a string token to the prefix of a Command. func matchVariableOrCommand(token string) string { token = strings.ToLower(token) var matches []string for cmd := range pprofCommands { if strings.HasPrefix(cmd, token) { matches = append(matches, cmd) } } matches = append(matches, completeConfig(token)...) if len(matches) == 1 { return matches[0] } return "" } // functionCompleter replaces provided substring with a function // name retrieved from a profile if a single match exists. Otherwise, // it returns unchanged substring. It defaults to no-op if the profile // is not specified. func functionCompleter(substring string, fns []string) string { found := "" for _, fName := range fns { if strings.Contains(fName, substring) { if found != "" { return substring } found = fName } } if found != "" { return found } return substring } func functionNames(p *profile.Profile) []string { var fns []string for _, fn := range p.Function { fns = append(fns, fn.Name) } return fns }