Browse Source

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.
Sanjay Ghemawat 7 years ago
parent
commit
f83a3d89c1

+ 1
- 0
.gitignore View File

5
 .*.swp
5
 .*.swp
6
 core
6
 core
7
 coverage.txt
7
 coverage.txt
8
+pprof

+ 13
- 0
README.md View File

77
 Type 'help' for available commands/options.
77
 Type 'help' for available commands/options.
78
 ```
78
 ```
79
 
79
 
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
+
80
 ## Using pprof with Linux Perf
93
 ## Using pprof with Linux Perf
81
 
94
 
82
 pprof can read `perf.data` files generated by the
95
 pprof can read `perf.data` files generated by the

+ 39
- 8
doc/pprof.md View File

29
 possible. The interpretation of the reports generated by pprof depends on the
29
 possible. The interpretation of the reports generated by pprof depends on the
30
 semantics defined by the source of the profile.
30
 semantics defined by the source of the profile.
31
 
31
 
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
33
 
66
 
34
 The objective of pprof is to generate a report for a profile. The report is
67
 The objective of pprof is to generate a report for a profile. The report is
35
 generated from a location hierarchy, which is reconstructed from the profile
68
 generated from a location hierarchy, which is reconstructed from the profile
38
 descendants. Samples that include a location multiple times (eg for recursive
71
 descendants. Samples that include a location multiple times (eg for recursive
39
 functions) are counted only once per location.
72
 functions) are counted only once per location.
40
 
73
 
41
-The basic usage of pprof is
42
-
43
-    pprof <format> [options] source
74
+## Options
44
 
75
 
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.
49
 
80
 
50
 Some common pprof options are:
81
 Some common pprof options are:
51
 
82
 

+ 26
- 1
internal/driver/cli.go View File

32
 	Seconds   int
32
 	Seconds   int
33
 	Timeout   int
33
 	Timeout   int
34
 	Symbolize string
34
 	Symbolize string
35
+	HTTPPort  int
35
 }
36
 }
36
 
37
 
37
 // Parse parses the command lines through the specified flags package
38
 // Parse parses the command lines through the specified flags package
58
 	flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames")
59
 	flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames")
59
 
60
 
60
 	flagTimeout := flag.Int("timeout", -1, "Timeout in seconds for fetching a profile")
61
 	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")
61
 
63
 
62
 	// Flags used during command processing
64
 	// Flags used during command processing
63
 	installedFlags := installFlags(flag)
65
 	installedFlags := installFlags(flag)
106
 	if err != nil {
108
 	if err != nil {
107
 		return nil, nil, err
109
 		return nil, nil, err
108
 	}
110
 	}
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
+	}
109
 
114
 
110
 	si := pprofVariables["sample_index"].value
115
 	si := pprofVariables["sample_index"].value
111
 	si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
116
 	si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
128
 		Seconds:   *flagSeconds,
133
 		Seconds:   *flagSeconds,
129
 		Timeout:   *flagTimeout,
134
 		Timeout:   *flagTimeout,
130
 		Symbolize: *flagSymbolize,
135
 		Symbolize: *flagSymbolize,
136
+		HTTPPort:  *flagHTTPPort,
131
 	}
137
 	}
132
 
138
 
133
 	for _, s := range *flagBase {
139
 	for _, s := range *flagBase {
240
 	return cmd, nil
246
 	return cmd, nil
241
 }
247
 }
242
 
248
 
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
+`
244
 
268
 
245
 var usageMsgSrc = "\n\n" +
269
 var usageMsgSrc = "\n\n" +
246
 	"  Source options:\n" +
270
 	"  Source options:\n" +
261
 
285
 
262
 var usageMsgVars = "\n\n" +
286
 var usageMsgVars = "\n\n" +
263
 	"  Misc options:\n" +
287
 	"  Misc options:\n" +
288
+	"   -http port             Provide web based interface at port\n" +
264
 	"   -tools                 Search path for object tools\n" +
289
 	"   -tools                 Search path for object tools\n" +
265
 	"\n" +
290
 	"\n" +
266
 	"  Environment Variables:\n" +
291
 	"  Environment Variables:\n" +

+ 1
- 1
internal/driver/commands.go View File

263
 
263
 
264
 	var help string
264
 	var help string
265
 	if commandLine {
265
 	if commandLine {
266
-		help = "  Output formats (select only one):\n"
266
+		help = "  Output formats (select at most one):\n"
267
 	} else {
267
 	} else {
268
 		help = "  Commands:\n"
268
 		help = "  Commands:\n"
269
 		commands = append(commands, fmtHelp("o/options", "List options and their current values"))
269
 		commands = append(commands, fmtHelp("o/options", "List options and their current values"))

+ 17
- 5
internal/driver/driver.go View File

52
 		return generateReport(p, cmd, pprofVariables, o)
52
 		return generateReport(p, cmd, pprofVariables, o)
53
 	}
53
 	}
54
 
54
 
55
+	if src.HTTPPort > 0 {
56
+		return serveWebInterface(src.HTTPPort, p, o)
57
+	}
55
 	return interactive(p, o)
58
 	return interactive(p, o)
56
 }
59
 }
57
 
60
 
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) {
59
 	p = p.Copy() // Prevent modification to the incoming profile.
62
 	p = p.Copy() // Prevent modification to the incoming profile.
60
 
63
 
61
 	vars = applyCommandOverrides(cmd, vars)
64
 	vars = applyCommandOverrides(cmd, vars)
64
 	relative := vars["relative_percentages"].boolValue()
67
 	relative := vars["relative_percentages"].boolValue()
65
 	if relative {
68
 	if relative {
66
 		if err := applyFocus(p, vars, o.UI); err != nil {
69
 		if err := applyFocus(p, vars, o.UI); err != nil {
67
-			return err
70
+			return nil, nil, err
68
 		}
71
 		}
69
 	}
72
 	}
70
 	ropt, err := reportOptions(p, vars)
73
 	ropt, err := reportOptions(p, vars)
71
 	if err != nil {
74
 	if err != nil {
72
-		return err
75
+		return nil, nil, err
73
 	}
76
 	}
74
 	c := pprofCommands[cmd[0]]
77
 	c := pprofCommands[cmd[0]]
75
 	if c == nil {
78
 	if c == nil {
79
 	if len(cmd) == 2 {
82
 	if len(cmd) == 2 {
80
 		s, err := regexp.Compile(cmd[1])
83
 		s, err := regexp.Compile(cmd[1])
81
 		if err != nil {
84
 		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)
83
 		}
86
 		}
84
 		ropt.Symbol = s
87
 		ropt.Symbol = s
85
 	}
88
 	}
87
 	rpt := report.New(p, ropt)
90
 	rpt := report.New(p, ropt)
88
 	if !relative {
91
 	if !relative {
89
 		if err := applyFocus(p, vars, o.UI); err != nil {
92
 		if err := applyFocus(p, vars, o.UI); err != nil {
90
-			return err
93
+			return nil, nil, err
91
 		}
94
 		}
92
 	}
95
 	}
93
 	if err := aggregate(p, vars); err != nil {
96
 	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 {
94
 		return err
106
 		return err
95
 	}
107
 	}
96
 
108
 

+ 556
- 0
internal/driver/webhtml.go View File

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 "html/template"
18
+
19
+var graphTemplate = template.Must(template.New("graph").Parse(
20
+	`<!DOCTYPE html>
21
+<html>
22
+<head>
23
+<meta charset="utf-8">
24
+<title>{{.Title}}</title>
25
+<style type="text/css">
26
+html, body {
27
+  height: 100%;
28
+  min-height: 100%;
29
+  margin: 0px;
30
+}
31
+body {
32
+  width: 100%;
33
+  overflow: hidden;
34
+}
35
+h1 {
36
+  font-weight: normal;
37
+  font-size: 24px;
38
+  padding: 0em;
39
+  margin-top: 5px;
40
+  margin-bottom: 5px;
41
+}
42
+#page {
43
+  display: flex;
44
+  flex-direction: column;
45
+  height: 100%;
46
+  min-height: 100%;
47
+  width: 100%;
48
+  min-width: 100%;
49
+  margin: 0px;
50
+}
51
+#header {
52
+  flex: 0 0 auto;
53
+  width: 100%;
54
+}
55
+#leftbuttons {
56
+  float: left;
57
+}
58
+#rightbuttons {
59
+  float: right;
60
+  display: table-cell;
61
+  vertical-align: middle;
62
+}
63
+#rightbuttons label {
64
+  vertical-align: middle;
65
+}
66
+#scale {
67
+  vertical-align: middle;
68
+}
69
+#graph {
70
+  flex: 1 1 auto;
71
+  overflow: hidden;
72
+}
73
+svg {
74
+  width: 100%;
75
+  height: auto;
76
+  border: 1px solid black;
77
+}
78
+button {
79
+  margin-top: 5px;
80
+  margin-bottom: 5px;
81
+}
82
+#reset, #scale {
83
+  margin-left: 10px;
84
+}
85
+#detailtext {
86
+  display: none;
87
+  position: absolute;
88
+  background-color: #ffffff;
89
+  min-width: 160px;
90
+  border-top: 1px solid black;
91
+  box-shadow: 2px 2px 2px 0px #aaa;
92
+  z-index: 1;
93
+}
94
+</style>
95
+</head>
96
+<body>
97
+<h1>{{.Title}}</h1>
98
+<div id="page">
99
+
100
+<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
101
+
102
+<div id="header">
103
+<div id="leftbuttons">
104
+<button id="details">&#x25b7; Details</button>
105
+<div id="detailtext">
106
+{{range .Legend}}<div>{{.}}</div>{{end}}
107
+</div>
108
+<button id="list">List</button>
109
+<button id="disasm">Disasm</button>
110
+<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
111
+<button id="focus">Focus</button>
112
+<button id="ignore">Ignore</button>
113
+<button id="hide">Hide</button>
114
+<button id="show">Show</button>
115
+<button id="reset">Reset</button>
116
+</div>
117
+<div id="rightbuttons">
118
+</div>
119
+</div>
120
+
121
+<div id="graph">
122
+{{.Svg}}
123
+</div>
124
+
125
+</div>
126
+<script>
127
+// Make svg pannable and zoomable.
128
+// Call clickHandler(t) if a click event is caught by the pan event handlers.
129
+function initPanAndZoom(svg, clickHandler) {
130
+  'use strict';
131
+
132
+  // Current mouse/touch handling mode
133
+  const IDLE = 0
134
+  const MOUSEPAN = 1
135
+  const TOUCHPAN = 2
136
+  const TOUCHZOOM = 3
137
+  let mode = IDLE
138
+
139
+  // State needed to implement zooming.
140
+  let currentScale = 1.0
141
+  const initWidth = svg.viewBox.baseVal.width
142
+  const initHeight = svg.viewBox.baseVal.height
143
+
144
+  // State needed to implement panning.
145
+  let panLastX = 0      // Last event X coordinate
146
+  let panLastY = 0      // Last event Y coordinate
147
+  let moved = false     // Have we seen significant movement
148
+  let touchid = null    // Current touch identifier
149
+
150
+  // State needed for pinch zooming
151
+  let touchid2 = null     // Second id for pinch zooming
152
+  let initGap = 1.0       // Starting gap between two touches
153
+  let initScale = 1.0     // currentScale when pinch zoom started
154
+  let centerPoint = null  // Center point for scaling
155
+
156
+  // Convert event coordinates to svg coordinates.
157
+  function toSvg(x, y) {
158
+    const p = svg.createSVGPoint()
159
+    p.x = x
160
+    p.y = y
161
+    let m = svg.getCTM()
162
+    if (m == null) m = svg.getScreenCTM()  // Firefox workaround.
163
+    return p.matrixTransform(m.inverse())
164
+  }
165
+
166
+  // Change the scaling for the svg to s, keeping the point denoted
167
+  // by u (in svg coordinates]) fixed at the same screen location.
168
+  function rescale(s, u) {
169
+    // Limit to a good range.
170
+    if (s < 0.2) s = 0.2
171
+    if (s > 10.0) s = 10.0
172
+
173
+    currentScale = s
174
+
175
+    // svg.viewBox defines the visible portion of the user coordinate
176
+    // system.  So to magnify by s, divide the visible portion by s,
177
+    // which will then be stretched to fit the viewport.
178
+    const vb = svg.viewBox
179
+    const w1 = vb.baseVal.width
180
+    const w2 = initWidth / s
181
+    const h1 = vb.baseVal.height
182
+    const h2 = initHeight / s
183
+    vb.baseVal.width = w2
184
+    vb.baseVal.height = h2
185
+
186
+    // We also want to adjust vb.baseVal.x so that u.x remains at same
187
+    // screen X coordinate.  In other words, want to change it from x1 to x2
188
+    // so that:
189
+    //     (u.x - x1) / w1 = (u.x - x2) / w2
190
+    // Simplifying that, we get
191
+    //     (u.x - x1) * (w2 / w1) = u.x - x2
192
+    //     x2 = u.x - (u.x - x1) * (w2 / w1)
193
+    vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1)
194
+    vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1)
195
+  }
196
+
197
+  function handleWheel(e) {
198
+    if (e.deltaY == 0) return
199
+    // Change scale factor by 1.1 or 1/1.1
200
+    rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)),
201
+            toSvg(e.offsetX, e.offsetY))
202
+  }
203
+
204
+  function setMode(m) {
205
+    mode = m
206
+    touchid = null
207
+    touchid2 = null
208
+  }
209
+
210
+  function panStart(x, y) {
211
+    moved = false
212
+    panLastX = x
213
+    panLastY = y
214
+  }
215
+
216
+  function panMove(x, y) {
217
+    let dx = x - panLastX
218
+    let dy = y - panLastY
219
+    if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return  // Ignore tiny moves
220
+
221
+    moved = true
222
+    panLastX = x
223
+    panLastY = y
224
+
225
+    // Firefox workaround: get dimensions from parentNode.
226
+    const swidth = svg.clientWidth || svg.parentNode.clientWidth
227
+    const sheight = svg.clientHeight || svg.parentNode.clientHeight
228
+
229
+    // Convert deltas from screen space to svg space.
230
+    dx *= (svg.viewBox.baseVal.width / swidth)
231
+    dy *= (svg.viewBox.baseVal.height / sheight)
232
+
233
+    svg.viewBox.baseVal.x -= dx
234
+    svg.viewBox.baseVal.y -= dy
235
+  }
236
+
237
+  function handleScanStart(e) {
238
+    if (e.button != 0) return  // Do not catch right-clicks etc.
239
+    setMode(MOUSEPAN)
240
+    panStart(e.clientX, e.clientY)
241
+    e.preventDefault()
242
+    svg.addEventListener("mousemove", handleScanMove)
243
+  }
244
+
245
+  function handleScanMove(e) {
246
+    if (mode == MOUSEPAN) panMove(e.clientX, e.clientY)
247
+  }
248
+
249
+  function handleScanEnd(e) {
250
+    if (mode == MOUSEPAN) panMove(e.clientX, e.clientY)
251
+    setMode(IDLE)
252
+    svg.removeEventListener("mousemove", handleScanMove)
253
+    if (!moved) clickHandler(e.target)
254
+  }
255
+
256
+  // Find touch object with specified identifier.
257
+  function findTouch(tlist, id) {
258
+    for (const t of tlist) {
259
+      if (t.identifier == id) return t
260
+    }
261
+    return null
262
+  }
263
+
264
+ // Return distance between two touch points
265
+  function touchGap(t1, t2) {
266
+    const dx = t1.clientX - t2.clientX
267
+    const dy = t1.clientY - t2.clientY
268
+    return Math.hypot(dx, dy)
269
+  }
270
+
271
+  function handleTouchStart(e) {
272
+    if (mode == IDLE && e.changedTouches.length == 1) {
273
+      // Start touch based panning
274
+      const t = e.changedTouches[0]
275
+      setMode(TOUCHPAN)
276
+      touchid = t.identifier
277
+      panStart(t.clientX, t.clientY)
278
+      e.preventDefault()
279
+    } else if (mode == TOUCHPAN && e.touches.length == 2) {
280
+      // Start pinch zooming
281
+      setMode(TOUCHZOOM)
282
+      const t1 = e.touches[0]
283
+      const t2 = e.touches[1]
284
+      touchid = t1.identifier
285
+      touchid2 = t2.identifier
286
+      initScale = currentScale
287
+      initGap = touchGap(t1, t2)
288
+      centerPoint = toSvg((t1.clientX + t2.clientX) / 2,
289
+                          (t1.clientY + t2.clientY) / 2)
290
+      e.preventDefault()
291
+    }
292
+  }
293
+
294
+  function handleTouchMove(e) {
295
+    if (mode == TOUCHPAN) {
296
+      const t = findTouch(e.changedTouches, touchid)
297
+      if (t == null) return
298
+      if (e.touches.length != 1) {
299
+        setMode(IDLE)
300
+        return
301
+      }
302
+      panMove(t.clientX, t.clientY)
303
+      e.preventDefault()
304
+    } else if (mode == TOUCHZOOM) {
305
+      // Get two touches; new gap; rescale to ratio.
306
+      const t1 = findTouch(e.touches, touchid)
307
+      const t2 = findTouch(e.touches, touchid2)
308
+      if (t1 == null || t2 == null) return
309
+      const gap = touchGap(t1, t2)
310
+      rescale(initScale * gap / initGap, centerPoint)
311
+      e.preventDefault()
312
+    }
313
+  }
314
+
315
+  function handleTouchEnd(e) {
316
+    if (mode == TOUCHPAN) {
317
+      const t = findTouch(e.changedTouches, touchid)
318
+      if (t == null) return
319
+      panMove(t.clientX, t.clientY)
320
+      setMode(IDLE)
321
+      e.preventDefault()
322
+      if (!moved) clickHandler(t.target)
323
+    } else if (mode == TOUCHZOOM) {
324
+      setMode(IDLE)
325
+      e.preventDefault()
326
+    }
327
+  }
328
+
329
+  svg.addEventListener("mousedown", handleScanStart)
330
+  svg.addEventListener("mouseup", handleScanEnd)
331
+  svg.addEventListener("touchstart", handleTouchStart)
332
+  svg.addEventListener("touchmove", handleTouchMove)
333
+  svg.addEventListener("touchend", handleTouchEnd)
334
+  svg.addEventListener("wheel", handleWheel, true)
335
+}
336
+
337
+function dotviewer(nodes) {
338
+  'use strict';
339
+
340
+  // Elements
341
+  const detailsButton = document.getElementById("details")
342
+  const detailsText = document.getElementById("detailtext")
343
+  const listButton = document.getElementById("list")
344
+  const disasmButton = document.getElementById("disasm")
345
+  const resetButton = document.getElementById("reset")
346
+  const focusButton = document.getElementById("focus")
347
+  const showButton = document.getElementById("show")
348
+  const ignoreButton = document.getElementById("ignore")
349
+  const hideButton = document.getElementById("hide")
350
+  const search = document.getElementById("searchbox")
351
+  const graph0 = document.getElementById("graph0")
352
+  const svg = graph0.parentElement
353
+
354
+  let currentRe = ""
355
+  let selected = new Map()
356
+  let origFill = new Map()
357
+  let searchAlarm = null
358
+  let buttonsEnabled = true
359
+
360
+  function handleDetails() {
361
+    if (detailtext.style.display == "block") {
362
+      detailtext.style.display = "none"
363
+      detailsButton.innerText = "\u25b7 Details"
364
+    } else {
365
+      detailtext.style.display = "block"
366
+      detailsButton.innerText = "\u25bd Details"
367
+    }
368
+  }
369
+
370
+  function handleReset() { window.location.href = "/" }
371
+  function handleList() { navigate("/weblist", "f", true) }
372
+  function handleDisasm() { navigate("/disasm", "f", true) }
373
+  function handleFocus() { navigate("/", "f", false) }
374
+  function handleShow() { navigate("/", "s", false) }
375
+  function handleIgnore() { navigate("/", "i", false) }
376
+  function handleHide() { navigate("/", "h", false) }
377
+
378
+  function handleSearch() {
379
+    // Delay processing so a flurry of key strokes is handled once.
380
+    if (searchAlarm != null) {
381
+      clearTimeout(searchAlarm)
382
+    }
383
+    searchAlarm = setTimeout(doSearch, 300)
384
+  }
385
+
386
+  function doSearch() {
387
+    searchAlarm = null
388
+    let re = null
389
+    if (search.value != "") {
390
+      try {
391
+        re = new RegExp(search.value)
392
+      } catch (e) {
393
+        // TODO: Display error state in search box
394
+        return
395
+      }
396
+    }
397
+    currentRe = search.value
398
+
399
+    function match(text) {
400
+      return re != null && re.test(text)
401
+    }
402
+
403
+    // drop currently selected items that do not match re.
404
+    selected.forEach(function(v, n) {
405
+      if (!match(nodes[n])) {
406
+        unselect(n, document.getElementById("node" + n))
407
+      }
408
+    })
409
+
410
+    // add matching items that are not currently selected.
411
+    for (let n = 0; n < nodes.length; n++) {
412
+      if (!selected.has(n) && match(nodes[n])) {
413
+        select(n, document.getElementById("node" + n))
414
+      }
415
+    }
416
+
417
+    updateButtons()
418
+  }
419
+
420
+  function toggleSelect(elem) {
421
+    // Walk up to immediate child of graph0
422
+    while (elem != null && elem.parentElement != graph0) {
423
+      elem = elem.parentElement
424
+    }
425
+    if (!elem) return
426
+
427
+    // Disable regexp mode.
428
+    currentRe = ""
429
+
430
+    const n = nodeId(elem)
431
+    if (n < 0) return
432
+    if (selected.has(n)) {
433
+      unselect(n, elem)
434
+    } else {
435
+      select(n, elem)
436
+    }
437
+    updateButtons()
438
+  }
439
+
440
+  function unselect(n, elem) {
441
+    if (elem == null) return
442
+    selected.delete(n)
443
+    setBackground(elem, false)
444
+  }
445
+
446
+  function select(n, elem) {
447
+    if (elem == null) return
448
+    selected.set(n, true)
449
+    setBackground(elem, true)
450
+  }
451
+
452
+  function nodeId(elem) {
453
+    const id = elem.id
454
+    if (!id) return -1
455
+    if (!id.startsWith("node")) return -1
456
+    const n = parseInt(id.slice(4), 10)
457
+    if (isNaN(n)) return -1
458
+    if (n < 0 || n >= nodes.length) return -1
459
+    return n
460
+  }
461
+
462
+  function setBackground(elem, set) {
463
+    const p = findPolygon(elem)
464
+    if (p != null) {
465
+      if (set) {
466
+        origFill.set(p, p.style.fill)
467
+        p.style.fill = "#ccccff"
468
+      } else if (origFill.has(p)) {
469
+        p.style.fill = origFill.get(p)
470
+      }
471
+    }
472
+  }
473
+
474
+  function findPolygon(elem) {
475
+    if (elem.localName == "polygon") return elem
476
+    for (const c of elem.children) {
477
+      const p = findPolygon(c)
478
+      if (p != null) return p
479
+    }
480
+    return null
481
+  }
482
+
483
+  // Navigate to specified path with current selection reflected
484
+  // in the named parameter.
485
+  function navigate(path, param, newWindow) {
486
+    // The selection can be in one of two modes: regexp-based or
487
+    // list-based.  Construct regular expression depending on mode.
488
+    let re = currentRe
489
+    if (re == "") {
490
+      selected.forEach(function(v, key) {
491
+        if (re != "") re += "|"
492
+        re += nodes[key]
493
+      })
494
+    }
495
+
496
+    const url = new URL(window.location.href)
497
+    url.pathname = path
498
+    url.hash = ""
499
+
500
+    if (re != "") {
501
+      // For focus/show, forget old parameter.  For others, add to re.
502
+      const params = url.searchParams
503
+      if (param != "f" && param != "s" && params.has(param)) {
504
+        const old = params.get(param)
505
+        if (old != "") {
506
+          re += "|" + old
507
+        }
508
+      }
509
+      params.set(param, re)
510
+    }
511
+
512
+    if (newWindow) {
513
+      window.open(url.toString(), "_blank")
514
+    } else {
515
+      window.location.href = url.toString()
516
+    }
517
+  }
518
+
519
+  function updateButtons() {
520
+    const enable = (currentRe != "" || selected.size != 0)
521
+    if (buttonsEnabled == enable) return
522
+    buttonsEnabled = enable
523
+    listButton.disabled = !enable
524
+    disasmButton.disabled = !enable
525
+    focusButton.disabled = !enable
526
+    showButton.disabled = !enable
527
+    ignoreButton.disabled = !enable
528
+    hideButton.disabled = !enable
529
+  }
530
+
531
+  // Initialize button states
532
+  updateButtons()
533
+
534
+  // Setup event handlers
535
+  initPanAndZoom(svg, toggleSelect)
536
+  
537
+  function bindButtons(evt) {
538
+    detailsButton.addEventListener(evt, handleDetails)
539
+    listButton.addEventListener(evt, handleList)
540
+    disasmButton.addEventListener(evt, handleDisasm)
541
+    resetButton.addEventListener(evt, handleReset)
542
+    focusButton.addEventListener(evt, handleFocus)
543
+    showButton.addEventListener(evt, handleShow)
544
+    ignoreButton.addEventListener(evt, handleIgnore)
545
+    hideButton.addEventListener(evt, handleHide)
546
+  }
547
+  bindButtons("click")
548
+  bindButtons("touchstart")
549
+  search.addEventListener("input", handleSearch)
550
+}
551
+
552
+dotviewer({{.Nodes}})
553
+</script>
554
+</body>
555
+</html>
556
+`))

+ 268
- 0
internal/driver/webui.go View File

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
+}

+ 185
- 0
internal/driver/webui_test.go View File

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
+}

+ 3
- 3
internal/graph/dotgraph.go View File

123
 // addLegend generates a legend in DOT format.
123
 // addLegend generates a legend in DOT format.
124
 func (b *builder) addLegend() {
124
 func (b *builder) addLegend() {
125
 	labels := b.config.Labels
125
 	labels := b.config.Labels
126
-	var title string
127
-	if len(labels) > 0 {
128
-		title = labels[0]
126
+	if len(labels) == 0 {
127
+		return
129
 	}
128
 	}
129
+	title := labels[0]
130
 	fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
130
 	fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
131
 	fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`))
131
 	fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`))
132
 	if b.config.LegendURL != "" {
132
 	if b.config.LegendURL != "" {

+ 10
- 0
internal/plugin/plugin.go View File

17
 
17
 
18
 import (
18
 import (
19
 	"io"
19
 	"io"
20
+	"net/http"
20
 	"regexp"
21
 	"regexp"
21
 	"time"
22
 	"time"
22
 
23
 
31
 	Sym     Symbolizer
32
 	Sym     Symbolizer
32
 	Obj     ObjTool
33
 	Obj     ObjTool
33
 	UI      UI
34
 	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
34
 }
44
 }
35
 
45
 
36
 // Writer provides a mechanism to write data under a certain name,
46
 // Writer provides a mechanism to write data under a certain name,

+ 9
- 2
internal/report/report.go View File

975
 	return nil
975
 	return nil
976
 }
976
 }
977
 
977
 
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) {
980
 	g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph()
981
 	g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph()
981
 	rpt.selectOutputUnit(g)
982
 	rpt.selectOutputUnit(g)
982
 	labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true)
983
 	labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true)
993
 		FormatTag:   formatTag,
994
 		FormatTag:   formatTag,
994
 		Total:       rpt.total,
995
 		Total:       rpt.total,
995
 	}
996
 	}
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)
996
 	graph.ComposeDot(w, g, &graph.DotAttributes{}, c)
1003
 	graph.ComposeDot(w, g, &graph.DotAttributes{}, c)
997
 	return nil
1004
 	return nil
998
 }
1005
 }