Skip to content

Commit f83a3d8

Browse files
ghemawataalexand
authored andcommittedJul 14, 2017
Added an interactive web interface (#154)
* Added an interactive web interface triggered by passing -http=port on the command line. The interface is available by visiting localhost:port in a browser. Requirements: * Graphviz must be installed. * Browser must support Javascript. * Tested in recent stable versions of chrome and firefox. Features: * The entry point is a dot graph display (equivalent to "web" output). * Nodes in the graph can be selected by clicking. * A regular expression can also be typed in for selection. * The current selection (either list of nodes or a regexp) can be focused, ignored, or hidden. * Source code or disassembly of the current selection can be displayed. * Remove unused function. * Skip graph generation test if graphviz is not installed. * Added -http port and the various modes of using pprof to the usage message. * Web interface now supports "show" option. * Web interface automatically opens the browser pointed at the page corresponding to command line arguments. * Some tweaks for firefox. * Handle review comments (better usage message, more testing). * Handled review comments: 1. Capture and display errors like "Focus expression matched no samples". 2. Re-ordered buttons to match other interfaces. 3. Use UI.PrintErr to print error messages. * Handle javascript code review comments (a bunch of cleanups). Also added pprof binary to .gitignore.
1 parent 5bd319a commit f83a3d8

File tree

12 files changed

+1128
-20
lines changed

12 files changed

+1128
-20
lines changed
 

‎.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
.*.swp
66
core
77
coverage.txt
8+
pprof

‎README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ This will open a simple shell that takes pprof commands to generate reports.
7777
Type 'help' for available commands/options.
7878
```
7979

80+
## Run pprof via a web interface
81+
82+
If the `-http=port` option is specified, pprof starts a web server at
83+
the specified port that provides an interactive web-based interface to pprof.
84+
85+
```
86+
pprof -http=[port] [main_binary] profile.pb.gz
87+
```
88+
89+
The preceding command should automatically open your web browser at
90+
the right page; if not, you can manually visit the specified port in
91+
your web browser.
92+
8093
## Using pprof with Linux Perf
8194

8295
pprof can read `perf.data` files generated by the

‎doc/pprof.md

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,40 @@ location. pprof is agnostic to the profile semantics, so other uses are
2929
possible. The interpretation of the reports generated by pprof depends on the
3030
semantics defined by the source of the profile.
3131

32-
# General usage
32+
# Usage Modes
33+
34+
There are few different ways of using `pprof`.
35+
36+
## Report generation
37+
38+
If a report format is requested on the command line:
39+
40+
pprof <format> [options] source
41+
42+
pprof will generate a report in the specified format and exit.
43+
Formats can be either text, or graphical. See below for details about
44+
supported formats, options, and sources.
45+
46+
## Interactive terminal use
47+
48+
Without a format specifier:
49+
50+
pprof [options] source
51+
52+
pprof will start an interactive shell in which the user can type
53+
commands. Type `help` to get online help.
54+
55+
## Web interface
56+
57+
If a port is specified on the command line:
58+
59+
pprof -http=<port> [options] source
60+
61+
pprof will start serving HTTP requests on the specified port. Visit
62+
the HTTP url corresponding to the port (typically `http://localhost:<port>/`)
63+
in a browser to see the interface.
64+
65+
# Details
3366

3467
The objective of pprof is to generate a report for a profile. The report is
3568
generated from a location hierarchy, which is reconstructed from the profile
@@ -38,14 +71,12 @@ itself, while *cum* is the value of the location plus all its
3871
descendants. Samples that include a location multiple times (eg for recursive
3972
functions) are counted only once per location.
4073

41-
The basic usage of pprof is
42-
43-
pprof <format> [options] source
74+
## Options
4475

45-
Where *format* selects the nature of the report, and *options* configure the
46-
contents of the report. Each option has a value, which can be boolean, numeric,
47-
or strings. While only one format can be specified, most options can be selected
48-
independently of each other.
76+
*options* configure the contents of a report. Each option has a value,
77+
which can be boolean, numeric, or strings. While only one format can
78+
be specified, most options can be selected independently of each
79+
other.
4980

5081
Some common pprof options are:
5182

‎internal/driver/cli.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type source struct {
3232
Seconds int
3333
Timeout int
3434
Symbolize string
35+
HTTPPort int
3536
}
3637

3738
// Parse parses the command lines through the specified flags package
@@ -58,6 +59,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
5859
flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames")
5960

6061
flagTimeout := flag.Int("timeout", -1, "Timeout in seconds for fetching a profile")
62+
flagHTTPPort := flag.Int("http", 0, "Present interactive web based UI at the specified http port")
6163

6264
// Flags used during command processing
6365
installedFlags := installFlags(flag)
@@ -106,6 +108,9 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
106108
if err != nil {
107109
return nil, nil, err
108110
}
111+
if cmd != nil && *flagHTTPPort != 0 {
112+
return nil, nil, fmt.Errorf("--http is not compatible with an output format on the command line")
113+
}
109114

110115
si := pprofVariables["sample_index"].value
111116
si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
@@ -128,6 +133,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
128133
Seconds: *flagSeconds,
129134
Timeout: *flagTimeout,
130135
Symbolize: *flagSymbolize,
136+
HTTPPort: *flagHTTPPort,
131137
}
132138

133139
for _, s := range *flagBase {
@@ -240,7 +246,25 @@ func outputFormat(bcmd map[string]*bool, acmd map[string]*string) (cmd []string,
240246
return cmd, nil
241247
}
242248

243-
var usageMsgHdr = "usage: pprof [options] [-base source] [binary] <source> ...\n"
249+
var usageMsgHdr = `usage:
250+
251+
Produce output in the specified format.
252+
253+
pprof <format> [options] [binary] <source> ...
254+
255+
Omit the format to get an interactive shell whose commands can be used
256+
to generate various views of a profile
257+
258+
pprof [options] [binary] <source> ...
259+
260+
Omit the format and provide the "-http" flag to get an interactive web
261+
interface at the specified port that can be used to navigate through
262+
various views of a profile.
263+
264+
pprof -http <port> [options] [binary] <source> ...
265+
266+
Details:
267+
`
244268

245269
var usageMsgSrc = "\n\n" +
246270
" Source options:\n" +
@@ -261,6 +285,7 @@ var usageMsgSrc = "\n\n" +
261285

262286
var usageMsgVars = "\n\n" +
263287
" Misc options:\n" +
288+
" -http port Provide web based interface at port\n" +
264289
" -tools Search path for object tools\n" +
265290
"\n" +
266291
" Environment Variables:\n" +

‎internal/driver/commands.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func usage(commandLine bool) string {
263263

264264
var help string
265265
if commandLine {
266-
help = " Output formats (select only one):\n"
266+
help = " Output formats (select at most one):\n"
267267
} else {
268268
help = " Commands:\n"
269269
commands = append(commands, fmtHelp("o/options", "List options and their current values"))

‎internal/driver/driver.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ func PProf(eo *plugin.Options) error {
5252
return generateReport(p, cmd, pprofVariables, o)
5353
}
5454

55+
if src.HTTPPort > 0 {
56+
return serveWebInterface(src.HTTPPort, p, o)
57+
}
5558
return interactive(p, o)
5659
}
5760

58-
func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
61+
func generateRawReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) (*command, *report.Report, error) {
5962
p = p.Copy() // Prevent modification to the incoming profile.
6063

6164
vars = applyCommandOverrides(cmd, vars)
@@ -64,12 +67,12 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
6467
relative := vars["relative_percentages"].boolValue()
6568
if relative {
6669
if err := applyFocus(p, vars, o.UI); err != nil {
67-
return err
70+
return nil, nil, err
6871
}
6972
}
7073
ropt, err := reportOptions(p, vars)
7174
if err != nil {
72-
return err
75+
return nil, nil, err
7376
}
7477
c := pprofCommands[cmd[0]]
7578
if c == nil {
@@ -79,18 +82,27 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
7982
if len(cmd) == 2 {
8083
s, err := regexp.Compile(cmd[1])
8184
if err != nil {
82-
return fmt.Errorf("parsing argument regexp %s: %v", cmd[1], err)
85+
return nil, nil, fmt.Errorf("parsing argument regexp %s: %v", cmd[1], err)
8386
}
8487
ropt.Symbol = s
8588
}
8689

8790
rpt := report.New(p, ropt)
8891
if !relative {
8992
if err := applyFocus(p, vars, o.UI); err != nil {
90-
return err
93+
return nil, nil, err
9194
}
9295
}
9396
if err := aggregate(p, vars); err != nil {
97+
return nil, nil, err
98+
}
99+
100+
return c, rpt, nil
101+
}
102+
103+
func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
104+
c, rpt, err := generateRawReport(p, cmd, vars, o)
105+
if err != nil {
94106
return err
95107
}
96108

‎internal/driver/webhtml.go

Lines changed: 556 additions & 0 deletions
Large diffs are not rendered by default.

‎internal/driver/webui.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package driver
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"html/template"
21+
"io"
22+
"net"
23+
"net/http"
24+
"net/url"
25+
"os"
26+
"os/exec"
27+
"regexp"
28+
"strings"
29+
"time"
30+
31+
"github.com/google/pprof/internal/graph"
32+
"github.com/google/pprof/internal/plugin"
33+
"github.com/google/pprof/internal/report"
34+
"github.com/google/pprof/profile"
35+
)
36+
37+
// webInterface holds the state needed for serving a browser based interface.
38+
type webInterface struct {
39+
prof *profile.Profile
40+
options *plugin.Options
41+
}
42+
43+
// errorCatcher is a UI that captures errors for reporting to the browser.
44+
type errorCatcher struct {
45+
plugin.UI
46+
errors []string
47+
}
48+
49+
func (ec *errorCatcher) PrintErr(args ...interface{}) {
50+
ec.errors = append(ec.errors, strings.TrimSuffix(fmt.Sprintln(args...), "\n"))
51+
ec.UI.PrintErr(args...)
52+
}
53+
54+
func serveWebInterface(port int, p *profile.Profile, o *plugin.Options) error {
55+
interactiveMode = true
56+
ui := &webInterface{
57+
prof: p,
58+
options: o,
59+
}
60+
// authorization wrapper
61+
wrap := o.HTTPWrapper
62+
if wrap == nil {
63+
// only allow requests from local host
64+
wrap = checkLocalHost
65+
}
66+
http.Handle("/", wrap(http.HandlerFunc(ui.dot)))
67+
http.Handle("/disasm", wrap(http.HandlerFunc(ui.disasm)))
68+
http.Handle("/weblist", wrap(http.HandlerFunc(ui.weblist)))
69+
go openBrowser(port, o)
70+
return http.ListenAndServe(fmt.Sprint(":", port), nil)
71+
}
72+
73+
func openBrowser(port int, o *plugin.Options) {
74+
// Construct URL.
75+
u, _ := url.Parse(fmt.Sprint("http://localhost:", port))
76+
q := u.Query()
77+
for _, p := range []struct{ param, key string }{
78+
{"f", "focus"},
79+
{"s", "show"},
80+
{"i", "ignore"},
81+
{"h", "hide"},
82+
} {
83+
if v := pprofVariables[p.key].value; v != "" {
84+
q.Set(p.param, v)
85+
}
86+
}
87+
u.RawQuery = q.Encode()
88+
89+
// Give server a little time to get ready.
90+
time.Sleep(time.Millisecond * 500)
91+
92+
for _, b := range browsers() {
93+
args := strings.Split(b, " ")
94+
if len(args) == 0 {
95+
continue
96+
}
97+
viewer := exec.Command(args[0], append(args[1:], u.String())...)
98+
viewer.Stderr = os.Stderr
99+
if err := viewer.Start(); err == nil {
100+
return
101+
}
102+
}
103+
// No visualizer succeeded, so just print URL.
104+
o.UI.PrintErr(u.String())
105+
}
106+
107+
func checkLocalHost(h http.Handler) http.Handler {
108+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
109+
host, _, err := net.SplitHostPort(req.RemoteAddr)
110+
if err != nil || ((host != "127.0.0.1") && (host != "::1")) {
111+
http.Error(w, "permission denied", http.StatusForbidden)
112+
return
113+
}
114+
h.ServeHTTP(w, req)
115+
})
116+
}
117+
118+
// dot generates a web page containing an svg diagram.
119+
func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
120+
if req.URL.Path != "/" {
121+
http.NotFound(w, req)
122+
return
123+
}
124+
125+
// Capture any error messages generated while generating a report.
126+
catcher := &errorCatcher{UI: ui.options.UI}
127+
options := *ui.options
128+
options.UI = catcher
129+
130+
// Generate dot graph.
131+
args := []string{"svg"}
132+
vars := pprofVariables.makeCopy()
133+
vars["focus"].value = req.URL.Query().Get("f")
134+
vars["show"].value = req.URL.Query().Get("s")
135+
vars["ignore"].value = req.URL.Query().Get("i")
136+
vars["hide"].value = req.URL.Query().Get("h")
137+
_, rpt, err := generateRawReport(ui.prof, args, vars, &options)
138+
if err != nil {
139+
http.Error(w, err.Error(), http.StatusBadRequest)
140+
ui.options.UI.PrintErr(err)
141+
return
142+
}
143+
g, config := report.GetDOT(rpt)
144+
legend := config.Labels
145+
config.Labels = nil
146+
dot := &bytes.Buffer{}
147+
graph.ComposeDot(dot, g, &graph.DotAttributes{}, config)
148+
149+
// Convert to svg.
150+
svg, err := dotToSvg(dot.Bytes())
151+
if err != nil {
152+
http.Error(w, "Could not execute dot; may need to install graphviz.",
153+
http.StatusNotImplemented)
154+
ui.options.UI.PrintErr("Failed to execute dot. Is Graphviz installed?\n", err)
155+
return
156+
}
157+
158+
// Get regular expression for each node.
159+
nodes := []string{""}
160+
for _, n := range g.Nodes {
161+
nodes = append(nodes, regexp.QuoteMeta(n.Info.Name))
162+
}
163+
164+
// Embed in html.
165+
file := getFromLegend(legend, "File: ", "unknown")
166+
profile := getFromLegend(legend, "Type: ", "unknown")
167+
data := struct {
168+
Title string
169+
Errors []string
170+
Svg template.HTML
171+
Legend []string
172+
Nodes []string
173+
}{
174+
Title: file + " " + profile,
175+
Errors: catcher.errors,
176+
Svg: template.HTML(string(svg)),
177+
Legend: legend,
178+
Nodes: nodes,
179+
}
180+
html := &bytes.Buffer{}
181+
if err := graphTemplate.Execute(html, data); err != nil {
182+
http.Error(w, "internal template error", http.StatusInternalServerError)
183+
ui.options.UI.PrintErr(err)
184+
return
185+
}
186+
w.Header().Set("Content-Type", "text/html")
187+
w.Write(html.Bytes())
188+
}
189+
190+
func dotToSvg(dot []byte) ([]byte, error) {
191+
cmd := exec.Command("dot", "-Tsvg")
192+
out := &bytes.Buffer{}
193+
cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr
194+
if err := cmd.Run(); err != nil {
195+
return nil, err
196+
}
197+
198+
// Fix dot bug related to unquoted amperands.
199+
svg := bytes.Replace(out.Bytes(), []byte("&;"), []byte("&amp;;"), -1)
200+
201+
// Cleanup for embedding by dropping stuff before the <svg> start.
202+
if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 {
203+
svg = svg[pos:]
204+
}
205+
return svg, nil
206+
}
207+
208+
// disasm generates a web page containing disassembly.
209+
func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
210+
ui.output(w, req, "disasm", "text/plain")
211+
}
212+
213+
// weblist generates a web page containing disassembly.
214+
func (ui *webInterface) weblist(w http.ResponseWriter, req *http.Request) {
215+
ui.output(w, req, "weblist", "text/html")
216+
}
217+
218+
// output generates a webpage that contains the output of the specified pprof cmd.
219+
func (ui *webInterface) output(w http.ResponseWriter, req *http.Request, cmd, ctype string) {
220+
focus := req.URL.Query().Get("f")
221+
if focus == "" {
222+
fmt.Fprintln(w, "no argument supplied for "+cmd)
223+
return
224+
}
225+
226+
// Capture any error messages generated while generating a report.
227+
catcher := &errorCatcher{UI: ui.options.UI}
228+
options := *ui.options
229+
options.UI = catcher
230+
231+
args := []string{cmd, focus}
232+
vars := pprofVariables.makeCopy()
233+
_, rpt, err := generateRawReport(ui.prof, args, vars, &options)
234+
if err != nil {
235+
http.Error(w, err.Error(), http.StatusBadRequest)
236+
ui.options.UI.PrintErr(err)
237+
return
238+
}
239+
240+
out := &bytes.Buffer{}
241+
if err := report.Generate(out, rpt, ui.options.Obj); err != nil {
242+
http.Error(w, err.Error(), http.StatusBadRequest)
243+
ui.options.UI.PrintErr(err)
244+
return
245+
}
246+
247+
if len(catcher.errors) > 0 {
248+
w.Header().Set("Content-Type", "text/plain")
249+
for _, msg := range catcher.errors {
250+
fmt.Println(w, msg)
251+
}
252+
return
253+
}
254+
255+
w.Header().Set("Content-Type", ctype)
256+
io.Copy(w, out)
257+
}
258+
259+
// getFromLegend returns the suffix of an entry in legend that starts
260+
// with param. It returns def if no such entry is found.
261+
func getFromLegend(legend []string, param, def string) string {
262+
for _, s := range legend {
263+
if strings.HasPrefix(s, param) {
264+
return s[len(param):]
265+
}
266+
}
267+
return def
268+
}

‎internal/driver/webui_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package driver
16+
17+
import (
18+
"fmt"
19+
"io/ioutil"
20+
"net/http"
21+
"net/http/httptest"
22+
"net/url"
23+
"os/exec"
24+
"regexp"
25+
"strings"
26+
"testing"
27+
28+
"github.com/google/pprof/internal/plugin"
29+
"github.com/google/pprof/profile"
30+
)
31+
32+
func TestWebInterface(t *testing.T) {
33+
prof := makeFakeProfile()
34+
ui := &webInterface{prof, &plugin.Options{Obj: fakeObjTool{}}}
35+
36+
// Start test server.
37+
server := httptest.NewServer(http.HandlerFunc(
38+
func(w http.ResponseWriter, r *http.Request) {
39+
switch r.URL.Path {
40+
case "/":
41+
ui.dot(w, r)
42+
case "/disasm":
43+
ui.disasm(w, r)
44+
case "/weblist":
45+
ui.weblist(w, r)
46+
}
47+
}))
48+
defer server.Close()
49+
50+
haveDot := false
51+
if _, err := exec.LookPath("dot"); err == nil {
52+
haveDot = true
53+
}
54+
55+
type testCase struct {
56+
path string
57+
want []string
58+
needDot bool
59+
}
60+
testcases := []testCase{
61+
{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
62+
{"/weblist?f=" + url.QueryEscape("F[12]"),
63+
[]string{"F1", "F2", "300ms line1"}, false},
64+
{"/disasm?f=" + url.QueryEscape("F[12]"),
65+
[]string{"f1:asm", "f2:asm"}, false},
66+
}
67+
for _, c := range testcases {
68+
if c.needDot && !haveDot {
69+
t.Log("skpping", c.path, "since dot (graphviz) does not seem to be installed")
70+
continue
71+
}
72+
73+
res, err := http.Get(server.URL + c.path)
74+
if err != nil {
75+
t.Error("could not fetch", c.path, err)
76+
continue
77+
}
78+
data, err := ioutil.ReadAll(res.Body)
79+
if err != nil {
80+
t.Error("could not read response", c.path, err)
81+
continue
82+
}
83+
result := string(data)
84+
for _, w := range c.want {
85+
if !strings.Contains(result, w) {
86+
t.Errorf("response for %s does not contain "+
87+
"expected string '%s'; "+
88+
"actual result:\n%s", c.path, w, result)
89+
}
90+
}
91+
}
92+
93+
}
94+
95+
// Implement fake object file support.
96+
97+
const addrBase = 0x1000
98+
const fakeSource = "testdata/file1000.src"
99+
100+
type fakeObj struct{}
101+
102+
func (f fakeObj) Close() error { return nil }
103+
func (f fakeObj) Name() string { return "testbin" }
104+
func (f fakeObj) Base() uint64 { return 0 }
105+
func (f fakeObj) BuildID() string { return "" }
106+
func (f fakeObj) SourceLine(addr uint64) ([]plugin.Frame, error) {
107+
return nil, fmt.Errorf("SourceLine unimplemented")
108+
}
109+
func (f fakeObj) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) {
110+
return []*plugin.Sym{
111+
{[]string{"F1"}, fakeSource, addrBase, addrBase + 10},
112+
{[]string{"F2"}, fakeSource, addrBase + 10, addrBase + 20},
113+
{[]string{"F3"}, fakeSource, addrBase + 20, addrBase + 30},
114+
}, nil
115+
}
116+
117+
type fakeObjTool struct{}
118+
119+
func (obj fakeObjTool) Open(file string, start, limit, offset uint64) (plugin.ObjFile, error) {
120+
return fakeObj{}, nil
121+
}
122+
123+
func (obj fakeObjTool) Disasm(file string, start, end uint64) ([]plugin.Inst, error) {
124+
return []plugin.Inst{
125+
{Addr: addrBase + 0, Text: "f1:asm", Function: "F1"},
126+
{Addr: addrBase + 10, Text: "f2:asm", Function: "F2"},
127+
{Addr: addrBase + 20, Text: "d3:asm", Function: "F3"},
128+
}, nil
129+
}
130+
131+
func makeFakeProfile() *profile.Profile {
132+
// Three functions: F1, F2, F3 with three lines, 11, 22, 33.
133+
funcs := []*profile.Function{
134+
{ID: 1, Name: "F1", Filename: fakeSource, StartLine: 3},
135+
{ID: 2, Name: "F2", Filename: fakeSource, StartLine: 5},
136+
{ID: 3, Name: "F3", Filename: fakeSource, StartLine: 7},
137+
}
138+
lines := []profile.Line{
139+
{Function: funcs[0], Line: 11},
140+
{Function: funcs[1], Line: 22},
141+
{Function: funcs[2], Line: 33},
142+
}
143+
mapping := []*profile.Mapping{
144+
{
145+
ID: 1,
146+
Start: addrBase,
147+
Limit: addrBase + 10,
148+
Offset: 0,
149+
File: "testbin",
150+
HasFunctions: true,
151+
HasFilenames: true,
152+
HasLineNumbers: true,
153+
},
154+
}
155+
156+
// Three interesting addresses: base+{10,20,30}
157+
locs := []*profile.Location{
158+
{ID: 1, Address: addrBase + 10, Line: lines[0:1], Mapping: mapping[0]},
159+
{ID: 2, Address: addrBase + 20, Line: lines[1:2], Mapping: mapping[0]},
160+
{ID: 3, Address: addrBase + 30, Line: lines[2:3], Mapping: mapping[0]},
161+
}
162+
163+
// Two stack traces.
164+
return &profile.Profile{
165+
PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
166+
Period: 1,
167+
DurationNanos: 10e9,
168+
SampleType: []*profile.ValueType{
169+
{Type: "cpu", Unit: "milliseconds"},
170+
},
171+
Sample: []*profile.Sample{
172+
{
173+
Location: []*profile.Location{locs[2], locs[1], locs[0]},
174+
Value: []int64{100},
175+
},
176+
{
177+
Location: []*profile.Location{locs[1], locs[0]},
178+
Value: []int64{200},
179+
},
180+
},
181+
Location: locs,
182+
Function: funcs,
183+
Mapping: mapping,
184+
}
185+
}

‎internal/graph/dotgraph.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ func (b *builder) finish() {
123123
// addLegend generates a legend in DOT format.
124124
func (b *builder) addLegend() {
125125
labels := b.config.Labels
126-
var title string
127-
if len(labels) > 0 {
128-
title = labels[0]
126+
if len(labels) == 0 {
127+
return
129128
}
129+
title := labels[0]
130130
fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
131131
fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`))
132132
if b.config.LegendURL != "" {

‎internal/plugin/plugin.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package plugin
1717

1818
import (
1919
"io"
20+
"net/http"
2021
"regexp"
2122
"time"
2223

@@ -31,6 +32,15 @@ type Options struct {
3132
Sym Symbolizer
3233
Obj ObjTool
3334
UI UI
35+
36+
// HTTPWrapper takes a pprof http handler as an argument and
37+
// returns the actual handler that should be invoked by http.
38+
// A typical use is to add authentication before calling the
39+
// pprof handler.
40+
//
41+
// If HTTPWrapper is nil, a default wrapper will be used that
42+
// disallows all requests except from the localhost.
43+
HTTPWrapper func(http.Handler) http.Handler
3444
}
3545

3646
// Writer provides a mechanism to write data under a certain name,

‎internal/report/report.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -975,8 +975,9 @@ func printTree(w io.Writer, rpt *Report) error {
975975
return nil
976976
}
977977

978-
// printDOT prints an annotated callgraph in DOT format.
979-
func printDOT(w io.Writer, rpt *Report) error {
978+
// GetDOT returns a graph suitable for dot processing along with some
979+
// configuration information.
980+
func GetDOT(rpt *Report) (*graph.Graph, *graph.DotConfig) {
980981
g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph()
981982
rpt.selectOutputUnit(g)
982983
labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true)
@@ -993,6 +994,12 @@ func printDOT(w io.Writer, rpt *Report) error {
993994
FormatTag: formatTag,
994995
Total: rpt.total,
995996
}
997+
return g, c
998+
}
999+
1000+
// printDOT prints an annotated callgraph in DOT format.
1001+
func printDOT(w io.Writer, rpt *Report) error {
1002+
g, c := GetDOT(rpt)
9961003
graph.ComposeDot(w, g, &graph.DotAttributes{}, c)
9971004
return nil
9981005
}

0 commit comments

Comments
 (0)
Please sign in to comment.