123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- // 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"
- "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
-
- // 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)))
- pprofVariables.set("compact_labels", "true")
- pprofVariables["sample_index"].help += 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.
- waitForVisualizer = false
- 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 v := pprofVariables[name]; v != nil {
- if name == "sample_index" {
- // Error check sample_index=xxx to ensure xxx is a valid sample type.
- index, err := locateSampleIndex(p, value)
- if err != nil {
- o.UI.PrintErr(err)
- continue
- }
- value = p.SampleType[index].Type
- }
- if err := pprofVariables.set(name, value); err != nil {
- o.UI.PrintErr(err)
- }
- continue
- }
- // Allow group=variable syntax by converting into variable="".
- if v := pprofVariables[value]; v != nil && v.group == name {
- if err := pprofVariables.set(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":
- return nil
- case "help":
- commandHelp(strings.Join(tokens[1:], " "), o.UI)
- continue
- }
-
- args, vars, err := parseCommandLine(tokens)
- if err == nil {
- err = generateReportWrapper(p, args, vars, 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) {
- ropt, err := reportOptions(p, pprofVariables)
- if err == nil {
- ui.Print(strings.Join(report.ProfileLabels(report.New(p, ropt)), "\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 printCurrentOptions(p *profile.Profile, ui plugin.UI) {
- var args []string
- type groupInfo struct {
- set string
- values []string
- }
- groups := make(map[string]*groupInfo)
- for n, o := range pprofVariables {
- v := o.stringValue()
- comment := ""
- if g := o.group; g != "" {
- gi, ok := groups[g]
- if !ok {
- gi = &groupInfo{}
- groups[g] = gi
- }
- if o.boolValue() {
- gi.set = n
- }
- gi.values = append(gi.values, n)
- continue
- }
- switch {
- 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))
- }
- for g, vars := range groups {
- sort.Strings(vars.values)
- comment := commentStart + " [" + strings.Join(vars.values, " | ") + "]"
- args = append(args, fmt.Sprintf(" %-25s = %-20s %s", g, vars.set, comment))
- }
- sort.Strings(args)
- ui.Print(strings.Join(args, "\n"))
- }
-
- // parseCommandLine parses a command and returns the pprof command to
- // execute and a set of variables for the report.
- func parseCommandLine(input []string) ([]string, variables, error) {
- cmd, args := input[:1], input[1:]
- name := cmd[0]
-
- c := pprofCommands[name]
- if c == nil {
- // Attempt splitting digits on abbreviated commands (eg top10)
- for i := len(name); i > 0; i-- {
- if !strings.ContainsAny(name[i-1:i], "0123456789") {
- if n, d := name[:i], name[i:]; n != "" && d != "" {
- cmd[0], args = n, append([]string{d}, args...)
- name = n
- c = pprofCommands[n]
- }
- break
- }
- }
- }
- if c == nil {
- return nil, nil, fmt.Errorf("Unrecognized command: %q", name)
- }
-
- if c.hasParam {
- if len(args) == 0 {
- return nil, nil, fmt.Errorf("command %s requires an argument", name)
- }
- cmd = append(cmd, args[0])
- args = args[1:]
- }
-
- // Copy the variables as options set in the command line are not persistent.
- vcopy := pprofVariables.makeCopy()
-
- var focus, ignore string
- for i := 0; i < len(args); i++ {
- t := args[i]
- if _, err := strconv.ParseInt(t, 10, 32); err == nil {
- vcopy.set("nodecount", t)
- continue
- }
- switch t[0] {
- case '>':
- outputFile := t[1:]
- if outputFile == "" {
- i++
- if i >= len(args) {
- return nil, nil, fmt.Errorf("Unexpected end of line after >")
- }
- outputFile = args[i]
- }
- vcopy.set("output", outputFile)
- case '-':
- if t == "--cum" || t == "-cum" {
- vcopy.set("cum", "t")
- continue
- }
- ignore = catRegex(ignore, t[1:])
- default:
- focus = catRegex(focus, t)
- }
- }
-
- if name == "tags" {
- updateFocusIgnore(vcopy, "tag", focus, ignore)
- } else {
- updateFocusIgnore(vcopy, "", focus, ignore)
- }
-
- if vcopy["nodecount"].intValue() == -1 && (name == "text" || name == "top") {
- vcopy.set("nodecount", "10")
- }
-
- return cmd, vcopy, nil
- }
-
- func updateFocusIgnore(v variables, prefix, f, i string) {
- if f != "" {
- focus := prefix + "focus"
- v.set(focus, catRegex(v[focus].value, f))
- }
-
- if i != "" {
- ignore := prefix + "ignore"
- v.set(ignore, catRegex(v[ignore].value, i))
- }
- }
-
- 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 v := pprofVariables[args]; v != nil {
- ui.Print(v.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 {
- v := pprofVariables
- switch tokens := strings.Fields(line); len(tokens) {
- case 0:
- // Nothing to complete
- case 1:
- // Single token -- complete command name
- if match := matchVariableOrCommand(v, tokens[0]); match != "" {
- return match
- }
- case 2:
- if tokens[0] == "help" {
- if match := matchVariableOrCommand(v, 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
- }
- }
-
- // matchCommand attempts to match a string token to the prefix of a Command.
- func matchVariableOrCommand(v variables, token string) string {
- token = strings.ToLower(token)
- found := ""
- for cmd := range pprofCommands {
- if strings.HasPrefix(cmd, token) {
- if found != "" {
- return ""
- }
- found = cmd
- }
- }
- for variable := range v {
- if strings.HasPrefix(variable, token) {
- if found != "" {
- return ""
- }
- found = variable
- }
- }
- return found
- }
-
- // 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
- }
|