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