Index: src/cmd/pprof/internal/driver/interactive.go |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/src/cmd/pprof/internal/driver/interactive.go |
@@ -0,0 +1,492 @@ |
+// Copyright 2014 The Go Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style |
+// license that can be found in the LICENSE file. |
+ |
+package driver |
+ |
+import ( |
+ "fmt" |
+ "io" |
+ "regexp" |
+ "sort" |
+ "strconv" |
+ "strings" |
+ |
+ "cmd/pprof/internal/commands" |
+ "cmd/pprof/internal/plugin" |
+ "cmd/pprof/internal/profile" |
+) |
+ |
+var profileFunctionNames = []string{} |
+ |
+// 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) string { |
+ found := "" |
+ for _, fName := range profileFunctionNames { |
+ if strings.Contains(fName, substring) { |
+ if found != "" { |
+ return substring |
+ } |
+ found = fName |
+ } |
+ } |
+ if found != "" { |
+ return found |
+ } |
+ return substring |
+} |
+ |
+// updateAutoComplete enhances autocompletion with information that can be |
+// retrieved from the profile |
+func updateAutoComplete(p *profile.Profile) { |
+ profileFunctionNames = nil // remove function names retrieved previously |
+ for _, fn := range p.Function { |
+ profileFunctionNames = append(profileFunctionNames, fn.Name) |
+ } |
+} |
+ |
+// splitCommand splits the command line input into tokens separated by |
+// spaces. Takes care to separate commands of the form 'top10' into |
+// two tokens: 'top' and '10' |
+func splitCommand(input string) []string { |
+ fields := strings.Fields(input) |
+ if num := strings.IndexAny(fields[0], "0123456789"); num != -1 { |
+ inputNumber := fields[0][num:] |
+ fields[0] = fields[0][:num] |
+ fields = append([]string{fields[0], inputNumber}, fields[1:]...) |
+ } |
+ return fields |
+} |
+ |
+// interactive displays a prompt and reads commands for profile |
+// manipulation/visualization. |
+func interactive(p *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error { |
+ updateAutoComplete(p) |
+ |
+ // Enter command processing loop. |
+ ui.Print("Entering interactive mode (type \"help\" for commands)") |
+ ui.SetAutoComplete(commands.NewCompleter(f.commands)) |
+ |
+ for { |
+ input, err := readCommand(p, ui, f) |
+ if err != nil { |
+ if err != io.EOF { |
+ return err |
+ } |
+ if input == "" { |
+ return nil |
+ } |
+ } |
+ // Process simple commands. |
+ switch input { |
+ case "": |
+ continue |
+ case ":": |
+ f.flagFocus = newString("") |
+ f.flagIgnore = newString("") |
+ f.flagTagFocus = newString("") |
+ f.flagTagIgnore = newString("") |
+ f.flagHide = newString("") |
+ continue |
+ } |
+ |
+ fields := splitCommand(input) |
+ // Process report generation commands. |
+ if _, ok := f.commands[fields[0]]; ok { |
+ if err := generateReport(p, fields, obj, ui, f); err != nil { |
+ if err == io.EOF { |
+ return nil |
+ } |
+ ui.PrintErr(err) |
+ } |
+ continue |
+ } |
+ |
+ switch cmd := fields[0]; cmd { |
+ case "help": |
+ commandHelp(fields, ui, f) |
+ continue |
+ case "exit", "quit": |
+ return nil |
+ } |
+ |
+ // Process option settings. |
+ if of, err := optFlags(p, input, f); err == nil { |
+ f = of |
+ } else { |
+ ui.PrintErr("Error: ", err.Error()) |
+ } |
+ } |
+} |
+ |
+func generateReport(p *profile.Profile, cmd []string, obj plugin.ObjTool, ui plugin.UI, f *flags) error { |
+ prof := p.Copy() |
+ |
+ cf, err := cmdFlags(prof, cmd, ui, f) |
+ if err != nil { |
+ return err |
+ } |
+ |
+ return generate(true, prof, obj, ui, cf) |
+} |
+ |
+// validateRegex checks if a string is a valid regular expression. |
+func validateRegex(v string) error { |
+ _, err := regexp.Compile(v) |
+ return err |
+} |
+ |
+// readCommand prompts for and reads the next command. |
+func readCommand(p *profile.Profile, ui plugin.UI, f *flags) (string, error) { |
+ //ui.Print("Options:\n", f.String(p)) |
+ s, err := ui.ReadLine() |
+ return strings.TrimSpace(s), err |
+} |
+ |
+func commandHelp(_ []string, ui plugin.UI, f *flags) error { |
+ help := ` |
+ Commands: |
+ cmd [n] [--cum] [focus_regex]* [-ignore_regex]* |
+ Produce a text report with the top n entries. |
+ Include samples matching focus_regex, and exclude ignore_regex. |
+ Add --cum to sort using cumulative data. |
+ Available commands: |
+` |
+ var commands []string |
+ for name, cmd := range f.commands { |
+ commands = append(commands, fmt.Sprintf(" %-12s %s", name, cmd.Usage)) |
+ } |
+ sort.Strings(commands) |
+ |
+ help = help + strings.Join(commands, "\n") + ` |
+ peek func_regex |
+ Display callers and callees of functions matching func_regex. |
+ |
+ dot [n] [focus_regex]* [-ignore_regex]* [>file] |
+ Produce an annotated callgraph with the top n entries. |
+ Include samples matching focus_regex, and exclude ignore_regex. |
+ For other outputs, replace dot with: |
+ - Graphic formats: dot, svg, pdf, ps, gif, png (use > to name output file) |
+ - Graph viewer: gv, web, evince, eog |
+ |
+ callgrind [n] [focus_regex]* [-ignore_regex]* [>file] |
+ Produce a file in callgrind-compatible format. |
+ Include samples matching focus_regex, and exclude ignore_regex. |
+ |
+ weblist func_regex [-ignore_regex]* |
+ Show annotated source with interspersed assembly in a web browser. |
+ |
+ list func_regex [-ignore_regex]* |
+ Print source for routines matching func_regex, and exclude ignore_regex. |
+ |
+ disasm func_regex [-ignore_regex]* |
+ Disassemble routines matching func_regex, and exclude ignore_regex. |
+ |
+ tags tag_regex [-ignore_regex]* |
+ List tags with key:value matching tag_regex and exclude ignore_regex. |
+ |
+ quit/exit/^D |
+ Exit pprof. |
+ |
+ option=value |
+ The following options can be set individually: |
+ cum/flat: Sort entries based on cumulative or flat data |
+ call_tree: Build context-sensitive call trees |
+ nodecount: Max number of entries to display |
+ nodefraction: Min frequency ratio of nodes to display |
+ edgefraction: Min frequency ratio of edges to display |
+ focus/ignore: Regexp to include/exclude samples by name/file |
+ tagfocus/tagignore: Regexp or value range to filter samples by tag |
+ eg "1mb", "1mb:2mb", ":64kb" |
+ |
+ functions: Level of aggregation for sample data |
+ files: |
+ lines: |
+ addresses: |
+ |
+ unit: Measurement unit to use on reports |
+ |
+ Sample value selection by index: |
+ sample_index: Index of sample value to display |
+ mean: Average sample value over first value |
+ |
+ Sample value selection by name: |
+ alloc_space for heap profiles |
+ alloc_objects |
+ inuse_space |
+ inuse_objects |
+ |
+ total_delay for contention profiles |
+ mean_delay |
+ contentions |
+ |
+ : Clear focus/ignore/hide/tagfocus/tagignore` |
+ |
+ ui.Print(help) |
+ return nil |
+} |
+ |
+// cmdFlags parses the options of an interactive command and returns |
+// an updated flags object. |
+func cmdFlags(prof *profile.Profile, input []string, ui plugin.UI, f *flags) (*flags, error) { |
+ cf := *f |
+ |
+ var focus, ignore string |
+ output := *cf.flagOutput |
+ nodeCount := *cf.flagNodeCount |
+ cmd := input[0] |
+ |
+ // Update output flags based on parameters. |
+ tokens := input[1:] |
+ for p := 0; p < len(tokens); p++ { |
+ t := tokens[p] |
+ if t == "" { |
+ continue |
+ } |
+ if c, err := strconv.ParseInt(t, 10, 32); err == nil { |
+ nodeCount = int(c) |
+ continue |
+ } |
+ switch t[0] { |
+ case '>': |
+ if len(t) > 1 { |
+ output = t[1:] |
+ continue |
+ } |
+ // find next token |
+ for p++; p < len(tokens); p++ { |
+ if tokens[p] != "" { |
+ output = tokens[p] |
+ break |
+ } |
+ } |
+ case '-': |
+ if t == "--cum" || t == "-cum" { |
+ cf.flagCum = newBool(true) |
+ continue |
+ } |
+ ignore = catRegex(ignore, t[1:]) |
+ default: |
+ focus = catRegex(focus, t) |
+ } |
+ } |
+ |
+ pcmd, ok := f.commands[cmd] |
+ if !ok { |
+ return nil, fmt.Errorf("Unexpected parse failure: %v", input) |
+ } |
+ // Reset flags |
+ cf.flagCommands = make(map[string]*bool) |
+ cf.flagParamCommands = make(map[string]*string) |
+ |
+ if !pcmd.HasParam { |
+ cf.flagCommands[cmd] = newBool(true) |
+ |
+ switch cmd { |
+ case "tags": |
+ cf.flagTagFocus = newString(focus) |
+ cf.flagTagIgnore = newString(ignore) |
+ default: |
+ cf.flagFocus = newString(catRegex(*cf.flagFocus, focus)) |
+ cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore)) |
+ } |
+ } else { |
+ if focus == "" { |
+ focus = "." |
+ } |
+ cf.flagParamCommands[cmd] = newString(focus) |
+ cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore)) |
+ } |
+ |
+ if nodeCount < 0 { |
+ switch cmd { |
+ case "text", "top": |
+ // Default text/top to 10 nodes on interactive mode |
+ nodeCount = 10 |
+ default: |
+ nodeCount = 80 |
+ } |
+ } |
+ |
+ cf.flagNodeCount = newInt(nodeCount) |
+ cf.flagOutput = newString(output) |
+ |
+ // Do regular flags processing |
+ if err := processFlags(prof, ui, &cf); err != nil { |
+ cf.usage(ui) |
+ return nil, err |
+ } |
+ |
+ return &cf, nil |
+} |
+ |
+func catRegex(a, b string) string { |
+ if a == "" { |
+ return b |
+ } |
+ if b == "" { |
+ return a |
+ } |
+ return a + "|" + b |
+} |
+ |
+// optFlags parses an interactive option setting and returns |
+// an updated flags object. |
+func optFlags(p *profile.Profile, input string, f *flags) (*flags, error) { |
+ inputs := strings.SplitN(input, "=", 2) |
+ option := strings.ToLower(strings.TrimSpace(inputs[0])) |
+ var value string |
+ if len(inputs) == 2 { |
+ value = strings.TrimSpace(inputs[1]) |
+ } |
+ |
+ of := *f |
+ |
+ var err error |
+ var bv bool |
+ var uv uint64 |
+ var fv float64 |
+ |
+ switch option { |
+ case "cum": |
+ if bv, err = parseBool(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagCum = newBool(bv) |
+ case "flat": |
+ if bv, err = parseBool(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagCum = newBool(!bv) |
+ case "call_tree": |
+ if bv, err = parseBool(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagCallTree = newBool(bv) |
+ case "unit": |
+ of.flagDisplayUnit = newString(value) |
+ case "sample_index": |
+ if uv, err = strconv.ParseUint(value, 10, 32); err != nil { |
+ return nil, err |
+ } |
+ if ix := int(uv); ix < 0 || ix >= len(p.SampleType) { |
+ return nil, fmt.Errorf("sample_index out of range [0..%d]", len(p.SampleType)-1) |
+ } |
+ of.flagSampleIndex = newInt(int(uv)) |
+ case "mean": |
+ if bv, err = parseBool(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagMean = newBool(bv) |
+ case "nodecount": |
+ if uv, err = strconv.ParseUint(value, 10, 32); err != nil { |
+ return nil, err |
+ } |
+ of.flagNodeCount = newInt(int(uv)) |
+ case "nodefraction": |
+ if fv, err = strconv.ParseFloat(value, 64); err != nil { |
+ return nil, err |
+ } |
+ of.flagNodeFraction = newFloat64(fv) |
+ case "edgefraction": |
+ if fv, err = strconv.ParseFloat(value, 64); err != nil { |
+ return nil, err |
+ } |
+ of.flagEdgeFraction = newFloat64(fv) |
+ case "focus": |
+ if err = validateRegex(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagFocus = newString(value) |
+ case "ignore": |
+ if err = validateRegex(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagIgnore = newString(value) |
+ case "tagfocus": |
+ if err = validateRegex(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagTagFocus = newString(value) |
+ case "tagignore": |
+ if err = validateRegex(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagTagIgnore = newString(value) |
+ case "hide": |
+ if err = validateRegex(value); err != nil { |
+ return nil, err |
+ } |
+ of.flagHide = newString(value) |
+ case "addresses", "files", "lines", "functions": |
+ if bv, err = parseBool(value); err != nil { |
+ return nil, err |
+ } |
+ if !bv { |
+ return nil, fmt.Errorf("select one of addresses/files/lines/functions") |
+ } |
+ setGranularityToggle(option, &of) |
+ default: |
+ if ix := findSampleIndex(p, "", option); ix >= 0 { |
+ of.flagSampleIndex = newInt(ix) |
+ } else if ix := findSampleIndex(p, "total_", option); ix >= 0 { |
+ of.flagSampleIndex = newInt(ix) |
+ of.flagMean = newBool(false) |
+ } else if ix := findSampleIndex(p, "mean_", option); ix >= 1 { |
+ of.flagSampleIndex = newInt(ix) |
+ of.flagMean = newBool(true) |
+ } else { |
+ return nil, fmt.Errorf("unrecognized command: %s", input) |
+ } |
+ } |
+ return &of, nil |
+} |
+ |
+// parseBool parses a string as a boolean value. |
+func parseBool(v string) (bool, error) { |
+ switch strings.ToLower(v) { |
+ case "true", "t", "yes", "y", "1", "": |
+ return true, nil |
+ case "false", "f", "no", "n", "0": |
+ return false, nil |
+ } |
+ return false, fmt.Errorf(`illegal input "%s" for bool value`, v) |
+} |
+ |
+func findSampleIndex(p *profile.Profile, prefix, sampleType string) int { |
+ if !strings.HasPrefix(sampleType, prefix) { |
+ return -1 |
+ } |
+ sampleType = strings.TrimPrefix(sampleType, prefix) |
+ for i, r := range p.SampleType { |
+ if r.Type == sampleType { |
+ return i |
+ } |
+ } |
+ return -1 |
+} |
+ |
+// setGranularityToggle manages the set of granularity options. These |
+// operate as a toggle; turning one on turns the others off. |
+func setGranularityToggle(o string, fl *flags) { |
+ t, f := newBool(true), newBool(false) |
+ fl.flagFunctions = f |
+ fl.flagFiles = f |
+ fl.flagLines = f |
+ fl.flagAddresses = f |
+ switch o { |
+ case "functions": |
+ fl.flagFunctions = t |
+ case "files": |
+ fl.flagFiles = t |
+ case "lines": |
+ fl.flagLines = t |
+ case "addresses": |
+ fl.flagAddresses = t |
+ default: |
+ panic(fmt.Errorf("unexpected option %s", o)) |
+ } |
+} |