Home | History | Annotate | Download | only in commands
      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