Kaynağa Gözat

Added peek command to web interface. (#161)

Detailed list of changes:
* Since the list of actions is now long, move it into
  an action box that is displayed only when applicable.
* Display help messages when hovering over action buttons.
* Hitting return in the search box invokes "focus" action.
* Cleaned up stale css entries.

I have attached one of the resulting pages to see what things look like easily.  It is static, so won't allow navigation to other pages, but should be interactive enough.

[benchcpu.zip](https://github.com/google/pprof/files/1151238/benchcpu.zip)
Sanjay Ghemawat 7 yıl önce
ebeveyn
işleme
7209e76b00
3 değiştirilmiş dosya ile 115 ekleme ve 69 silme
  1. 82
    60
      internal/driver/webhtml.go
  2. 21
    4
      internal/driver/webui.go
  3. 12
    5
      internal/driver/webui_test.go

+ 82
- 60
internal/driver/webhtml.go Dosyayı Görüntüle

@@ -32,13 +32,6 @@ body {
32 32
   width: 100%;
33 33
   overflow: hidden;
34 34
 }
35
-h1 {
36
-  font-weight: normal;
37
-  font-size: 24px;
38
-  padding: 0em;
39
-  margin-top: 5px;
40
-  margin-bottom: 5px;
41
-}
42 35
 #page {
43 36
   display: flex;
44 37
   flex-direction: column;
@@ -48,24 +41,6 @@ h1 {
48 41
   min-width: 100%;
49 42
   margin: 0px;
50 43
 }
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 44
 #graph {
70 45
   flex: 1 1 auto;
71 46
   overflow: hidden;
@@ -73,13 +48,12 @@ h1 {
73 48
 svg {
74 49
   width: 100%;
75 50
   height: auto;
76
-  border: 1px solid black;
77 51
 }
78 52
 button {
79 53
   margin-top: 5px;
80 54
   margin-bottom: 5px;
81 55
 }
82
-#reset, #scale {
56
+#reset {
83 57
   margin-left: 10px;
84 58
 }
85 59
 #detailtext {
@@ -91,34 +65,74 @@ button {
91 65
   box-shadow: 2px 2px 2px 0px #aaa;
92 66
   z-index: 1;
93 67
 }
68
+#actionbox {
69
+  display: none;
70
+  position: fixed;
71
+  background-color: #ffffff;
72
+  border: 1px solid black;
73
+  box-shadow: 2px 2px 2px 0px #aaa;
74
+  top: 20px;
75
+  right: 20px;
76
+  z-index: 1;
77
+}
78
+.actionhdr {
79
+  background-color: #ddd;
80
+  width: 100%;
81
+  border-bottom: 1px solid black;
82
+  border-top: 1px solid black;
83
+  font-size: 14pt;
84
+}
85
+#actionbox > button {
86
+  display: block;
87
+  width: 100%;
88
+  margin: 0px;
89
+  text-align: left;
90
+  padding-left: 0.5em;
91
+  background-color: #fff;
92
+  border: none;
93
+  font-size: 12pt;
94
+}
95
+#actionbox > button:hover {
96
+  background-color: #ddd;
97
+}
98
+#home {
99
+  font-size: 20pt;
100
+  padding-left: 0.5em;
101
+  padding-right: 0.5em;
102
+}
94 103
 </style>
95 104
 </head>
96 105
 <body>
97
-<h1>{{.Title}}</h1>
98
-<div id="page">
99 106
 
100
-<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
101
-
102
-<div id="header">
103
-<div id="leftbuttons">
104 107
 <button id="details">&#x25b7; Details</button>
105 108
 <div id="detailtext">
106 109
 {{range .Legend}}<div>{{.}}</div>{{end}}
107 110
 </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>
111
+
115 112
 <button id="reset">Reset</button>
116
-</div>
117
-<div id="rightbuttons">
118
-</div>
119
-</div>
113
+
114
+<span id="home">{{.Title}}</span>
115
+
116
+<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
117
+
118
+<div id="page">
119
+
120
+<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
120 121
 
121 122
 <div id="graph">
123
+
124
+<div id="actionbox">
125
+<div class="actionhdr">Refine graph</div>
126
+<button title="{{.Help.focus}}" id="focus">Focus</button>
127
+<button title="{{.Help.ignore}}" id="ignore">Ignore</button>
128
+<button title="{{.Help.hide}}" id="hide">Hide</button>
129
+<button title="{{.Help.show}}" id="show">Show</button>
130
+<div class="actionhdr">Show Functions</div>
131
+<button title="{{.Help.peek}}" id="peek">Peek</button>
132
+<button title="{{.Help.list}}" id="list">List</button>
133
+<button title="{{.Help.disasm}}" id="disasm">Disassemble</button>
134
+</div>
135
+
122 136
 {{.Svg}}
123 137
 </div>
124 138
 
@@ -340,9 +354,11 @@ function dotviewer(nodes) {
340 354
   // Elements
341 355
   const detailsButton = document.getElementById("details")
342 356
   const detailsText = document.getElementById("detailtext")
357
+  const actionBox = document.getElementById("actionbox")
343 358
   const listButton = document.getElementById("list")
344 359
   const disasmButton = document.getElementById("disasm")
345 360
   const resetButton = document.getElementById("reset")
361
+  const peekButton = document.getElementById("peek")
346 362
   const focusButton = document.getElementById("focus")
347 363
   const showButton = document.getElementById("show")
348 364
   const ignoreButton = document.getElementById("ignore")
@@ -351,7 +367,7 @@ function dotviewer(nodes) {
351 367
   const graph0 = document.getElementById("graph0")
352 368
   const svg = graph0.parentElement
353 369
 
354
-  let currentRe = ""
370
+  let regexpActive = false
355 371
   let selected = new Map()
356 372
   let origFill = new Map()
357 373
   let searchAlarm = null
@@ -370,20 +386,30 @@ function dotviewer(nodes) {
370 386
   function handleReset() { window.location.href = "/" }
371 387
   function handleList() { navigate("/weblist", "f", true) }
372 388
   function handleDisasm() { navigate("/disasm", "f", true) }
389
+  function handlePeek() { navigate("/peek", "f", true) }
373 390
   function handleFocus() { navigate("/", "f", false) }
374 391
   function handleShow() { navigate("/", "s", false) }
375 392
   function handleIgnore() { navigate("/", "i", false) }
376 393
   function handleHide() { navigate("/", "h", false) }
377 394
 
395
+  function handleKey(e) {
396
+    if (e.keyCode != 13) return
397
+    handleFocus()
398
+    e.preventDefault()
399
+  }
400
+
378 401
   function handleSearch() {
379
-    // Delay processing so a flurry of key strokes is handled once.
402
+    // Delay expensive processing so a flurry of key strokes is handled once.
380 403
     if (searchAlarm != null) {
381 404
       clearTimeout(searchAlarm)
382 405
     }
383
-    searchAlarm = setTimeout(doSearch, 300)
406
+    searchAlarm = setTimeout(selectMatching, 300)
407
+
408
+    regexpActive = true
409
+    updateButtons()
384 410
   }
385 411
 
386
-  function doSearch() {
412
+  function selectMatching() {
387 413
     searchAlarm = null
388 414
     let re = null
389 415
     if (search.value != "") {
@@ -394,7 +420,6 @@ function dotviewer(nodes) {
394 420
         return
395 421
       }
396 422
     }
397
-    currentRe = search.value
398 423
 
399 424
     function match(text) {
400 425
       return re != null && re.test(text)
@@ -425,7 +450,7 @@ function dotviewer(nodes) {
425 450
     if (!elem) return
426 451
 
427 452
     // Disable regexp mode.
428
-    currentRe = ""
453
+    regexpActive = false
429 454
 
430 455
     const n = nodeId(elem)
431 456
     if (n < 0) return
@@ -485,8 +510,8 @@ function dotviewer(nodes) {
485 510
   function navigate(path, param, newWindow) {
486 511
     // The selection can be in one of two modes: regexp-based or
487 512
     // list-based.  Construct regular expression depending on mode.
488
-    let re = currentRe
489
-    if (re == "") {
513
+    let re = regexpActive ? search.value : ""
514
+    if (!regexpActive) {
490 515
       selected.forEach(function(v, key) {
491 516
         if (re != "") re += "|"
492 517
         re += nodes[key]
@@ -517,15 +542,10 @@ function dotviewer(nodes) {
517 542
   }
518 543
 
519 544
   function updateButtons() {
520
-    const enable = (currentRe != "" || selected.size != 0)
545
+    const enable = (search.value != "" || selected.size != 0)
521 546
     if (buttonsEnabled == enable) return
522 547
     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
548
+    actionBox.style.display = enable ? "block" : "none"
529 549
   }
530 550
 
531 551
   // Initialize button states
@@ -536,9 +556,10 @@ function dotviewer(nodes) {
536 556
   
537 557
   function bindButtons(evt) {
538 558
     detailsButton.addEventListener(evt, handleDetails)
559
+    resetButton.addEventListener(evt, handleReset)
539 560
     listButton.addEventListener(evt, handleList)
540 561
     disasmButton.addEventListener(evt, handleDisasm)
541
-    resetButton.addEventListener(evt, handleReset)
562
+    peekButton.addEventListener(evt, handlePeek)
542 563
     focusButton.addEventListener(evt, handleFocus)
543 564
     showButton.addEventListener(evt, handleShow)
544 565
     ignoreButton.addEventListener(evt, handleIgnore)
@@ -547,6 +568,7 @@ function dotviewer(nodes) {
547 568
   bindButtons("click")
548 569
   bindButtons("touchstart")
549 570
   search.addEventListener("input", handleSearch)
571
+  search.addEventListener("keydown", handleKey)
550 572
 }
551 573
 
552 574
 dotviewer({{.Nodes}})

+ 21
- 4
internal/driver/webui.go Dosyayı Görüntüle

@@ -38,6 +38,7 @@ import (
38 38
 type webInterface struct {
39 39
 	prof    *profile.Profile
40 40
 	options *plugin.Options
41
+	help    map[string]string
41 42
 }
42 43
 
43 44
 // errorCatcher is a UI that captures errors for reporting to the browser.
@@ -56,6 +57,13 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) e
56 57
 	ui := &webInterface{
57 58
 		prof:    p,
58 59
 		options: o,
60
+		help:    make(map[string]string),
61
+	}
62
+	for n, c := range pprofCommands {
63
+		ui.help[n] = c.description
64
+	}
65
+	for n, v := range pprofVariables {
66
+		ui.help[n] = v.help
59 67
 	}
60 68
 
61 69
 	ln, url, isLocal, err := newListenerAndURL(hostport)
@@ -74,6 +82,7 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) e
74 82
 	mux.Handle("/", wrap(http.HandlerFunc(ui.dot)))
75 83
 	mux.Handle("/disasm", wrap(http.HandlerFunc(ui.disasm)))
76 84
 	mux.Handle("/weblist", wrap(http.HandlerFunc(ui.weblist)))
85
+	mux.Handle("/peek", wrap(http.HandlerFunc(ui.peek)))
77 86
 
78 87
 	s := &http.Server{Handler: mux}
79 88
 	go openBrowser(url, o)
@@ -204,12 +213,14 @@ func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
204 213
 		Svg    template.HTML
205 214
 		Legend []string
206 215
 		Nodes  []string
216
+		Help   map[string]string
207 217
 	}{
208 218
 		Title:  file + " " + profile,
209 219
 		Errors: catcher.errors,
210 220
 		Svg:    template.HTML(string(svg)),
211 221
 		Legend: legend,
212 222
 		Nodes:  nodes,
223
+		Help:   ui.help,
213 224
 	}
214 225
 	html := &bytes.Buffer{}
215 226
 	if err := graphTemplate.Execute(html, data); err != nil {
@@ -241,16 +252,23 @@ func dotToSvg(dot []byte) ([]byte, error) {
241 252
 
242 253
 // disasm generates a web page containing disassembly.
243 254
 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
244
-	ui.output(w, req, "disasm", "text/plain")
255
+	ui.output(w, req, "disasm", "text/plain", pprofVariables.makeCopy())
245 256
 }
246 257
 
247 258
 // weblist generates a web page containing disassembly.
248 259
 func (ui *webInterface) weblist(w http.ResponseWriter, req *http.Request) {
249
-	ui.output(w, req, "weblist", "text/html")
260
+	ui.output(w, req, "weblist", "text/html", pprofVariables.makeCopy())
261
+}
262
+
263
+// peek generates a web page listing callers/callers.
264
+func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
265
+	vars := pprofVariables.makeCopy()
266
+	vars.set("lines", "t") // Switch to line granularity
267
+	ui.output(w, req, "peek", "text/plain", vars)
250 268
 }
251 269
 
252 270
 // output generates a webpage that contains the output of the specified pprof cmd.
253
-func (ui *webInterface) output(w http.ResponseWriter, req *http.Request, cmd, ctype string) {
271
+func (ui *webInterface) output(w http.ResponseWriter, req *http.Request, cmd, ctype string, vars variables) {
254 272
 	focus := req.URL.Query().Get("f")
255 273
 	if focus == "" {
256 274
 		fmt.Fprintln(w, "no argument supplied for "+cmd)
@@ -263,7 +281,6 @@ func (ui *webInterface) output(w http.ResponseWriter, req *http.Request, cmd, ct
263 281
 	options.UI = catcher
264 282
 
265 283
 	args := []string{cmd, focus}
266
-	vars := pprofVariables.makeCopy()
267 284
 	_, rpt, err := generateRawReport(ui.prof, args, vars, &options)
268 285
 	if err != nil {
269 286
 		http.Error(w, err.Error(), http.StatusBadRequest)

+ 12
- 5
internal/driver/webui_test.go Dosyayı Görüntüle

@@ -22,7 +22,6 @@ import (
22 22
 	"net/url"
23 23
 	"os/exec"
24 24
 	"regexp"
25
-	"strings"
26 25
 	"testing"
27 26
 
28 27
 	"github.com/google/pprof/internal/plugin"
@@ -31,7 +30,11 @@ import (
31 30
 
32 31
 func TestWebInterface(t *testing.T) {
33 32
 	prof := makeFakeProfile()
34
-	ui := &webInterface{prof, &plugin.Options{Obj: fakeObjTool{}}}
33
+	ui := &webInterface{
34
+		prof:    prof,
35
+		options: &plugin.Options{Obj: fakeObjTool{}},
36
+		help:    make(map[string]string),
37
+	}
35 38
 
36 39
 	// Start test server.
37 40
 	server := httptest.NewServer(http.HandlerFunc(
@@ -41,6 +44,8 @@ func TestWebInterface(t *testing.T) {
41 44
 				ui.dot(w, r)
42 45
 			case "/disasm":
43 46
 				ui.disasm(w, r)
47
+			case "/peek":
48
+				ui.peek(w, r)
44 49
 			case "/weblist":
45 50
 				ui.weblist(w, r)
46 51
 			}
@@ -61,6 +66,8 @@ func TestWebInterface(t *testing.T) {
61 66
 		{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
62 67
 		{"/weblist?f=" + url.QueryEscape("F[12]"),
63 68
 			[]string{"F1", "F2", "300ms line1"}, false},
69
+		{"/peek?f=" + url.QueryEscape("F[12]"),
70
+			[]string{"300ms.*F1", "200ms.*300ms.*F2"}, false},
64 71
 		{"/disasm?f=" + url.QueryEscape("F[12]"),
65 72
 			[]string{"f1:asm", "f2:asm"}, false},
66 73
 	}
@@ -82,9 +89,9 @@ func TestWebInterface(t *testing.T) {
82 89
 		}
83 90
 		result := string(data)
84 91
 		for _, w := range c.want {
85
-			if !strings.Contains(result, w) {
86
-				t.Errorf("response for %s does not contain "+
87
-					"expected string '%s'; "+
92
+			if match, _ := regexp.MatchString(w, result); !match {
93
+				t.Errorf("response for %s does not match "+
94
+					"expected pattern '%s'; "+
88 95
 					"actual result:\n%s", c.path, w, result)
89 96
 			}
90 97
 		}