1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package commands defines and manages the basic pprof commands 6 package commands 7 8 import ( 9 "bytes" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "os/exec" 15 "strings" 16 "time" 17 18 "cmd/internal/browser" 19 "cmd/pprof/internal/plugin" 20 "cmd/pprof/internal/report" 21 "cmd/pprof/internal/svg" 22 "cmd/pprof/internal/tempfile" 23 ) 24 25 // Commands describes the commands accepted by pprof. 26 type Commands map[string]*Command 27 28 // Command describes the actions for a pprof command. Includes a 29 // function for command-line completion, the report format to use 30 // during report generation, any postprocessing functions, and whether 31 // the command expects a regexp parameter (typically a function name). 32 type Command struct { 33 Complete Completer // autocomplete for interactive mode 34 Format int // report format to generate 35 PostProcess PostProcessor // postprocessing to run on report 36 HasParam bool // Collect a parameter from the CLI 37 Usage string // Help text 38 } 39 40 // Completer is a function for command-line autocompletion 41 type Completer func(prefix string) string 42 43 // PostProcessor is a function that applies post-processing to the report output 44 type PostProcessor func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error 45 46 // PProf returns the basic pprof report-generation commands 47 func PProf(c Completer, interactive **bool) Commands { 48 return Commands{ 49 // Commands that require no post-processing. 50 "tags": {nil, report.Tags, nil, false, "Outputs all tags in the profile"}, 51 "raw": {c, report.Raw, nil, false, "Outputs a text representation of the raw profile"}, 52 "dot": {c, report.Dot, nil, false, "Outputs a graph in DOT format"}, 53 "top": {c, report.Text, nil, false, "Outputs top entries in text form"}, 54 "tree": {c, report.Tree, nil, false, "Outputs a text rendering of call graph"}, 55 "text": {c, report.Text, nil, false, "Outputs top entries in text form"}, 56 "disasm": {c, report.Dis, nil, true, "Output annotated assembly for functions matching regexp or address"}, 57 "list": {c, report.List, nil, true, "Output annotated source for functions matching regexp"}, 58 "peek": {c, report.Tree, nil, true, "Output callers/callees of functions matching regexp"}, 59 60 // Save binary formats to a file 61 "callgrind": {c, report.Callgrind, awayFromTTY(interactive, "callgraph.out"), false, "Outputs a graph in callgrind format"}, 62 "proto": {c, report.Proto, awayFromTTY(interactive, "pb.gz"), false, "Outputs the profile in compressed protobuf format"}, 63 64 // Generate report in DOT format and postprocess with dot 65 "gif": {c, report.Dot, invokeDot(interactive, "gif"), false, "Outputs a graph image in GIF format"}, 66 "pdf": {c, report.Dot, invokeDot(interactive, "pdf"), false, "Outputs a graph in PDF format"}, 67 "png": {c, report.Dot, invokeDot(interactive, "png"), false, "Outputs a graph image in PNG format"}, 68 "ps": {c, report.Dot, invokeDot(interactive, "ps"), false, "Outputs a graph in PS format"}, 69 70 // Save SVG output into a file after including svgpan library 71 "svg": {c, report.Dot, saveSVGToFile(interactive), false, "Outputs a graph in SVG format"}, 72 73 // Visualize postprocessed dot output 74 "eog": {c, report.Dot, invokeVisualizer(interactive, invokeDot(nil, "svg"), "svg", []string{"eog"}), false, "Visualize graph through eog"}, 75 "evince": {c, report.Dot, invokeVisualizer(interactive, invokeDot(nil, "pdf"), "pdf", []string{"evince"}), false, "Visualize graph through evince"}, 76 "gv": {c, report.Dot, invokeVisualizer(interactive, invokeDot(nil, "ps"), "ps", []string{"gv --noantialias"}), false, "Visualize graph through gv"}, 77 "web": {c, report.Dot, invokeVisualizer(interactive, saveSVGToFile(nil), "svg", browsers()), false, "Visualize graph through web browser"}, 78 79 // Visualize HTML directly generated by report. 80 "weblist": {c, report.WebList, invokeVisualizer(interactive, awayFromTTY(nil, "html"), "html", browsers()), true, "Output annotated source in HTML for functions matching regexp or address"}, 81 } 82 } 83 84 // browsers returns a list of commands to attempt for web visualization 85 // on the current platform 86 func browsers() []string { 87 var cmds []string 88 for _, cmd := range browser.Commands() { 89 cmds = append(cmds, strings.Join(cmd, " ")) 90 } 91 return cmds 92 } 93 94 // NewCompleter creates an autocompletion function for a set of commands. 95 func NewCompleter(cs Commands) Completer { 96 return func(line string) string { 97 switch tokens := strings.Fields(line); len(tokens) { 98 case 0: 99 // Nothing to complete 100 case 1: 101 // Single token -- complete command name 102 found := "" 103 for c := range cs { 104 if strings.HasPrefix(c, tokens[0]) { 105 if found != "" { 106 return line 107 } 108 found = c 109 } 110 } 111 if found != "" { 112 return found 113 } 114 default: 115 // Multiple tokens -- complete using command completer 116 if c, ok := cs[tokens[0]]; ok { 117 if c.Complete != nil { 118 lastTokenIdx := len(tokens) - 1 119 lastToken := tokens[lastTokenIdx] 120 if strings.HasPrefix(lastToken, "-") { 121 lastToken = "-" + c.Complete(lastToken[1:]) 122 } else { 123 lastToken = c.Complete(lastToken) 124 } 125 return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ") 126 } 127 } 128 } 129 return line 130 } 131 } 132 133 // awayFromTTY saves the output in a file if it would otherwise go to 134 // the terminal screen. This is used to avoid dumping binary data on 135 // the screen. 136 func awayFromTTY(interactive **bool, format string) PostProcessor { 137 return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 138 if output == os.Stdout && (ui.IsTerminal() || interactive != nil && **interactive) { 139 tempFile, err := tempfile.New("", "profile", "."+format) 140 if err != nil { 141 return err 142 } 143 ui.PrintErr("Generating report in ", tempFile.Name()) 144 _, err = fmt.Fprint(tempFile, input) 145 return err 146 } 147 _, err := fmt.Fprint(output, input) 148 return err 149 } 150 } 151 152 func invokeDot(interactive **bool, format string) PostProcessor { 153 divert := awayFromTTY(interactive, format) 154 return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 155 if _, err := exec.LookPath("dot"); err != nil { 156 ui.PrintErr("Cannot find dot, have you installed Graphviz?") 157 return err 158 } 159 cmd := exec.Command("dot", "-T"+format) 160 var buf bytes.Buffer 161 cmd.Stdin, cmd.Stdout, cmd.Stderr = input, &buf, os.Stderr 162 if err := cmd.Run(); err != nil { 163 return err 164 } 165 return divert(&buf, output, ui) 166 } 167 } 168 169 func saveSVGToFile(interactive **bool) PostProcessor { 170 generateSVG := invokeDot(nil, "svg") 171 divert := awayFromTTY(interactive, "svg") 172 return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 173 baseSVG := &bytes.Buffer{} 174 generateSVG(input, baseSVG, ui) 175 massaged := &bytes.Buffer{} 176 fmt.Fprint(massaged, svg.Massage(*baseSVG)) 177 return divert(massaged, output, ui) 178 } 179 } 180 181 var vizTmpDir string 182 183 func makeVizTmpDir() error { 184 if vizTmpDir != "" { 185 return nil 186 } 187 name, err := ioutil.TempDir("", "pprof-") 188 if err != nil { 189 return err 190 } 191 tempfile.DeferDelete(name) 192 vizTmpDir = name 193 return nil 194 } 195 196 func invokeVisualizer(interactive **bool, format PostProcessor, suffix string, visualizers []string) PostProcessor { 197 return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 198 if err := makeVizTmpDir(); err != nil { 199 return err 200 } 201 tempFile, err := tempfile.New(vizTmpDir, "pprof", "."+suffix) 202 if err != nil { 203 return err 204 } 205 tempfile.DeferDelete(tempFile.Name()) 206 if err = format(input, tempFile, ui); err != nil { 207 return err 208 } 209 tempFile.Close() // on windows, if the file is Open, start cannot access it. 210 // Try visualizers until one is successful 211 for _, v := range visualizers { 212 // Separate command and arguments for exec.Command. 213 args := strings.Split(v, " ") 214 if len(args) == 0 { 215 continue 216 } 217 viewer := exec.Command(args[0], append(args[1:], tempFile.Name())...) 218 viewer.Stderr = os.Stderr 219 if err = viewer.Start(); err == nil { 220 // The viewer might just send a message to another program 221 // to open the file. Give that program a little time to open the 222 // file before we remove it. 223 time.Sleep(1 * time.Second) 224 225 if !**interactive { 226 // In command-line mode, wait for the viewer to be closed 227 // before proceeding 228 return viewer.Wait() 229 } 230 return nil 231 } 232 } 233 return err 234 } 235 } 236