summaryrefslogtreecommitdiff
path: root/src/cmd/pprof/internal/commands/commands.go
blob: 51397a3c60db55db5ca8105f9bacdfe3530f7e52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// 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 commands defines and manages the basic pprof commands
package commands

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"runtime"
	"strings"

	"cmd/pprof/internal/plugin"
	"cmd/pprof/internal/report"
	"cmd/pprof/internal/svg"
	"cmd/pprof/internal/tempfile"
)

// 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 {
	Complete    Completer     // autocomplete for interactive mode
	Format      int           // report format to generate
	PostProcess PostProcessor // postprocessing to run on report
	HasParam    bool          // Collect a parameter from the CLI
	Usage       string        // Help text
}

// Completer is a function for command-line autocompletion
type Completer func(prefix string) string

// PostProcessor is a function that applies post-processing to the report output
type PostProcessor func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error

// PProf returns the basic pprof report-generation commands
func PProf(c Completer, interactive **bool, svgpan **string) Commands {
	return Commands{
		// Commands that require no post-processing.
		"tags":   {nil, report.Tags, nil, false, "Outputs all tags in the profile"},
		"raw":    {c, report.Raw, nil, false, "Outputs a text representation of the raw profile"},
		"dot":    {c, report.Dot, nil, false, "Outputs a graph in DOT format"},
		"top":    {c, report.Text, nil, false, "Outputs top entries in text form"},
		"tree":   {c, report.Tree, nil, false, "Outputs a text rendering of call graph"},
		"text":   {c, report.Text, nil, false, "Outputs top entries in text form"},
		"disasm": {c, report.Dis, nil, true, "Output annotated assembly for functions matching regexp or address"},
		"list":   {c, report.List, nil, true, "Output annotated source for functions matching regexp"},
		"peek":   {c, report.Tree, nil, true, "Output callers/callees of functions matching regexp"},

		// Save binary formats to a file
		"callgrind": {c, report.Callgrind, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format"},
		"proto":     {c, report.Proto, awayFromTTY("pb.gz"), false, "Outputs the profile in compressed protobuf format"},

		// Generate report in DOT format and postprocess with dot
		"gif": {c, report.Dot, invokeDot("gif"), false, "Outputs a graph image in GIF format"},
		"pdf": {c, report.Dot, invokeDot("pdf"), false, "Outputs a graph in PDF format"},
		"png": {c, report.Dot, invokeDot("png"), false, "Outputs a graph image in PNG format"},
		"ps":  {c, report.Dot, invokeDot("ps"), false, "Outputs a graph in PS format"},

		// Save SVG output into a file after including svgpan library
		"svg": {c, report.Dot, saveSVGToFile(svgpan), false, "Outputs a graph in SVG format"},

		// Visualize postprocessed dot output
		"eog":    {c, report.Dot, invokeVisualizer(interactive, invokeDot("svg"), "svg", []string{"eog"}), false, "Visualize graph through eog"},
		"evince": {c, report.Dot, invokeVisualizer(interactive, invokeDot("pdf"), "pdf", []string{"evince"}), false, "Visualize graph through evince"},
		"gv":     {c, report.Dot, invokeVisualizer(interactive, invokeDot("ps"), "ps", []string{"gv --noantialias"}), false, "Visualize graph through gv"},
		"web":    {c, report.Dot, invokeVisualizer(interactive, saveSVGToFile(svgpan), "svg", browsers()), false, "Visualize graph through web browser"},

		// Visualize HTML directly generated by report.
		"weblist": {c, report.WebList, invokeVisualizer(interactive, awayFromTTY("html"), "html", browsers()), true, "Output annotated source in HTML for functions matching regexp or address"},
	}
}

// browsers returns a list of commands to attempt for web visualization
// on the current platform
func browsers() []string {
	cmds := []string{"chrome", "google-chrome", "firefox"}
	switch runtime.GOOS {
	case "darwin":
		cmds = append(cmds, "/usr/bin/open")
	case "windows":
		cmds = append(cmds, "cmd /c start")
	default:
		cmds = append(cmds, "xdg-open")
	}
	return cmds
}

// NewCompleter creates an autocompletion function for a set of commands.
func NewCompleter(cs Commands) Completer {
	return func(line string) string {
		switch tokens := strings.Fields(line); len(tokens) {
		case 0:
			// Nothing to complete
		case 1:
			// Single token -- complete command name
			found := ""
			for c := range cs {
				if strings.HasPrefix(c, tokens[0]) {
					if found != "" {
						return line
					}
					found = c
				}
			}
			if found != "" {
				return found
			}
		default:
			// Multiple tokens -- complete using command completer
			if c, ok := cs[tokens[0]]; ok {
				if c.Complete != nil {
					lastTokenIdx := len(tokens) - 1
					lastToken := tokens[lastTokenIdx]
					if strings.HasPrefix(lastToken, "-") {
						lastToken = "-" + c.Complete(lastToken[1:])
					} else {
						lastToken = c.Complete(lastToken)
					}
					return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ")
				}
			}
		}
		return line
	}
}

// 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 *bytes.Buffer, output io.Writer, ui plugin.UI) error {
		if output == os.Stdout && ui.IsTerminal() {
			tempFile, err := tempfile.New("", "profile", "."+format)
			if err != nil {
				return err
			}
			ui.PrintErr("Generating report in ", tempFile.Name())
			_, err = fmt.Fprint(tempFile, input)
			return err
		}
		_, err := fmt.Fprint(output, input)
		return err
	}
}

func invokeDot(format string) PostProcessor {
	divert := awayFromTTY(format)
	return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error {
		if _, err := exec.LookPath("dot"); err != nil {
			ui.PrintErr("Cannot find dot, have you installed Graphviz?")
			return err
		}
		cmd := exec.Command("dot", "-T"+format)
		var buf bytes.Buffer
		cmd.Stdin, cmd.Stdout, cmd.Stderr = input, &buf, os.Stderr
		if err := cmd.Run(); err != nil {
			return err
		}
		return divert(&buf, output, ui)
	}
}

func saveSVGToFile(svgpan **string) PostProcessor {
	generateSVG := invokeDot("svg")
	divert := awayFromTTY("svg")
	return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error {
		baseSVG := &bytes.Buffer{}
		generateSVG(input, baseSVG, ui)
		massaged := &bytes.Buffer{}
		fmt.Fprint(massaged, svg.Massage(*baseSVG, **svgpan))
		return divert(massaged, output, ui)
	}
}

func invokeVisualizer(interactive **bool, format PostProcessor, suffix string, visualizers []string) PostProcessor {
	return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error {
		tempFile, err := tempfile.New(os.Getenv("PPROF_TMPDIR"), "pprof", "."+suffix)
		if err != nil {
			return err
		}
		tempfile.DeferDelete(tempFile.Name())
		if err = format(input, tempFile, ui); err != nil {
			return err
		}
		tempFile.Close() // on windows, if the file is Open, start cannot access it.
		// 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 {
				if !**interactive {
					// In command-line mode, wait for the viewer to be closed
					// before proceeding
					return viewer.Wait()
				}
				return nil
			}
		}
		return err
	}
}