| // 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 <f>*total"}, |
| "edgefraction": &variable{floatKind, "0.001", "", "Hide edges below <f>*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.", |
| "Takes into account the filename/lineno 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 function level.", |
| "Includes functions' addresses in the output.")}, |
| "noinlines": &variable{boolKind, "f", "granularity", helpText( |
| "Aggregate at the function level.", |
| "Attributes inlined functions to their first out-of-line caller.")}, |
| "addressnoinlines": &variable{boolKind, "f", "granularity", helpText( |
| "Aggregate at the function level, including functions' addresses in the output.", |
| "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 + "<func_regex|address> [-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 { |
| cmds := []string{"chrome", "google-chrome", "firefox"} |
| switch runtime.GOOS { |
| case "darwin": |
| return append(cmds, "/usr/bin/open") |
| case "windows": |
| return append(cmds, "cmd /c start") |
| default: |
| userBrowser := os.Getenv("BROWSER") |
| if userBrowser != "" { |
| cmds = append([]string{userBrowser, "sensible-browser"}, cmds...) |
| } else { |
| cmds = append([]string{"sensible-browser"}, cmds...) |
| } |
| return append(cmds, "xdg-open") |
| } |
| } |
| |
| 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 |
| } |