| // 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)) |
| } |
| } |