Bläddra i källkod

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 år sedan
förälder
incheckning
f83a3d89c1

+ 1
- 0
.gitignore Visa fil

@@ -5,3 +5,4 @@
5 5
 .*.swp
6 6
 core
7 7
 coverage.txt
8
+pprof

+ 13
- 0
README.md Visa fil

@@ -77,6 +77,19 @@ This will open a simple shell that takes pprof commands to generate reports.
77 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 93
 ## Using pprof with Linux Perf
81 94
 
82 95
 pprof can read `perf.data` files generated by the

+ 39
- 8
doc/pprof.md Visa fil

@@ -29,7 +29,40 @@ location. pprof is agnostic to the profile semantics, so other uses are
29 29
 possible. The interpretation of the reports generated by pprof depends on the
30 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 67
 The objective of pprof is to generate a report for a profile. The report is
35 68
 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
38 71
 descendants. Samples that include a location multiple times (eg for recursive
39 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 81
 Some common pprof options are:
51 82
 

+ 26
- 1
internal/driver/cli.go Visa fil

@@ -32,6 +32,7 @@ type source struct {
32 32
 	Seconds   int
33 33
 	Timeout   int
34 34
 	Symbolize string
35
+	HTTPPort  int
35 36
 }
36 37
 
37 38
 // Parse parses the command lines through the specified flags package
@@ -58,6 +59,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
58 59
 	flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames")
59 60
 
60 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 64
 	// Flags used during command processing
63 65
 	installedFlags := installFlags(flag)
@@ -106,6 +108,9 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
106 108
 	if err != nil {
107 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 115
 	si := pprofVariables["sample_index"].value
111 116
 	si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
@@ -128,6 +133,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
128 133
 		Seconds:   *flagSeconds,
129 134
 		Timeout:   *flagTimeout,
130 135
 		Symbolize: *flagSymbolize,
136
+		HTTPPort:  *flagHTTPPort,
131 137
 	}
132 138
 
133 139
 	for _, s := range *flagBase {
@@ -240,7 +246,25 @@ func outputFormat(bcmd map[string]*bool, acmd map[string]*string) (cmd []string,
240 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 269
 var usageMsgSrc = "\n\n" +
246 270
 	"  Source options:\n" +
@@ -261,6 +285,7 @@ var usageMsgSrc = "\n\n" +
261 285
 
262 286
 var usageMsgVars = "\n\n" +
263 287
 	"  Misc options:\n" +
288
+	"   -http port             Provide web based interface at port\n" +
264 289
 	"   -tools                 Search path for object tools\n" +
265 290
 	"\n" +
266 291
 	"  Environment Variables:\n" +

+ 1
- 1
internal/driver/commands.go Visa fil

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

+ 17
- 5
internal/driver/driver.go Visa fil

@@ -52,10 +52,13 @@ func PProf(eo *plugin.Options) error {
52 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 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 62
 	p = p.Copy() // Prevent modification to the incoming profile.
60 63
 
61 64
 	vars = applyCommandOverrides(cmd, vars)
@@ -64,12 +67,12 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
64 67
 	relative := vars["relative_percentages"].boolValue()
65 68
 	if relative {
66 69
 		if err := applyFocus(p, vars, o.UI); err != nil {
67
-			return err
70
+			return nil, nil, err
68 71
 		}
69 72
 	}
70 73
 	ropt, err := reportOptions(p, vars)
71 74
 	if err != nil {
72
-		return err
75
+		return nil, nil, err
73 76
 	}
74 77
 	c := pprofCommands[cmd[0]]
75 78
 	if c == nil {
@@ -79,7 +82,7 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
79 82
 	if len(cmd) == 2 {
80 83
 		s, err := regexp.Compile(cmd[1])
81 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 87
 		ropt.Symbol = s
85 88
 	}
@@ -87,10 +90,19 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
87 90
 	rpt := report.New(p, ropt)
88 91
 	if !relative {
89 92
 		if err := applyFocus(p, vars, o.UI); err != nil {
90
-			return err
93
+			return nil, nil, err
91 94
 		}
92 95
 	}
93 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 106
 		return err
95 107
 	}
96 108
 

+ 556
- 0
internal/driver/webhtml.go Visa fil

@@ -0,0 +1,556 @@
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 Visa fil

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

+ 185
- 0
internal/driver/webui_test.go Visa fil

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

+ 3
- 3
internal/graph/dotgraph.go Visa fil

@@ -123,10 +123,10 @@ func (b *builder) finish() {
123 123
 // addLegend generates a legend in DOT format.
124 124
 func (b *builder) addLegend() {
125 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 130
 	fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
131 131
 	fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`))
132 132
 	if b.config.LegendURL != "" {

+ 10
- 0
internal/plugin/plugin.go Visa fil

@@ -17,6 +17,7 @@ package plugin
17 17
 
18 18
 import (
19 19
 	"io"
20
+	"net/http"
20 21
 	"regexp"
21 22
 	"time"
22 23
 
@@ -31,6 +32,15 @@ type Options struct {
31 32
 	Sym     Symbolizer
32 33
 	Obj     ObjTool
33 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 46
 // Writer provides a mechanism to write data under a certain name,

+ 9
- 2
internal/report/report.go Visa fil

@@ -975,8 +975,9 @@ func printTree(w io.Writer, rpt *Report) error {
975 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 981
 	g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph()
981 982
 	rpt.selectOutputUnit(g)
982 983
 	labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true)
@@ -993,6 +994,12 @@ func printDOT(w io.Writer, rpt *Report) error {
993 994
 		FormatTag:   formatTag,
994 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 1003
 	graph.ComposeDot(w, g, &graph.DotAttributes{}, c)
997 1004
 	return nil
998 1005
 }