blob: e6e865f385d7083503f012879e9dc4c4689bf202 [file] [log] [blame]
// 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 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
}