// 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 ( "bytes" "fmt" "io" "os" "os/exec" "runtime" "sort" "strconv" "strings" "time" "github.com/google/pprof/internal/plugin" "github.com/google/pprof/internal/report" ) // commands describes the commands accepted by pprof. type commands map[string]*command // command describes the actions for a pprof command. Includes a // function for command-line completion, the report format to use // during report generation, any postprocessing functions, and whether // the command expects a regexp parameter (typically a function name). type command struct { format int // report format to generate postProcess PostProcessor // postprocessing to run on report visualizer PostProcessor // display output using some callback hasParam bool // collect a parameter from the CLI description string // single-line description text saying what the command does usage string // multi-line help text saying how the command is used } // help returns a help string for a command. func (c *command) help(name string) string { message := c.description + "\n" if c.usage != "" { message += " Usage:\n" lines := strings.Split(c.usage, "\n") for _, line := range lines { message += fmt.Sprintf(" %s\n", line) } } return message + "\n" } // AddCommand adds an additional command to the set of commands // accepted by pprof. This enables extensions to add new commands for // specialized visualization formats. If the command specified already // exists, it is overwritten. func AddCommand(cmd string, format int, post PostProcessor, desc, usage string) { pprofCommands[cmd] = &command{format, post, nil, false, desc, usage} } // SetVariableDefault sets the default value for a pprof // variable. This enables extensions to set their own defaults. func SetVariableDefault(variable, value string) { if v := pprofVariables[variable]; v != nil { v.value = value } } // PostProcessor is a function that applies post-processing to the report output type PostProcessor func(input io.Reader, output io.Writer, ui plugin.UI) error // interactiveMode is true if pprof is running on interactive mode, reading // commands from its shell. var interactiveMode = false // pprofCommands are the report generation commands recognized by pprof. var pprofCommands = commands{ // Commands that require no post-processing. "comments": {report.Comments, nil, nil, false, "Output all profile comments", ""}, "disasm": {report.Dis, nil, nil, true, "Output assembly listings annotated with samples", listHelp("disasm", true)}, "dot": {report.Dot, nil, nil, false, "Outputs a graph in DOT format", reportHelp("dot", false, true)}, "list": {report.List, nil, nil, true, "Output annotated source for functions matching regexp", listHelp("list", false)}, "peek": {report.Tree, nil, nil, true, "Output callers/callees of functions matching regexp", "peek func_regex\nDisplay callers and callees of functions matching func_regex."}, "raw": {report.Raw, nil, nil, false, "Outputs a text representation of the raw profile", ""}, "tags": {report.Tags, nil, nil, false, "Outputs all tags in the profile", "tags [tag_regex]* [-ignore_regex]* [>file]\nList tags with key:value matching tag_regex and exclude ignore_regex."}, "text": {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("text", true, true)}, "top": {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("top", true, true)}, "traces": {report.Traces, nil, nil, false, "Outputs all profile samples in text form", ""}, "tree": {report.Tree, nil, nil, false, "Outputs a text rendering of call graph", reportHelp("tree", true, true)}, // Save binary formats to a file "callgrind": {report.Callgrind, nil, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format", reportHelp("callgrind", false, true)}, "proto": {report.Proto, nil, awayFromTTY("pb.gz"), false, "Outputs the profile in compressed protobuf format", ""}, "topproto": {report.TopProto, nil, awayFromTTY("pb.gz"), false, "Outputs top entries in compressed protobuf format", ""}, // Generate report in DOT format and postprocess with dot "gif": {report.Dot, invokeDot("gif"), awayFromTTY("gif"), false, "Outputs a graph image in GIF format", reportHelp("gif", false, true)}, "pdf": {report.Dot, invokeDot("pdf"), awayFromTTY("pdf"), false, "Outputs a graph in PDF format", reportHelp("pdf", false, true)}, "png": {report.Dot, invokeDot("png"), awayFromTTY("png"), false, "Outputs a graph image in PNG format", reportHelp("png", false, true)}, "ps": {report.Dot, invokeDot("ps"), awayFromTTY("ps"), false, "Outputs a graph in PS format", reportHelp("ps", false, true)}, // Save SVG output into a file "svg": {report.Dot, massageDotSVG(), awayFromTTY("svg"), false, "Outputs a graph in SVG format", reportHelp("svg", false, true)}, // Visualize postprocessed dot output "eog": {report.Dot, invokeDot("svg"), invokeVisualizer("svg", []string{"eog"}), false, "Visualize graph through eog", reportHelp("eog", false, false)}, "evince": {report.Dot, invokeDot("pdf"), invokeVisualizer("pdf", []string{"evince"}), false, "Visualize graph through evince", reportHelp("evince", false, false)}, "gv": {report.Dot, invokeDot("ps"), invokeVisualizer("ps", []string{"gv --noantialias"}), false, "Visualize graph through gv", reportHelp("gv", false, false)}, "web": {report.Dot, massageDotSVG(), invokeVisualizer("svg", browsers()), false, "Visualize graph through web browser", reportHelp("web", false, false)}, // Visualize callgrind output "kcachegrind": {report.Callgrind, nil, invokeVisualizer("grind", kcachegrind), false, "Visualize report in KCachegrind", reportHelp("kcachegrind", false, false)}, // Visualize HTML directly generated by report. "weblist": {report.WebList, nil, invokeVisualizer("html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)}, } // pprofVariables are the configuration parameters that affect the // reported generated by pprof. var pprofVariables = variables{ // Filename for file-based output formats, stdout by default. "output": &variable{stringKind, "", "", helpText("Output filename for file-based outputs")}, // Comparisons. "drop_negative": &variable{boolKind, "f", "", helpText( "Ignore negative differences", "Do not show any locations with values <0.")}, // Graph handling options. "call_tree": &variable{boolKind, "f", "", helpText( "Create a context-sensitive call tree", "Treat locations reached through different paths as separate.")}, // Display options. "relative_percentages": &variable{boolKind, "f", "", helpText( "Show percentages relative to focused subgraph", "If unset, percentages are relative to full graph before focusing", "to facilitate comparison with original graph.")}, "unit": &variable{stringKind, "minimum", "", helpText( "Measurement units to display", "Scale the sample values to this unit.", "For time-based profiles, use seconds, milliseconds, nanoseconds, etc.", "For memory profiles, use megabytes, kilobytes, bytes, etc.", "Using auto will scale each value independently to the most natural unit.")}, "compact_labels": &variable{boolKind, "f", "", "Show minimal headers"}, "source_path": &variable{stringKind, "", "", "Search path for source files"}, "trim_path": &variable{stringKind, "", "", "Path to trim from source paths before search"}, // Filtering options "nodecount": &variable{intKind, "-1", "", helpText( "Max number of nodes to show", "Uses heuristics to limit the number of locations to be displayed.", "On graphs, dotted edges represent paths through nodes that have been removed.")}, "nodefraction": &variable{floatKind, "0.005", "", "Hide nodes below *total"}, "edgefraction": &variable{floatKind, "0.001", "", "Hide edges below *total"}, "trim": &variable{boolKind, "t", "", helpText( "Honor nodefraction/edgefraction/nodecount defaults", "Set to false to get the full profile, without any trimming.")}, "focus": &variable{stringKind, "", "", helpText( "Restricts to samples going through a node matching regexp", "Discard samples that do not include a node matching this regexp.", "Matching includes the function name, filename or object name.")}, "ignore": &variable{stringKind, "", "", helpText( "Skips paths going through any nodes matching regexp", "If set, discard samples that include a node matching this regexp.", "Matching includes the function name, filename or object name.")}, "prune_from": &variable{stringKind, "", "", helpText( "Drops any functions below the matched frame.", "If set, any frames matching the specified regexp and any frames", "below it will be dropped from each sample.")}, "hide": &variable{stringKind, "", "", helpText( "Skips nodes matching regexp", "Discard nodes that match this location.", "Other nodes from samples that include this location will be shown.", "Matching includes the function name, filename or object name.")}, "show": &variable{stringKind, "", "", helpText( "Only show nodes matching regexp", "If set, only show nodes that match this location.", "Matching includes the function name, filename or object name.")}, "show_from": &variable{stringKind, "", "", helpText( "Drops functions above the highest matched frame.", "If set, all frames above the highest match are dropped from every sample.", "Matching includes the function name, filename or object name.")}, "tagfocus": &variable{stringKind, "", "", helpText( "Restricts to samples with tags in range or matched by regexp", "Use name=value syntax to limit the matching to a specific tag.", "Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:", "String tag filter examples: foo, foo.*bar, mytag=foo.*bar")}, "tagignore": &variable{stringKind, "", "", helpText( "Discard samples with tags in range or matched by regexp", "Use name=value syntax to limit the matching to a specific tag.", "Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:", "String tag filter examples: foo, foo.*bar, mytag=foo.*bar")}, "tagshow": &variable{stringKind, "", "", helpText( "Only consider tags matching this regexp", "Discard tags that do not match this regexp")}, "taghide": &variable{stringKind, "", "", helpText( "Skip tags matching this regexp", "Discard tags that match this regexp")}, // Heap profile options "divide_by": &variable{floatKind, "1", "", helpText( "Ratio to divide all samples before visualization", "Divide all samples values by a constant, eg the number of processors or jobs.")}, "mean": &variable{boolKind, "f", "", helpText( "Average sample value over first value (count)", "For memory profiles, report average memory per allocation.", "For time-based profiles, report average time per event.")}, "sample_index": &variable{stringKind, "", "", helpText( "Sample value to report (0-based index or name)", "Profiles contain multiple values per sample.", "Use sample_index=i to select the ith value (starting at 0).")}, "normalize": &variable{boolKind, "f", "", helpText( "Scales profile based on the base profile.")}, // Data sorting criteria "flat": &variable{boolKind, "t", "cumulative", helpText("Sort entries based on own weight")}, "cum": &variable{boolKind, "f", "cumulative", helpText("Sort entries based on cumulative weight")}, // Output granularity "functions": &variable{boolKind, "t", "granularity", helpText( "Aggregate at the function level.", "Ignores the filename where the function was defined.")}, "filefunctions": &variable{boolKind, "t", "granularity", helpText( "Aggregate at the function level.", "Takes into account the filename where the function was defined.")}, "files": &variable{boolKind, "f", "granularity", "Aggregate at the file level."}, "lines": &variable{boolKind, "f", "granularity", "Aggregate at the source code line level."}, "addresses": &variable{boolKind, "f", "granularity", helpText( "Aggregate at the address level.", "Includes functions' addresses in the output.")}, "noinlines": &variable{boolKind, "f", "", helpText( "Ignore inlines.", "Attributes inlined functions to their first out-of-line caller.")}, } func helpText(s ...string) string { return strings.Join(s, "\n") + "\n" } // usage returns a string describing the pprof commands and variables. // if commandLine is set, the output reflect cli usage. func usage(commandLine bool) string { var prefix string if commandLine { prefix = "-" } fmtHelp := func(c, d string) string { return fmt.Sprintf(" %-16s %s", c, strings.SplitN(d, "\n", 2)[0]) } var commands []string for name, cmd := range pprofCommands { commands = append(commands, fmtHelp(prefix+name, cmd.description)) } sort.Strings(commands) var help string if commandLine { help = " Output formats (select at most one):\n" } else { help = " Commands:\n" commands = append(commands, fmtHelp("o/options", "List options and their current values")) commands = append(commands, fmtHelp("quit/exit/^D", "Exit pprof")) } help = help + strings.Join(commands, "\n") + "\n\n" + " Options:\n" // Print help for variables after sorting them. // Collect radio variables by their group name to print them together. radioOptions := make(map[string][]string) var variables []string for name, vr := range pprofVariables { if vr.group != "" { radioOptions[vr.group] = append(radioOptions[vr.group], name) continue } variables = append(variables, fmtHelp(prefix+name, vr.help)) } sort.Strings(variables) help = help + strings.Join(variables, "\n") + "\n\n" + " Option groups (only set one per group):\n" var radioStrings []string for radio, ops := range radioOptions { sort.Strings(ops) s := []string{fmtHelp(radio, "")} for _, op := range ops { s = append(s, " "+fmtHelp(prefix+op, pprofVariables[op].help)) } radioStrings = append(radioStrings, strings.Join(s, "\n")) } sort.Strings(radioStrings) return help + strings.Join(radioStrings, "\n") } func reportHelp(c string, cum, redirect bool) string { h := []string{ c + " [n] [focus_regex]* [-ignore_regex]*", "Include up to n samples", "Include samples matching focus_regex, and exclude ignore_regex.", } if cum { h[0] += " [-cum]" h = append(h, "-cum sorts the output by cumulative weight") } if redirect { h[0] += " >f" h = append(h, "Optionally save the report on the file f") } return strings.Join(h, "\n") } func listHelp(c string, redirect bool) string { h := []string{ c + " [-focus_regex]* [-ignore_regex]*", "Include functions matching func_regex, or including the address specified.", "Include samples matching focus_regex, and exclude ignore_regex.", } if redirect { h[0] += " >f" h = append(h, "Optionally save the report on the file f") } return strings.Join(h, "\n") } // browsers returns a list of commands to attempt for web visualization. func browsers() []string { var cmds []string if userBrowser := os.Getenv("BROWSER"); userBrowser != "" { cmds = append(cmds, userBrowser) } switch runtime.GOOS { case "darwin": cmds = append(cmds, "/usr/bin/open") case "windows": cmds = append(cmds, "cmd /c start") default: // Commands opening browsers are prioritized over xdg-open, so browser() // command can be used on linux to open the .svg file generated by the -web // command (the .svg file includes embedded javascript so is best viewed in // a browser). cmds = append(cmds, []string{"chrome", "google-chrome", "chromium", "firefox", "sensible-browser"}...) if os.Getenv("DISPLAY") != "" { // xdg-open is only for use in a desktop environment. cmds = append(cmds, "xdg-open") } } return cmds } var kcachegrind = []string{"kcachegrind"} // awayFromTTY saves the output in a file if it would otherwise go to // the terminal screen. This is used to avoid dumping binary data on // the screen. func awayFromTTY(format string) PostProcessor { return func(input io.Reader, output io.Writer, ui plugin.UI) error { if output == os.Stdout && (ui.IsTerminal() || interactiveMode) { tempFile, err := newTempFile("", "profile", "."+format) if err != nil { return err } ui.PrintErr("Generating report in ", tempFile.Name()) output = tempFile } _, err := io.Copy(output, input) return err } } func invokeDot(format string) PostProcessor { return func(input io.Reader, output io.Writer, ui plugin.UI) error { cmd := exec.Command("dot", "-T"+format) cmd.Stdin, cmd.Stdout, cmd.Stderr = input, output, os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to execute dot. Is Graphviz installed? Error: %v", err) } return nil } } // massageDotSVG invokes the dot tool to generate an SVG image and alters // the image to have panning capabilities when viewed in a browser. func massageDotSVG() PostProcessor { generateSVG := invokeDot("svg") return func(input io.Reader, output io.Writer, ui plugin.UI) error { baseSVG := new(bytes.Buffer) if err := generateSVG(input, baseSVG, ui); err != nil { return err } _, err := output.Write([]byte(massageSVG(baseSVG.String()))) return err } } func invokeVisualizer(suffix string, visualizers []string) PostProcessor { return func(input io.Reader, output io.Writer, ui plugin.UI) error { tempFile, err := newTempFile(os.TempDir(), "pprof", "."+suffix) if err != nil { return err } deferDeleteTempFile(tempFile.Name()) if _, err := io.Copy(tempFile, input); err != nil { return err } tempFile.Close() // Try visualizers until one is successful for _, v := range visualizers { // Separate command and arguments for exec.Command. args := strings.Split(v, " ") if len(args) == 0 { continue } viewer := exec.Command(args[0], append(args[1:], tempFile.Name())...) viewer.Stderr = os.Stderr if err = viewer.Start(); err == nil { // Wait for a second so that the visualizer has a chance to // open the input file. This needs to be done even if we're // waiting for the visualizer as it can be just a wrapper that // spawns a browser tab and returns right away. defer func(t <-chan time.Time) { <-t }(time.After(time.Second)) // On interactive mode, let the visualizer run in the background // so other commands can be issued. if !interactiveMode { return viewer.Wait() } return nil } } return err } } // variables describe the configuration parameters recognized by pprof. type variables map[string]*variable // variable is a single configuration parameter. type variable struct { kind int // How to interpret the value, must be one of the enums below. value string // Effective value. Only values appropriate for the Kind should be set. group string // boolKind variables with the same Group != "" cannot be set simultaneously. help string // Text describing the variable, in multiple lines separated by newline. } const ( // variable.kind must be one of these variables. boolKind = iota intKind floatKind stringKind ) // set updates the value of a variable, checking that the value is // suitable for the variable Kind. func (vars variables) set(name, value string) error { v := vars[name] if v == nil { return fmt.Errorf("no variable %s", name) } var err error switch v.kind { case boolKind: var b bool if b, err = stringToBool(value); err == nil { if v.group != "" && !b { err = fmt.Errorf("%q can only be set to true", name) } } case intKind: _, err = strconv.Atoi(value) case floatKind: _, err = strconv.ParseFloat(value, 64) case stringKind: // Remove quotes, particularly useful for empty values. if len(value) > 1 && strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) { value = value[1 : len(value)-1] } } if err != nil { return err } vars[name].value = value if group := vars[name].group; group != "" { for vname, vvar := range vars { if vvar.group == group && vname != name { vvar.value = "f" } } } return err } // boolValue returns the value of a boolean variable. func (v *variable) boolValue() bool { b, err := stringToBool(v.value) if err != nil { panic("unexpected value " + v.value + " for bool ") } return b } // intValue returns the value of an intKind variable. func (v *variable) intValue() int { i, err := strconv.Atoi(v.value) if err != nil { panic("unexpected value " + v.value + " for int ") } return i } // floatValue returns the value of a Float variable. func (v *variable) floatValue() float64 { f, err := strconv.ParseFloat(v.value, 64) if err != nil { panic("unexpected value " + v.value + " for float ") } return f } // stringValue returns a canonical representation for a variable. func (v *variable) stringValue() string { switch v.kind { case boolKind: return fmt.Sprint(v.boolValue()) case intKind: return fmt.Sprint(v.intValue()) case floatKind: return fmt.Sprint(v.floatValue()) } return v.value } func stringToBool(s string) (bool, error) { switch strings.ToLower(s) { case "true", "t", "yes", "y", "1", "": return true, nil case "false", "f", "no", "n", "0": return false, nil default: return false, fmt.Errorf(`illegal value "%s" for bool variable`, s) } } // makeCopy returns a duplicate of a set of shell variables. func (vars variables) makeCopy() variables { varscopy := make(variables, len(vars)) for n, v := range vars { vcopy := *v varscopy[n] = &vcopy } return varscopy }