// 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" "internal/pprof/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)) } }