| // 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) | |
| copier := makeProfileCopier(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=<val>", 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(copier.newCopy(), 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 n == "granularity" && v == "" { | |
| v = "(default)" | |
| } | |
| 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 := "<val>" | |
| 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 <cmd|option>" 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 | |
| } |